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
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).
+5 -4
View File
@@ -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)
}
+1 -1
View File
@@ -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
+8 -4
View File
@@ -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
}
+5 -5
View File
@@ -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 <passwd>
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
+4
View File
@@ -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)
+3
View File
@@ -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)
+21 -6
View File
@@ -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...)
}
+13 -7
View File
@@ -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)")
}