mirror of
https://github.com/rwinkhart/rcw.git
synced 2026-08-27 20:36:30 -04:00
Significant hardening (always work on bytes; zeroize everything); address JetBrains warnings
This commit is contained in:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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,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
@@ -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...)
|
||||
}
|
||||
|
||||
+12
-6
@@ -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" {
|
||||
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)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user