mirror of
https://github.com/rwinkhart/rcw.git
synced 2026-08-28 04:46:42 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8318b2e325 | ||
|
|
ba4a2c8396 | ||
|
|
718a2676c2 | ||
|
|
e79adb82a0 | ||
|
|
e8ef243e87 | ||
|
|
ddd7f893e9 | ||
|
|
f49248930b | ||
|
|
1745b85662 | ||
|
|
c5e259a446 | ||
|
|
4fd72b0042 | ||
|
|
57eefdf8b4 |
@@ -3,12 +3,13 @@ RCW is a cascading symmetric cryptography agent meant to be embedded within Go p
|
|||||||
|
|
||||||
It encrypts all data with both AES256-GCM and ChaCha20-Poly1305.
|
It encrypts all data with both AES256-GCM and ChaCha20-Poly1305.
|
||||||
|
|
||||||
Passphrases are securely cached for three minutes and RPC authentication is used to
|
Passwords are securely cached for three minutes and RPC authentication is used to
|
||||||
ensure that only the binary+user responsible for caching the passphrase can utilize it.
|
ensure that only the binary+user responsible for caching the password can utilize it.
|
||||||
This feature is supported on Linux, FreeBSD, MacOS, and Windows.
|
This feature is supported on Linux, FreeBSD, MacOS, and Windows. It is disabled if
|
||||||
|
built with `-tags=interactive`.
|
||||||
|
|
||||||
RCW also features a sanity check to ensure no data loss occurs due to a user entering the
|
RCW also features a sanity check to ensure no data loss occurs due to a user entering the
|
||||||
incorrect passphrase during encryption.
|
incorrect password during encryption.
|
||||||
|
|
||||||
Please note that RCW is a work-in-progress and breaking changes should be expected.
|
Please note that RCW is a work-in-progress and breaking changes should be expected.
|
||||||
Future versions may not be capable of decrypting the output of the current version.
|
Future versions may not be capable of decrypting the output of the current version.
|
||||||
@@ -20,3 +21,16 @@ Future versions may not be capable of decrypting the output of the current versi
|
|||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
For now, please reference [example.go](https://github.com/rwinkhart/randalls-cryptographic-wrappers/blob/main/example.go).
|
For now, please reference [example.go](https://github.com/rwinkhart/randalls-cryptographic-wrappers/blob/main/example.go).
|
||||||
|
|
||||||
|
# IMPORTANT - READ FOR FreeBSD+Windows SUPPORT w/RCWD!
|
||||||
|
There are replacements in the `go.mod` for this module. Make sure you maintain those
|
||||||
|
same replacements in your importing module, otherwise FreeBSD and Windows support will break
|
||||||
|
(RCWD support for those operating systems requires patched modules).
|
||||||
|
|
||||||
|
# runtime/secret.Do()?
|
||||||
|
This module does not yet make use of the new `secret.Do()` function in Go 1.26.
|
||||||
|
The new function is currently in an experimental state and does not function on
|
||||||
|
non-Linux platforms, thus the current approach to zeroizing in-memory secrets is
|
||||||
|
a manual one that will work everywhere. If `secret.Do()` becomes more complete in
|
||||||
|
a future Go version, it will likely be adopted here (on top of the current manual
|
||||||
|
approach).
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux || freebsd
|
//go:build linux || freebsd || solaris || illumos
|
||||||
|
|
||||||
package daemon
|
package daemon
|
||||||
|
|
||||||
|
|||||||
+13
-8
@@ -2,6 +2,7 @@ package daemon
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -11,16 +12,16 @@ import (
|
|||||||
var Timeout = 300 // seconds for RPC server timeout; configurable
|
var Timeout = 300 // seconds for RPC server timeout; configurable
|
||||||
|
|
||||||
var daemonHash []byte
|
var daemonHash []byte
|
||||||
var globalPassphrase []byte
|
var globalPassword []byte
|
||||||
|
|
||||||
// RCWService provides an RPC method.
|
// RCWService provides an RPC method.
|
||||||
type RCWService struct{}
|
type RCWService struct{}
|
||||||
|
|
||||||
// DecryptRequest is the RPC method that decrypts the incoming data using
|
// DecryptRequest is the RPC method that decrypts the incoming data using
|
||||||
// the global passphrase and returns the decrypted data
|
// the global password and returns the decrypted data
|
||||||
func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
|
func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
|
||||||
var err error
|
var err error
|
||||||
*reply, err = wrappers.Decrypt(encBytes, globalPassphrase)
|
*reply, err = wrappers.Decrypt(encBytes, globalPassword, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -28,16 +29,20 @@ func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EncryptRequest is the RPC method that encrypts the incoming data using
|
// EncryptRequest is the RPC method that encrypts the incoming data using
|
||||||
// the global passphrase and returns the encrypted data
|
// the global password and returns the encrypted data
|
||||||
func (h *RCWService) EncryptRequest(decBytes []byte, reply *[]byte) error {
|
func (h *RCWService) EncryptRequest(decBytes []byte, reply *[]byte) error {
|
||||||
*reply = wrappers.Encrypt(decBytes, globalPassphrase)
|
*reply = wrappers.Encrypt(decBytes, globalPassword, true, false)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFileHash returns the SHA256 hash of the file at the given path.
|
// getFileHash returns the SHA256 hash of the file at the given path.
|
||||||
func getFileHash(path string) []byte {
|
func getFileHash(path string) ([]byte, error) {
|
||||||
file, _ := os.Open(path)
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("unable to read path (" + path + ") for hashing: " + err.Error())
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
io.Copy(hash, file)
|
io.Copy(hash, file)
|
||||||
return hash.Sum(nil)
|
return hash.Sum(nil), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//go:build solaris || illumos
|
||||||
|
|
||||||
|
package daemon
|
||||||
|
|
||||||
|
const pidPathFile = "path/a.out"
|
||||||
@@ -13,22 +13,27 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
peercred "github.com/rwinkhart/peercred-mini"
|
"github.com/rwinkhart/go-boilerplate/security"
|
||||||
|
"github.com/rwinkhart/peercred-mini"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start is the entry point for the RPC server responsible for
|
// Start is the entry point for the RPC server responsible for
|
||||||
// returning decrypted data to authenticated clients.
|
// returning decrypted data to authenticated clients.
|
||||||
func Start(passphrase []byte) {
|
func Start(password []byte) {
|
||||||
// store passphrase to be referenced by DecryptRequest method
|
// store password to be referenced by DecryptRequest method
|
||||||
globalPassphrase = passphrase
|
globalPassword = password
|
||||||
|
|
||||||
// register RCWService with the RPC package
|
// register RCWService with the RPC package
|
||||||
if err := rpc.Register(&RCWService{}); err != nil {
|
err := rpc.Register(&RCWService{})
|
||||||
|
if err != nil {
|
||||||
log.Fatalf("Error registering RPC service: %v", err)
|
log.Fatalf("Error registering RPC service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// store the hash of the daemon binary
|
// store the hash of the daemon binary
|
||||||
daemonHash = getFileHash(binPath)
|
daemonHash, err = getFileHash(binPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error hashing daemon binary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// listen on the Unix domain socket
|
// listen on the Unix domain socket
|
||||||
listener, err := net.Listen("unix", socketPath)
|
listener, err := net.Listen("unix", socketPath)
|
||||||
@@ -38,12 +43,13 @@ func Start(passphrase []byte) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
log.Printf("RPC daemon listening on unix://%s", socketPath)
|
log.Printf("RPC daemon listening on unix://%s", socketPath)
|
||||||
|
|
||||||
// capture sigterms to ensure listener is closed
|
// capture termination signals to ensure listener is closed
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
go func() {
|
go func() {
|
||||||
<-sigChan
|
<-sigChan
|
||||||
listener.Close()
|
listener.Close()
|
||||||
|
security.ZeroizeBytes(globalPassword)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -56,6 +62,7 @@ func Start(passphrase []byte) {
|
|||||||
if err.(net.Error).Timeout() {
|
if err.(net.Error).Timeout() {
|
||||||
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
|
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
|
||||||
listener.Close()
|
listener.Close()
|
||||||
|
security.ZeroizeBytes(globalPassword)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
log.Printf("Accept error: %v", err)
|
log.Printf("Accept error: %v", err)
|
||||||
@@ -69,22 +76,30 @@ func Start(passphrase []byte) {
|
|||||||
// handleConn verifies the identity of the client.
|
// handleConn verifies the identity of the client.
|
||||||
// It uses the file descriptor of the connection to get the PID of the client,
|
// It uses the file descriptor of the connection to get the PID of the client,
|
||||||
// which is then used to get the path of the client's executable and calculate its hash.
|
// which is then used to get the path of the client's executable and calculate its hash.
|
||||||
// The passphrase is only returned if the client's executable hash matches the daemon's hash
|
// The password is only returned if the client's executable hash matches the daemon's hash
|
||||||
// and if the request is coming from the same user.
|
// and if the request is coming from the same user.
|
||||||
// This ensures that only the binary the daemon is embedded in can retrieve the passphrase.
|
// This ensures that only the binary the daemon is embedded in can retrieve the password.
|
||||||
func handleConn(conn net.Conn, sigChan chan os.Signal) {
|
func handleConn(conn net.Conn, sigChan chan os.Signal) {
|
||||||
ucred := peercred.Get(conn)
|
ucred := peercred.Get(conn)
|
||||||
|
|
||||||
// check if the RPC call is coming from an identical binary and from the same user
|
// check if the RPC call is coming from an identical binary and from the same user
|
||||||
callingBinPath := pidToPath(ucred.PID)
|
callingBinPath := pidToPath(ucred.PID)
|
||||||
if ucred.UID == strconv.Itoa(os.Getuid()) && bytes.Equal(getFileHash(callingBinPath), daemonHash) {
|
callingBinHash, err := getFileHash(callingBinPath)
|
||||||
|
if err != nil {
|
||||||
|
// calling binary hash failure
|
||||||
|
conn.Close()
|
||||||
|
log.Printf("Failed to hash calling binary: PID(%d), UID(%s), Path(%s) - %v", ucred.PID, ucred.UID, callingBinPath, err)
|
||||||
|
sigChan <- syscall.SIGTERM // this zeroizes globalPassword and triggers os.Exit(0)
|
||||||
|
return // explicitly return to avoid race
|
||||||
|
}
|
||||||
|
if ucred.UID == strconv.Itoa(os.Getuid()) && bytes.Equal(callingBinHash, daemonHash) {
|
||||||
// valid client; hand off the connection to the RPC server
|
// valid client; hand off the connection to the RPC server
|
||||||
rpc.ServeConn(conn)
|
rpc.ServeConn(conn)
|
||||||
} else {
|
} else {
|
||||||
// invalid client; close the connection w/o a response,
|
// invalid client; close the connection w/o a response,
|
||||||
// log the client's path, and kill the daemon
|
// log the client's path, and kill the daemon
|
||||||
conn.Close()
|
conn.Close()
|
||||||
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file
|
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath)
|
||||||
sigChan <- syscall.SIGTERM
|
sigChan <- syscall.SIGTERM // this zeroizes globalPassword and triggers os.Exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-12
@@ -10,10 +10,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Microsoft/go-winio"
|
"github.com/Microsoft/go-winio"
|
||||||
|
"github.com/rwinkhart/go-boilerplate/security"
|
||||||
"github.com/rwinkhart/peercred-mini"
|
"github.com/rwinkhart/peercred-mini"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
@@ -24,17 +24,21 @@ const (
|
|||||||
|
|
||||||
// Start is the entry point for the RPC server responsible for
|
// Start is the entry point for the RPC server responsible for
|
||||||
// returning decrypted data to authenticated clients.
|
// returning decrypted data to authenticated clients.
|
||||||
func Start(passphrase []byte) {
|
func Start(password []byte) {
|
||||||
// store passphrase to be referenced by DecryptRequest method
|
// store password to be referenced by DecryptRequest method
|
||||||
globalPassphrase = passphrase
|
globalPassword = password
|
||||||
|
|
||||||
// register RCWService with the RPC package
|
// register RCWService with the RPC package
|
||||||
if err := rpc.Register(&RCWService{}); err != nil {
|
err := rpc.Register(&RCWService{})
|
||||||
|
if err != nil {
|
||||||
log.Fatalf("Error registering RPC service: %v", err)
|
log.Fatalf("Error registering RPC service: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// store the hash of the daemon binary
|
// store the hash of the daemon binary
|
||||||
daemonHash = getFileHash(binPath)
|
daemonHash, err = getFileHash(binPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error hashing daemon binary: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// configure the named pipe
|
// configure the named pipe
|
||||||
pipeConfig := &winio.PipeConfig{
|
pipeConfig := &winio.PipeConfig{
|
||||||
@@ -52,9 +56,9 @@ func Start(passphrase []byte) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
log.Printf("RPC daemon listening on %s", socketPath)
|
log.Printf("RPC daemon listening on %s", socketPath)
|
||||||
|
|
||||||
// capture sigterms to ensure listener is closed
|
// capture termination signals to ensure listener is closed
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigChan, os.Interrupt)
|
||||||
// create inactivity timer
|
// create inactivity timer
|
||||||
timer := time.NewTimer(time.Duration(Timeout) * time.Second)
|
timer := time.NewTimer(time.Duration(Timeout) * time.Second)
|
||||||
killTimer := make(chan struct{})
|
killTimer := make(chan struct{})
|
||||||
@@ -63,11 +67,13 @@ func Start(passphrase []byte) {
|
|||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
|
log.Println(strconv.Itoa(Timeout) + " seconds have passed without any connections. Exiting...")
|
||||||
listener.Close()
|
listener.Close()
|
||||||
|
security.ZeroizeBytes(globalPassword)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
case <-killTimer:
|
case <-killTimer:
|
||||||
return
|
return
|
||||||
case <-sigChan:
|
case <-sigChan:
|
||||||
listener.Close()
|
listener.Close()
|
||||||
|
security.ZeroizeBytes(globalPassword)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -102,14 +108,21 @@ func handleConn(conn net.Conn, sigChan chan os.Signal) {
|
|||||||
|
|
||||||
// check if the RPC call is coming from an identical binary and from the same user
|
// check if the RPC call is coming from an identical binary and from the same user
|
||||||
callingBinPath := pidToPath(uint32(ucred.PID))
|
callingBinPath := pidToPath(uint32(ucred.PID))
|
||||||
if ucred.UID == user.User.Sid.String() && bytes.Equal(getFileHash(callingBinPath), daemonHash) {
|
callingBinHash, err := getFileHash(callingBinPath)
|
||||||
|
if err != nil {
|
||||||
|
// calling binary hash failure
|
||||||
|
conn.Close()
|
||||||
|
log.Printf("Failed to hash calling binary: PID(%d), UID(%s), Path(%s) - %v", ucred.PID, ucred.UID, callingBinPath, err)
|
||||||
|
sigChan <- os.Interrupt // this zeroizes globalPassword and triggers os.Exit(0)
|
||||||
|
return // explicitly return to avoid race
|
||||||
|
}
|
||||||
|
if ucred.UID == user.User.Sid.String() && bytes.Equal(callingBinHash, daemonHash) {
|
||||||
rpc.ServeConn(conn)
|
rpc.ServeConn(conn)
|
||||||
} else {
|
} else {
|
||||||
// invalid client; close the connection w/o a response,
|
// invalid client; close the connection w/o a response,
|
||||||
// log the client's path, and kill the daemon
|
// log the client's path, and kill the daemon
|
||||||
conn.Close()
|
conn.Close()
|
||||||
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file
|
log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath)
|
||||||
sigChan <- syscall.SIGTERM
|
sigChan <- os.Interrupt // this zeroizes globalPassword and triggers os.Exit(0)
|
||||||
os.Exit(2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -4,6 +4,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/rpc"
|
"net/rpc"
|
||||||
|
|
||||||
|
"github.com/rwinkhart/go-boilerplate/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetDec requests the RCW daemon to decrypt the given data.
|
// GetDec requests the RCW daemon to decrypt the given data.
|
||||||
@@ -23,14 +25,18 @@ func GetDec(encBytes []byte) []byte {
|
|||||||
|
|
||||||
// GetEnc requests the RCW daemon to encrypt the given data.
|
// GetEnc requests the RCW daemon to encrypt the given data.
|
||||||
// It returns the encrypted data.
|
// It returns the encrypted data.
|
||||||
func GetEnc(decBytes []byte) []byte {
|
func GetEnc(decBytes []byte, zeroizeDecBytes bool) []byte {
|
||||||
conn, client := connectToDaemon()
|
conn, client := connectToDaemon()
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
|
||||||
// request encBytes from the RPC server
|
// request encBytes from the RPC server
|
||||||
var encBytes []byte
|
var encBytes []byte
|
||||||
if err := client.Call("RCWService.EncryptRequest", decBytes, &encBytes); err != nil {
|
err := client.Call("RCWService.EncryptRequest", decBytes, &encBytes)
|
||||||
|
if zeroizeDecBytes {
|
||||||
|
security.ZeroizeBytes(decBytes)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
log.Fatalf("Error calling RCWService.EncryptRequest: %v", err)
|
log.Fatalf("Error calling RCWService.EncryptRequest: %v", err)
|
||||||
}
|
}
|
||||||
return encBytes
|
return encBytes
|
||||||
|
|||||||
+17
-19
@@ -14,25 +14,25 @@ import (
|
|||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
// rcw init <passwd> : Generates the required sanity check file
|
// rcw init <passwd> : Generates the required sanity check file
|
||||||
// rcw <passphrase> : Runs the rcw daemon to decrypt data for three minutes
|
// rcw <password> : Runs the rcw daemon to decrypt data for three minutes
|
||||||
// rcw enc <text> : Encrypts the provided text and outputs the ciphertext to ex-cipher.rcw (attempts to use daemon, falls back to user input for passphrase)
|
// rcw enc <text> : Encrypts the provided text and outputs the ciphertext to ex-cipher.rcw (attempts to use daemon, falls back to user input for password)
|
||||||
// rcw dec : Decrypts ex-cipher.rcw and outputs the plaintext to stdout (attempts to use daemon, falls back to user input for passphrase)
|
// rcw dec : Decrypts ex-cipher.rcw and outputs the plaintext to stdout (attempts to use daemon, falls back to user input for password)
|
||||||
|
|
||||||
// Implementation Notes:
|
// Implementation Notes:
|
||||||
// There are two main ways to use the RCW library:
|
// There are two main ways to use the RCW library:
|
||||||
//
|
//
|
||||||
// 1. Daemon mode:
|
// 1. Daemon mode:
|
||||||
// The daemon is started with a passphrase and runs in the background.
|
// The daemon is started with a password and runs in the background.
|
||||||
// All encryption/decryption occurs in the daemon.
|
// All encryption/decryption occurs in the daemon.
|
||||||
// Avoid using the wrapper.Encrypt/Decrypt functions directly.
|
// Avoid using the wrapper.Encrypt/Decrypt functions directly.
|
||||||
// Instead, cache the passphrase with the daemon and use the daemon to encrypt/decrypt data.
|
// Instead, cache the password with the daemon and use the daemon to encrypt/decrypt data.
|
||||||
//
|
//
|
||||||
// 2. Standalone mode:
|
// 2. Standalone mode:
|
||||||
// The wrapper.Encrypt/Decrypt functions are used directly.
|
// The wrapper.Encrypt/Decrypt functions are used directly.
|
||||||
// The passphrase is provided directly to the functions.
|
// The password is provided directly to the functions.
|
||||||
//
|
//
|
||||||
// It is up to the client to perform the sanity check before encrypting data.
|
// It is up to the client to perform the sanity check before encrypting data.
|
||||||
// This means that when using the daemon to cache the passphrase, the client should
|
// This means that when using the daemon to cache the password, the client should
|
||||||
// perform the sanity check before activating the daemon.
|
// perform the sanity check before activating the daemon.
|
||||||
|
|
||||||
// TODO Tests:
|
// TODO Tests:
|
||||||
@@ -66,7 +66,7 @@ func main() {
|
|||||||
if daemon.IsOpen() {
|
if daemon.IsOpen() {
|
||||||
decBytes = daemon.GetDec(encBytes)
|
decBytes = daemon.GetDec(encBytes)
|
||||||
} else {
|
} else {
|
||||||
decBytes, err = wrappers.Decrypt(encBytes, front.InputHidden("Enter RCW passphrase:"))
|
decBytes, err = wrappers.Decrypt(encBytes, front.InputSecret("Enter RCW password:"), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
@@ -81,36 +81,34 @@ func main() {
|
|||||||
fmt.Println("Daemon already running")
|
fmt.Println("Daemon already running")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := wrappers.RunSanityCheck(sanityFile, []byte(os.Args[1]))
|
if err := wrappers.RunSanityCheck(sanityFile, []byte(os.Args[1])); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
daemon.Start([]byte(os.Args[1]))
|
daemon.Start([]byte(os.Args[1]))
|
||||||
case 3:
|
case 3:
|
||||||
if os.Args[1] == "init" {
|
switch os.Args[1] {
|
||||||
|
case "init":
|
||||||
// create sanity check file
|
// create sanity check file
|
||||||
// rcw init <passwd>
|
// rcw init <passwd>
|
||||||
err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2]))
|
if err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2]), true); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
} else if os.Args[1] == "enc" {
|
case "enc":
|
||||||
// encrypt data (using daemon if available)
|
// encrypt data (using daemon if available)
|
||||||
// rcw enc <data>
|
// rcw enc <data>
|
||||||
decBytes := []byte(os.Args[2])
|
decBytes := []byte(os.Args[2])
|
||||||
var encBytes []byte
|
var encBytes []byte
|
||||||
if daemon.IsOpen() {
|
if daemon.IsOpen() {
|
||||||
encBytes = daemon.GetEnc(decBytes)
|
encBytes = daemon.GetEnc(decBytes, true)
|
||||||
} else {
|
} else {
|
||||||
passphrase := front.InputHidden("Enter RCW passphrase: ")
|
password := front.InputSecret("Enter RCW password: ")
|
||||||
err := wrappers.RunSanityCheck(sanityFile, passphrase)
|
if err := wrappers.RunSanityCheck(sanityFile, password); err != nil {
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
encBytes = wrappers.Encrypt(decBytes, passphrase)
|
encBytes = wrappers.Encrypt(decBytes, password, true, true)
|
||||||
}
|
}
|
||||||
os.WriteFile(outputFile, encBytes, 0600)
|
os.WriteFile(outputFile, encBytes, 0600)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
module github.com/rwinkhart/rcw
|
module github.com/rwinkhart/rcw
|
||||||
|
|
||||||
go 1.25.4
|
go 1.26.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
github.com/rwinkhart/go-boilerplate v0.1.0
|
github.com/rwinkhart/go-boilerplate v0.3.1
|
||||||
github.com/rwinkhart/peercred-mini v0.1.2
|
github.com/rwinkhart/peercred-mini v0.1.5
|
||||||
golang.org/x/crypto v0.45.0
|
golang.org/x/crypto v0.52.0
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.45.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/term v0.37.0 // indirect
|
require golang.org/x/term v0.43.0 // indirect
|
||||||
|
|
||||||
replace golang.org/x/sys => github.com/rwinkhart/sys v0.38.0
|
replace golang.org/x/sys => github.com/rwinkhart/sys v0.45.0
|
||||||
|
|
||||||
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio v0.1.0
|
replace github.com/Microsoft/go-winio => github.com/rwinkhart/go-winio v0.1.1
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
github.com/rwinkhart/go-boilerplate v0.1.0 h1:EzlVj6R7Bxtl79Nl7R5zRcg6s+Cf2FAqGIzR4giWTQg=
|
github.com/rwinkhart/go-boilerplate v0.3.1 h1:vkVRuptO2s1yPzzwpvtiiB0c/hPB6yr0p1mzZ2Myb9E=
|
||||||
github.com/rwinkhart/go-boilerplate v0.1.0/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
|
github.com/rwinkhart/go-boilerplate v0.3.1/go.mod h1:ES13A2r9fnCVfyezwMBgY/RgA4pOIudOUXz3Jk/ikes=
|
||||||
github.com/rwinkhart/go-winio v0.1.0 h1:b72agLW+dETGmhR3VbcbwnStfgKfc5AfgJOXBJDkaHg=
|
github.com/rwinkhart/go-winio v0.1.1 h1:kAJKiqneR7cUR01Wn5/doAAV4kOGTEGPug4oinXc5N4=
|
||||||
github.com/rwinkhart/go-winio v0.1.0/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
|
github.com/rwinkhart/go-winio v0.1.1/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0=
|
||||||
github.com/rwinkhart/peercred-mini v0.1.2 h1:4cGWDbv0whvLeVvbUdx84V/9p+2fS+DEXgrA1KxlRFo=
|
github.com/rwinkhart/peercred-mini v0.1.5 h1:9sBR0ascteCJfzfhv7Gq/juqu7OOU+AphiEIG1+LcWA=
|
||||||
github.com/rwinkhart/peercred-mini v0.1.2/go.mod h1:LLHG7YshHEpbpJJP+Il9nx2dnGj5O3VGE32rWmflj0c=
|
github.com/rwinkhart/peercred-mini v0.1.5/go.mod h1:C73IeDteQwsKp1+PDdUBj4FC6dTziAJNbQsVDZSWE+M=
|
||||||
github.com/rwinkhart/sys v0.38.0 h1:V1PGKcUutWtD3+VdHvyvfGtPy7PDXzKHNM1IzVq1ysY=
|
github.com/rwinkhart/sys v0.45.0 h1:HXZL0SuyToqKkvakaUuRAdH8sPUHelfO6q4PQRpL26Q=
|
||||||
github.com/rwinkhart/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
github.com/rwinkhart/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/rwinkhart/go-boilerplate/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,6 +48,7 @@ func decryptAES(encBytes, key1 []byte) ([]byte, error) {
|
|||||||
|
|
||||||
// create AES-256 cipher
|
// create AES-256 cipher
|
||||||
block, _ := aes.NewCipher(key2)
|
block, _ := aes.NewCipher(key2)
|
||||||
|
security.ZeroizeBytes(key2)
|
||||||
|
|
||||||
// create GCM mode
|
// create GCM mode
|
||||||
aesGCM, _ := cipher.NewGCM(block)
|
aesGCM, _ := cipher.NewGCM(block)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package wrappers
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/rwinkhart/go-boilerplate/security"
|
||||||
"golang.org/x/crypto/chacha20poly1305"
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ func decryptCha(encBytes, key1 []byte) ([]byte, error) {
|
|||||||
|
|
||||||
// create ChaCha20-Poly1305 cipher
|
// create ChaCha20-Poly1305 cipher
|
||||||
stream, _ := chacha20poly1305.NewX(key2)
|
stream, _ := chacha20poly1305.NewX(key2)
|
||||||
|
security.ZeroizeBytes(key2)
|
||||||
|
|
||||||
// decrypt the data
|
// decrypt the data
|
||||||
plaintext, err := stream.Open(nil, nonce, ciphertext, nil)
|
plaintext, err := stream.Open(nil, nonce, ciphertext, nil)
|
||||||
|
|||||||
+32
-7
@@ -1,11 +1,24 @@
|
|||||||
package wrappers
|
package wrappers
|
||||||
|
|
||||||
// Decrypt decrypts the provided byte slice using the provided passphrase.
|
import (
|
||||||
func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) {
|
"errors"
|
||||||
var err error = nil
|
|
||||||
|
"github.com/rwinkhart/go-boilerplate/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Decrypt decrypts the provided byte slice using the provided password.
|
||||||
|
func Decrypt(encBytes, password []byte, zeroizePassword bool) ([]byte, error) {
|
||||||
|
if len(encBytes) < saltSize1 {
|
||||||
|
return nil, errors.New("high-level decrypt: encrypted data is too short (invalid Argon2 salt)")
|
||||||
|
}
|
||||||
salt1 := encBytes[:saltSize1]
|
salt1 := encBytes[:saltSize1]
|
||||||
encBytes = encBytes[saltSize1:]
|
encBytes = encBytes[saltSize1:]
|
||||||
key1 := derivePrimaryKey(passphrase, salt1)
|
key1 := derivePrimaryKey(password, salt1)
|
||||||
|
defer security.ZeroizeBytes(key1)
|
||||||
|
if zeroizePassword {
|
||||||
|
security.ZeroizeBytes(password)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
encBytes, err = decryptCha(encBytes, key1)
|
encBytes, err = decryptCha(encBytes, key1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -17,16 +30,28 @@ func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) {
|
|||||||
return encBytes, err
|
return encBytes, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt encrypts the provided byte slice using the provided passphrase.
|
// Encrypt encrypts the provided byte slice using the provided password.
|
||||||
func Encrypt(decBytes []byte, passphrase []byte) []byte {
|
func Encrypt(decBytes, password []byte, zeroizeDecBytes, zeroizePassword bool) []byte {
|
||||||
|
if zeroizeDecBytes {
|
||||||
|
defer security.ZeroizeBytes(decBytes)
|
||||||
|
}
|
||||||
salt1 := getRandomBytes(saltSize1)
|
salt1 := getRandomBytes(saltSize1)
|
||||||
|
defer security.ZeroizeBytes(salt1)
|
||||||
salt2AES := getRandomBytes(saltSize2)
|
salt2AES := getRandomBytes(saltSize2)
|
||||||
salt2Cha := getRandomBytes(saltSize2)
|
salt2Cha := getRandomBytes(saltSize2)
|
||||||
key1 := derivePrimaryKey(passphrase, salt1)
|
key1 := derivePrimaryKey(password, salt1)
|
||||||
|
if zeroizePassword {
|
||||||
|
security.ZeroizeBytes(password)
|
||||||
|
}
|
||||||
key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES))
|
key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES))
|
||||||
key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha))
|
key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha))
|
||||||
|
security.ZeroizeBytes(key1)
|
||||||
decBytes = encryptAES(decBytes, key2AES, salt2AES)
|
decBytes = encryptAES(decBytes, key2AES, salt2AES)
|
||||||
|
security.ZeroizeBytes(key2AES)
|
||||||
|
security.ZeroizeBytes(salt2AES)
|
||||||
decBytes = encryptCha(decBytes, key2Cha, salt2Cha)
|
decBytes = encryptCha(decBytes, key2Cha, salt2Cha)
|
||||||
|
security.ZeroizeBytes(key2Cha)
|
||||||
|
security.ZeroizeBytes(salt2Cha)
|
||||||
// format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext)
|
// format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext)
|
||||||
return append(append(make([]byte, 0, saltSize1+len(decBytes)), salt1...), decBytes...)
|
return append(append(make([]byte, 0, saltSize1+len(decBytes)), salt1...), decBytes...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ const (
|
|||||||
saltSize2 = 32 // 256 bits, recommended salt size for HKDF
|
saltSize2 = 32 // 256 bits, recommended salt size for HKDF
|
||||||
)
|
)
|
||||||
|
|
||||||
// derivePrimaryKey derives an encryption key from a passphrase using Argon2.
|
// derivePrimaryKey derives an encryption key from a password using Argon2.
|
||||||
// The resulting key is not meant to be used directly for encryption, but rather as a key to derive other keys.
|
// The resulting key is not meant to be used directly for encryption, but rather as a key to derive other keys.
|
||||||
func derivePrimaryKey(passphrase, salt []byte) []byte {
|
func derivePrimaryKey(password, salt []byte) []byte {
|
||||||
return argon2.IDKey(passphrase, salt, argonTime, argonMemory, argonThreads, keyLen)
|
return argon2.IDKey(password, salt, argonTime, argonMemory, argonThreads, keyLen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deriveSecondaryKey derives a secondary key from the primary key using HKDF.
|
// deriveSecondaryKey derives a secondary key from the primary key using HKDF.
|
||||||
// It is meant to be an efficient way to derive multiple keys from a single passphrase.
|
// It is meant to be an efficient way to derive multiple keys from a single password.
|
||||||
func deriveSecondaryKey(primaryKey, salt, info []byte) []byte {
|
func deriveSecondaryKey(primaryKey, salt, info []byte) []byte {
|
||||||
h := hkdf.New(sha256.New, primaryKey, salt, info)
|
h := hkdf.New(sha256.New, primaryKey, salt, info)
|
||||||
derivedKey := make([]byte, keyLen)
|
derivedKey := make([]byte, keyLen)
|
||||||
|
|||||||
+13
-10
@@ -1,29 +1,32 @@
|
|||||||
package wrappers
|
package wrappers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenSanityCheck creates an encrypted file containing known plaintext
|
// GenSanityCheck creates an encrypted file containing known plaintext
|
||||||
// to later be used for ensuring the user does not encrypt data with
|
// to later be used for ensuring the user does not encrypt data with
|
||||||
// an incorrect passphrase.
|
// an incorrect password.
|
||||||
func GenSanityCheck(path string, passphrase []byte) error {
|
func GenSanityCheck(path string, password []byte, zeroizePassword bool) error {
|
||||||
err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), passphrase), 0600)
|
err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), password, false, zeroizePassword), 0600)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunSanityCheck should be run before any encryption operation
|
// RunSanityCheck should be run before any encryption operation
|
||||||
// to ensure the user does not encrypt data with an incorrect passphrase.
|
// to ensure the user does not encrypt data with an incorrect password.
|
||||||
// Failure to perform this check could result in data loss.
|
// Failure to perform this check could result in data loss.
|
||||||
func RunSanityCheck(path string, passphrase []byte) error {
|
func RunSanityCheck(path string, password []byte) error {
|
||||||
encBytes, err := os.ReadFile(path)
|
encBytes, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Failed to read sanity check file (" + path + ")")
|
return errors.New("unable to read sanity check file (" + path + "): " + err.Error())
|
||||||
}
|
}
|
||||||
decBytes, _ := Decrypt(encBytes, passphrase)
|
decBytes, err := Decrypt(encBytes, password, false)
|
||||||
if string(decBytes) == "thx4usin'rcw" {
|
if err == nil {
|
||||||
return nil
|
if bytes.Equal(decBytes, []byte("thx4usin'rcw")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return errors.New("Sanity check failed (likely due to inconsistent passphrase)")
|
return errors.New("sanity check failed (likely due to inconsistent password)")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user