From 0e7a796bb194b6f620dd30c2fbd8c955c4e1666b Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Mon, 19 May 2025 19:59:57 -0400 Subject: [PATCH] Derive HKDF keys (per algo) from primary key: roughly doubles time/security efficiency --- wrappers/aes.go | 40 ++++++++++++++++------------------------ wrappers/chacha.go | 40 +++++++++++++++------------------------- wrappers/highLevel.go | 23 ++++++++++++++++++----- wrappers/keyDeriv.go | 37 ++++++++++++++++++++++++++++++------- 4 files changed, 79 insertions(+), 61 deletions(-) diff --git a/wrappers/aes.go b/wrappers/aes.go index 45f5922..96e8da6 100644 --- a/wrappers/aes.go +++ b/wrappers/aes.go @@ -3,40 +3,32 @@ package wrappers import ( "crypto/aes" "crypto/cipher" - "crypto/rand" "errors" - "io" ) const ( nonceSizeAES = 12 // GCM standard nonce size is 12 bytes + hkdfInfoAES = "AES256-GCM" + hkdfInfoCha = "ChaCha20-Poly1305" ) // EncryptAES encrypts data using AES-256-GCM. -func encryptAES(decBytes []byte, passphrase []byte) []byte { - // generate a random salt - salt := make([]byte, saltSize) - io.ReadFull(rand.Reader, salt) - - // derive key from passphrase using the salt - key := deriveKey(passphrase, salt) - +func encryptAES(decBytes, key2, salt2 []byte) []byte { // create AES-256 cipher - block, _ := aes.NewCipher(key) + block, _ := aes.NewCipher(key2) // create GCM mode aesGCM, _ := cipher.NewGCM(block) // generate a random nonce - nonce := make([]byte, nonceSizeAES) - io.ReadFull(rand.Reader, nonce) + nonce := getRandomBytes(nonceSizeAES) // encrypt the data ciphertext := aesGCM.Seal(nil, nonce, decBytes, nil) - // format: salt + nonce + ciphertext - result := make([]byte, 0, saltSize+nonceSizeAES+len(ciphertext)) - result = append(result, salt...) + // format: salt2 + nonce + ciphertext + result := make([]byte, 0, saltSize2+nonceSizeAES+len(ciphertext)) + result = append(result, salt2...) result = append(result, nonce...) result = append(result, ciphertext...) @@ -44,21 +36,21 @@ func encryptAES(decBytes []byte, passphrase []byte) []byte { } // DecryptAES decrypts data using AES256-GCM. -func decryptAES(encBytes []byte, passphrase []byte) ([]byte, error) { - if len(encBytes) < saltSize+nonceSizeAES { +func decryptAES(encBytes, key1 []byte) ([]byte, error) { + if len(encBytes) < saltSize2+nonceSizeAES { return nil, errors.New("AES256-GCM: Encrypted data is too short") } // extract salt, nonce, and ciphertext - salt := encBytes[:saltSize] - nonce := encBytes[saltSize : saltSize+nonceSizeAES] - ciphertext := encBytes[saltSize+nonceSizeAES:] + salt2 := encBytes[:saltSize2] + nonce := encBytes[saltSize2 : saltSize2+nonceSizeAES] + ciphertext := encBytes[saltSize2+nonceSizeAES:] - // derive key from passphrase using the salt - key := deriveKey(passphrase, salt) + // derive secondary key from primary key using the salt + key2 := deriveSecondaryKey(key1, salt2, []byte(hkdfInfoAES)) // create AES-256 cipher - block, _ := aes.NewCipher(key) + block, _ := aes.NewCipher(key2) // create GCM mode aesGCM, _ := cipher.NewGCM(block) diff --git a/wrappers/chacha.go b/wrappers/chacha.go index 270a277..bacd6ff 100644 --- a/wrappers/chacha.go +++ b/wrappers/chacha.go @@ -1,9 +1,7 @@ package wrappers import ( - "crypto/rand" "errors" - "io" "golang.org/x/crypto/chacha20poly1305" ) @@ -13,27 +11,19 @@ const ( ) // EncryptCha encrypts data using ChaCha20-Poly1305. -func encryptCha(data []byte, passphrase []byte) []byte { - // generate a random salt - salt := make([]byte, saltSize) - io.ReadFull(rand.Reader, salt) - - // derive key from passphrase using the salt - key := deriveKey(passphrase, salt) - +func encryptCha(decBytes, key2, salt2 []byte) []byte { // create ChaCha20-Poly1305 cipher - stream, _ := chacha20poly1305.NewX(key) + stream, _ := chacha20poly1305.NewX(key2) // generate a random nonce - nonce := make([]byte, nonceSizeCha) - io.ReadFull(rand.Reader, nonce) + nonce := getRandomBytes(nonceSizeCha) // encrypt the data - ciphertext := stream.Seal(nil, nonce, data, nil) + ciphertext := stream.Seal(nil, nonce, decBytes, nil) - // format: salt + nonce + ciphertext - result := make([]byte, 0, saltSize+nonceSizeCha+len(ciphertext)) - result = append(result, salt...) + // format: salt2 + nonce + ciphertext + result := make([]byte, 0, saltSize2+nonceSizeCha+len(ciphertext)) + result = append(result, salt2...) result = append(result, nonce...) result = append(result, ciphertext...) @@ -41,21 +31,21 @@ func encryptCha(data []byte, passphrase []byte) []byte { } // DecryptCha decrypts data using ChaCha20-Poly1305. -func decryptCha(encryptedData []byte, passphrase []byte) ([]byte, error) { - if len(encryptedData) < saltSize+nonceSizeCha { +func decryptCha(encBytes, key1 []byte) ([]byte, error) { + if len(encBytes) < saltSize2+nonceSizeCha { return nil, errors.New("ChaCha20-Poly1305: Encrypted data is too short") } // extract salt, nonce, and ciphertext - salt := encryptedData[:saltSize] - nonce := encryptedData[saltSize : saltSize+nonceSizeCha] - ciphertext := encryptedData[saltSize+nonceSizeCha:] + salt2 := encBytes[:saltSize2] + nonce := encBytes[saltSize2 : saltSize2+nonceSizeCha] + ciphertext := encBytes[saltSize2+nonceSizeCha:] - // derive key from passphrase using the salt - key := deriveKey(passphrase, salt) + // derive secondary key from primary key using the salt + key2 := deriveSecondaryKey(key1, salt2, []byte(hkdfInfoCha)) // create ChaCha20-Poly1305 cipher - stream, _ := chacha20poly1305.NewX(key) + stream, _ := chacha20poly1305.NewX(key2) // decrypt the data plaintext, err := stream.Open(nil, nonce, ciphertext, nil) diff --git a/wrappers/highLevel.go b/wrappers/highLevel.go index be5d0a7..b22a763 100644 --- a/wrappers/highLevel.go +++ b/wrappers/highLevel.go @@ -3,11 +3,14 @@ package wrappers // Decrypt decrypts the provided byte slice using the provided passphrase. func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) { var err error = nil - encBytes, err = decryptCha(encBytes, passphrase) + salt1 := encBytes[:saltSize1] + encBytes = encBytes[saltSize1:] + key1 := derivePrimaryKey(passphrase, salt1) + encBytes, err = decryptCha(encBytes, key1) if err != nil { return nil, err } - encBytes, err = decryptAES(encBytes, passphrase) + encBytes, err = decryptAES(encBytes, key1) if err != nil { return nil, err } @@ -16,7 +19,17 @@ func Decrypt(encBytes []byte, passphrase []byte) ([]byte, error) { // Encrypt encrypts the provided byte slice using the provided passphrase. func Encrypt(decBytes []byte, passphrase []byte) []byte { - decBytes = encryptAES(decBytes, passphrase) - decBytes = encryptCha(decBytes, passphrase) - return decBytes + salt1 := getRandomBytes(saltSize1) + salt2AES := getRandomBytes(saltSize2) + salt2Cha := getRandomBytes(saltSize2) + key1 := derivePrimaryKey(passphrase, salt1) + key2AES := deriveSecondaryKey(key1, salt2AES, []byte(hkdfInfoAES)) + key2Cha := deriveSecondaryKey(key1, salt2Cha, []byte(hkdfInfoCha)) + decBytes = encryptAES(decBytes, key2AES, salt2AES) + decBytes = encryptCha(decBytes, key2Cha, salt2Cha) + // format: salt1 + decBytes per algorithm (salt2* + nonce + ciphertext) + encBytes := make([]byte, 0, saltSize1+len(decBytes)) + encBytes = append(encBytes, salt1...) + encBytes = append(encBytes, decBytes...) + return encBytes } diff --git a/wrappers/keyDeriv.go b/wrappers/keyDeriv.go index 46a4831..a90e43a 100644 --- a/wrappers/keyDeriv.go +++ b/wrappers/keyDeriv.go @@ -1,21 +1,44 @@ package wrappers import ( + "crypto/rand" + "crypto/sha256" + "io" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/hkdf" ) const ( // parameters for Argon2 - argonTime = 8 // set to pass 1-second test in dev environment - argonMemory = 384 * 1024 // 384 MB (target running comfortably on a Pi Zero/512 MB RAM) - argonThreads = 32 // must use a static thread count for support across multiple devices + argonTime = 5 // pass 1-second test on dev environment + argonMemory = 1024 * 1024 // 1 GB + argonThreads = 32 // 32 threads offers the best balance between utilization on high-end devices and performance on low-end devices // general constants - keyLen = 32 // 256 bits, key length for both algorithms - saltSize = 16 // 128 bits, recommended salt size for both algorithms + keyLen = 32 // 256 bits, key length for both algorithms + saltSize1 = 16 // 128 bits, recommended salt size for AES256/ChaCha20/Argon2 + saltSize2 = 32 // 256 bits, recommended salt size for HKDF ) -// deriveKey derives an encryption key from a passphrase using Argon2. -func deriveKey(passphrase []byte, salt []byte) []byte { +// derivePrimaryKey derives an encryption key from a passphrase using Argon2. +// 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 { return argon2.IDKey(passphrase, salt, argonTime, argonMemory, argonThreads, keyLen) } + +// 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. +func deriveSecondaryKey(primaryKey, salt, info []byte) []byte { + h := hkdf.New(sha256.New, primaryKey, salt, info) + derivedKey := make([]byte, keyLen) + io.ReadFull(h, derivedKey) + return derivedKey +} + +// getRandomBytes returns a random salt/nonce of the specified size. +func getRandomBytes(size uint8) []byte { + salt := make([]byte, size) + io.ReadFull(rand.Reader, salt) + return salt +}