Significant hardening (always work on bytes; zeroize everything); address JetBrains warnings

This commit is contained in:
2026-02-10 00:33:56 -05:00
parent 4fd72b0042
commit c5e259a446
9 changed files with 75 additions and 28 deletions
+15 -1
View File
@@ -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 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. 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 RCW also features a sanity check to ensure no data loss occurs due to a user entering the
incorrect passphrase during encryption. incorrect passphrase during encryption.
@@ -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).
+5 -4
View File
@@ -20,17 +20,17 @@ type RCWService struct{}
// the global passphrase and returns the decrypted data // the global passphrase 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.DecryptAndZeroizePassphrase(encBytes, append([]byte{}, globalPassphrase...)) // pass new slice to avoid zeroizing cached passphrase)
if err != nil { if err != nil {
return err return err
} }
return nil 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 // the global passphrase and returns the encrypted data
func (h *RCWService) EncryptRequest(decBytes []byte, reply *[]byte) error { func (h *RCWService) EncryptRequestAndZeroizeDecBytes(decBytes []byte, reply *[]byte) error {
*reply = wrappers.Encrypt(decBytes, globalPassphrase) *reply = wrappers.EncryptAndZeroizeDecBytesAndPassphrase(decBytes, append([]byte{}, globalPassphrase...)) // pass new slice to avoid zeroizing cached passphrase
return nil return nil
} }
@@ -39,5 +39,6 @@ func getFileHash(path string) []byte {
file, _ := os.Open(path) file, _ := os.Open(path)
hash := sha256.New() hash := sha256.New()
io.Copy(hash, file) io.Copy(hash, file)
file.Close()
return hash.Sum(nil) return hash.Sum(nil)
} }
+1 -1
View File
@@ -13,7 +13,7 @@ import (
"syscall" "syscall"
"time" "time"
peercred "github.com/rwinkhart/peercred-mini" "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
+8 -4
View File
@@ -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.
@@ -21,17 +23,19 @@ func GetDec(encBytes []byte) []byte {
return decBytes 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. // It returns the encrypted data.
func GetEnc(decBytes []byte) []byte { func GetEncAndZeroizeDecBytes(decBytes []byte) []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.EncryptRequestAndZeroizeDecBytes", decBytes, &encBytes)
log.Fatalf("Error calling RCWService.EncryptRequest: %v", err) security.ZeroizeBytes(decBytes)
if err != nil {
log.Fatalf("Error calling RCWService.EncryptRequestAndZeroizeDecBytes: %v", err)
} }
return encBytes return encBytes
} }
+5 -5
View File
@@ -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.DecryptAndZeroizePassphrase(encBytes, front.InputHidden("Enter RCW passphrase:"))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
@@ -91,7 +91,7 @@ func main() {
if os.Args[1] == "init" { if os.Args[1] == "init" {
// create sanity check file // create sanity check file
// rcw init <passwd> // rcw init <passwd>
err := wrappers.GenSanityCheck(sanityFile, []byte(os.Args[2])) err := wrappers.GenSanityCheckAndZeroizePassphrase(sanityFile, []byte(os.Args[2]))
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
@@ -102,15 +102,15 @@ func main() {
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.GetEncAndZeroizeDecBytes(decBytes)
} else { } else {
passphrase := front.InputHidden("Enter RCW passphrase: ") 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 { if err != nil {
fmt.Println(err) fmt.Println(err)
return return
} }
encBytes = wrappers.Encrypt(decBytes, passphrase) encBytes = wrappers.EncryptAndZeroizeDecBytesAndPassphrase(decBytes, passphrase)
} }
os.WriteFile(outputFile, encBytes, 0600) os.WriteFile(outputFile, encBytes, 0600)
return return
+4
View File
@@ -4,6 +4,8 @@ import (
"crypto/aes" "crypto/aes"
"crypto/cipher" "crypto/cipher"
"errors" "errors"
"github.com/rwinkhart/go-boilerplate/security"
) )
const ( const (
@@ -22,6 +24,7 @@ func encryptAES(decBytes, key2, salt2 []byte) []byte {
// generate a random nonce // generate a random nonce
nonce := getRandomBytes(nonceSizeAES) nonce := getRandomBytes(nonceSizeAES)
defer security.ZeroizeBytes(nonce)
// encrypt the data // encrypt the data
ciphertext := aesGCM.Seal(nil, nonce, decBytes, nil) ciphertext := aesGCM.Seal(nil, nonce, decBytes, nil)
@@ -46,6 +49,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
View File
@@ -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"
) )
@@ -17,6 +18,7 @@ func encryptCha(decBytes, key2, salt2 []byte) []byte {
// generate a random nonce // generate a random nonce
nonce := getRandomBytes(nonceSizeCha) nonce := getRandomBytes(nonceSizeCha)
defer security.ZeroizeBytes(nonce)
// encrypt the data // encrypt the data
ciphertext := stream.Seal(nil, nonce, decBytes, nil) ciphertext := stream.Seal(nil, nonce, decBytes, nil)
@@ -41,6 +43,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)
+21 -6
View File
@@ -1,15 +1,21 @@
package wrappers package wrappers
import "errors" import (
"errors"
// Decrypt decrypts the provided byte slice using the provided passphrase. "github.com/rwinkhart/go-boilerplate/security"
func Decrypt(encBytes, passphrase []byte) ([]byte, error) { )
// DecryptAndZeroizePassphrase decrypts the provided byte slice using the provided passphrase.
func DecryptAndZeroizePassphrase(encBytes, passphrase []byte) ([]byte, error) {
if len(encBytes) < saltSize1 { 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] salt1 := encBytes[:saltSize1]
encBytes = encBytes[saltSize1:] encBytes = encBytes[saltSize1:]
key1 := derivePrimaryKey(passphrase, salt1) key1 := derivePrimaryKey(passphrase, salt1)
security.ZeroizeBytes(passphrase)
security.ZeroizeBytes(salt1)
var err error var err error
encBytes, err = decryptCha(encBytes, key1) encBytes, err = decryptCha(encBytes, key1)
if err != nil { if err != nil {
@@ -19,19 +25,28 @@ func Decrypt(encBytes, passphrase []byte) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
security.ZeroizeBytes(key1)
return encBytes, err return encBytes, err
} }
// Encrypt encrypts the provided byte slice using the provided passphrase. // EncryptAndZeroizeDecBytesAndPassphrase encrypts the provided byte slice using the provided passphrase.
func Encrypt(decBytes, passphrase []byte) []byte { func EncryptAndZeroizeDecBytesAndPassphrase(decBytes, passphrase []byte) []byte {
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(passphrase, salt1)
security.ZeroizeBytes(passphrase)
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...)
} }
+13 -7
View File
@@ -1,15 +1,19 @@
package wrappers package wrappers
import ( import (
"bytes"
"errors" "errors"
"os" "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 // to later be used for ensuring the user does not encrypt data with
// an incorrect passphrase. // an incorrect passphrase.
func GenSanityCheck(path string, passphrase []byte) error { func GenSanityCheckAndZeroizePassphrase(path string, passphrase []byte) error {
err := os.WriteFile(path, Encrypt([]byte("thx4usin'rcw"), passphrase), 0600) err := os.WriteFile(path, EncryptAndZeroizeDecBytesAndPassphrase([]byte("thx4usin'rcw"), passphrase), 0600)
security.ZeroizeBytes(passphrase)
return err return err
} }
@@ -21,9 +25,11 @@ func RunSanityCheck(path string, passphrase []byte) error {
if err != nil { if err != nil {
return errors.New("Failed to read sanity check file (" + path + ")") return errors.New("Failed to read sanity check file (" + path + ")")
} }
decBytes, _ := Decrypt(encBytes, passphrase) decBytes, err := DecryptAndZeroizePassphrase(encBytes, passphrase)
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 passphrase)")
} }