diff --git a/README.md b/README.md index 040da70..e1d186d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ It encrypts all data with both AES256-GCM and ChaCha20-Poly1305. Passphrases 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. -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 incorrect passphrase during encryption. @@ -20,3 +21,16 @@ Future versions may not be capable of decrypting the output of the current versi # Usage 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). diff --git a/daemon/2server.go b/daemon/2server.go index d4dfc09..9883bb6 100644 --- a/daemon/2server.go +++ b/daemon/2server.go @@ -20,17 +20,17 @@ type RCWService struct{} // the global passphrase and returns the decrypted data func (h *RCWService) DecryptRequest(encBytes []byte, reply *[]byte) error { var err error - *reply, err = wrappers.Decrypt(encBytes, globalPassphrase) + *reply, err = wrappers.DecryptAndZeroizePassphrase(encBytes, append([]byte{}, globalPassphrase...)) // pass new slice to avoid zeroizing cached passphrase) if err != nil { return err } return nil } -// EncryptRequest is the RPC method that encrypts the incoming data using +// EncryptRequestAndZeroizeDecBytes is the RPC method that encrypts the incoming data using // the global passphrase and returns the encrypted data -func (h *RCWService) EncryptRequest(decBytes []byte, reply *[]byte) error { - *reply = wrappers.Encrypt(decBytes, globalPassphrase) +func (h *RCWService) EncryptRequestAndZeroizeDecBytes(decBytes []byte, reply *[]byte) error { + *reply = wrappers.EncryptAndZeroizeDecBytesAndPassphrase(decBytes, append([]byte{}, globalPassphrase...)) // pass new slice to avoid zeroizing cached passphrase return nil } @@ -39,5 +39,6 @@ func getFileHash(path string) []byte { file, _ := os.Open(path) hash := sha256.New() io.Copy(hash, file) + file.Close() return hash.Sum(nil) } diff --git a/daemon/2serverUNIXGeneric.go b/daemon/2serverUNIXGeneric.go index 7324d5b..5cada59 100644 --- a/daemon/2serverUNIXGeneric.go +++ b/daemon/2serverUNIXGeneric.go @@ -13,7 +13,7 @@ import ( "syscall" "time" - peercred "github.com/rwinkhart/peercred-mini" + "github.com/rwinkhart/peercred-mini" ) // Start is the entry point for the RPC server responsible for diff --git a/daemon/3client.go b/daemon/3client.go index e77aaa7..941a765 100644 --- a/daemon/3client.go +++ b/daemon/3client.go @@ -4,6 +4,8 @@ import ( "log" "net" "net/rpc" + + "github.com/rwinkhart/go-boilerplate/security" ) // GetDec requests the RCW daemon to decrypt the given data. @@ -21,17 +23,19 @@ func GetDec(encBytes []byte) []byte { return decBytes } -// GetEnc requests the RCW daemon to encrypt the given data. +// GetEncAndZeroizeDecBytes requests the RCW daemon to encrypt the given data. // It returns the encrypted data. -func GetEnc(decBytes []byte) []byte { +func GetEncAndZeroizeDecBytes(decBytes []byte) []byte { conn, client := connectToDaemon() defer conn.Close() defer client.Close() // request encBytes from the RPC server var encBytes []byte - if err := client.Call("RCWService.EncryptRequest", decBytes, &encBytes); err != nil { - log.Fatalf("Error calling RCWService.EncryptRequest: %v", err) + err := client.Call("RCWService.EncryptRequestAndZeroizeDecBytes", decBytes, &encBytes) + security.ZeroizeBytes(decBytes) + if err != nil { + log.Fatalf("Error calling RCWService.EncryptRequestAndZeroizeDecBytes: %v", err) } return encBytes } diff --git a/example.go b/example.go index fa1f420..5f597c6 100644 --- a/example.go +++ b/example.go @@ -66,7 +66,7 @@ func main() { if daemon.IsOpen() { decBytes = daemon.GetDec(encBytes) } else { - decBytes, err = wrappers.Decrypt(encBytes, front.InputHidden("Enter RCW passphrase:")) + decBytes, err = wrappers.DecryptAndZeroizePassphrase(encBytes, front.InputHidden("Enter RCW passphrase:")) if err != nil { fmt.Println(err) return @@ -91,7 +91,7 @@ func main() { if os.Args[1] == "init" { // create sanity check file // rcw init - err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2])) + err := wrappers.GenSanityCheckAndZeroizePassphrase(sanityFile, []byte(os.Args[2])) if err != nil { fmt.Println(err) } @@ -102,15 +102,15 @@ func main() { decBytes := []byte(os.Args[2]) var encBytes []byte if daemon.IsOpen() { - encBytes = daemon.GetEnc(decBytes) + encBytes = daemon.GetEncAndZeroizeDecBytes(decBytes) } else { passphrase := front.InputHidden("Enter RCW passphrase: ") - err := wrappers.RunSanityCheck(sanityFile, passphrase) + err := wrappers.RunSanityCheck(sanityFile, append([]byte{}, passphrase...)) // pass new slice to avoid zeroizing passphrase) if err != nil { fmt.Println(err) return } - encBytes = wrappers.Encrypt(decBytes, passphrase) + encBytes = wrappers.EncryptAndZeroizeDecBytesAndPassphrase(decBytes, passphrase) } os.WriteFile(outputFile, encBytes, 0600) return diff --git a/wrappers/aes.go b/wrappers/aes.go index c780fc3..d6dbff9 100644 --- a/wrappers/aes.go +++ b/wrappers/aes.go @@ -4,6 +4,8 @@ import ( "crypto/aes" "crypto/cipher" "errors" + + "github.com/rwinkhart/go-boilerplate/security" ) const ( @@ -22,6 +24,7 @@ func encryptAES(decBytes, key2, salt2 []byte) []byte { // generate a random nonce nonce := getRandomBytes(nonceSizeAES) + defer security.ZeroizeBytes(nonce) // encrypt the data ciphertext := aesGCM.Seal(nil, nonce, decBytes, nil) @@ -46,6 +49,7 @@ func decryptAES(encBytes, key1 []byte) ([]byte, error) { // create AES-256 cipher block, _ := aes.NewCipher(key2) + security.ZeroizeBytes(key2) // create GCM mode aesGCM, _ := cipher.NewGCM(block) diff --git a/wrappers/chacha.go b/wrappers/chacha.go index a75ce4d..5f62d76 100644 --- a/wrappers/chacha.go +++ b/wrappers/chacha.go @@ -3,6 +3,7 @@ package wrappers import ( "errors" + "github.com/rwinkhart/go-boilerplate/security" "golang.org/x/crypto/chacha20poly1305" ) @@ -17,6 +18,7 @@ func encryptCha(decBytes, key2, salt2 []byte) []byte { // generate a random nonce nonce := getRandomBytes(nonceSizeCha) + defer security.ZeroizeBytes(nonce) // encrypt the data ciphertext := stream.Seal(nil, nonce, decBytes, nil) @@ -41,6 +43,7 @@ func decryptCha(encBytes, key1 []byte) ([]byte, error) { // create ChaCha20-Poly1305 cipher stream, _ := chacha20poly1305.NewX(key2) + security.ZeroizeBytes(key2) // decrypt the data plaintext, err := stream.Open(nil, nonce, ciphertext, nil) diff --git a/wrappers/highLevel.go b/wrappers/highLevel.go index d6e2e32..9e7eca5 100644 --- a/wrappers/highLevel.go +++ b/wrappers/highLevel.go @@ -1,15 +1,21 @@ package wrappers -import "errors" +import ( + "errors" -// Decrypt decrypts the provided byte slice using the provided passphrase. -func Decrypt(encBytes, passphrase []byte) ([]byte, error) { + "github.com/rwinkhart/go-boilerplate/security" +) + +// DecryptAndZeroizePassphrase decrypts the provided byte slice using the provided passphrase. +func DecryptAndZeroizePassphrase(encBytes, passphrase []byte) ([]byte, error) { if len(encBytes) < saltSize1 { - return nil, errors.New("High-level decrypt: Encrypted data is too short (invalid Argon2 salt)") + return nil, errors.New("high-level decrypt: encrypted data is too short (invalid Argon2 salt)") } salt1 := encBytes[:saltSize1] encBytes = encBytes[saltSize1:] key1 := derivePrimaryKey(passphrase, salt1) + security.ZeroizeBytes(passphrase) + security.ZeroizeBytes(salt1) var err error encBytes, err = decryptCha(encBytes, key1) if err != nil { @@ -19,19 +25,28 @@ func Decrypt(encBytes, passphrase []byte) ([]byte, error) { if err != nil { return nil, err } + security.ZeroizeBytes(key1) return encBytes, err } -// Encrypt encrypts the provided byte slice using the provided passphrase. -func Encrypt(decBytes, passphrase []byte) []byte { +// EncryptAndZeroizeDecBytesAndPassphrase encrypts the provided byte slice using the provided passphrase. +func EncryptAndZeroizeDecBytesAndPassphrase(decBytes, passphrase []byte) []byte { + defer security.ZeroizeBytes(decBytes) salt1 := getRandomBytes(saltSize1) + defer security.ZeroizeBytes(salt1) salt2AES := getRandomBytes(saltSize2) salt2Cha := getRandomBytes(saltSize2) key1 := derivePrimaryKey(passphrase, salt1) + security.ZeroizeBytes(passphrase) key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES)) key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha)) + security.ZeroizeBytes(key1) decBytes = encryptAES(decBytes, key2AES, salt2AES) + security.ZeroizeBytes(key2AES) + security.ZeroizeBytes(salt2AES) decBytes = encryptCha(decBytes, key2Cha, salt2Cha) + security.ZeroizeBytes(key2Cha) + security.ZeroizeBytes(salt2Cha) // format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext) return append(append(make([]byte, 0, saltSize1+len(decBytes)), salt1...), decBytes...) } diff --git a/wrappers/sanityCheck.go b/wrappers/sanityCheck.go index d92778b..7210ecc 100644 --- a/wrappers/sanityCheck.go +++ b/wrappers/sanityCheck.go @@ -1,15 +1,19 @@ package wrappers import ( + "bytes" "errors" "os" + + "github.com/rwinkhart/go-boilerplate/security" ) -// GenSanityCheck creates an encrypted file containing known plaintext +// GenSanityCheckAndZeroizePassphrase creates an encrypted file containing known plaintext // to later be used for ensuring the user does not encrypt data with // an incorrect passphrase. -func GenSanityCheck(path string, passphrase []byte) error { - err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), passphrase), 0600) +func GenSanityCheckAndZeroizePassphrase(path string, passphrase []byte) error { + err := os.WriteFile(path, EncryptAndZeroizeDecBytesAndPassphrase([]byte("thx4usin'rcw"), passphrase), 0600) + security.ZeroizeBytes(passphrase) return err } @@ -21,9 +25,11 @@ func RunSanityCheck(path string, passphrase []byte) error { if err != nil { return errors.New("Failed to read sanity check file (" + path + ")") } - decBytes, _ := Decrypt(encBytes, passphrase) - if string(decBytes) == "thx4usin'rcw" { - return nil + decBytes, err := DecryptAndZeroizePassphrase(encBytes, passphrase) + if err == 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 passphrase)") }