Compare commits

...

10 Commits

Author SHA1 Message Date
d4f557631b prototype secure enclave interface 2025-07-15 09:37:02 +02:00
e53161188c Fix remaining memory security issues
- Fixed gpgDecryptDefault to return *memguard.LockedBuffer instead of []byte
- Updated GPGDecryptFunc signature and all implementations
- Confirmed getSecretValue already returns LockedBuffer (was fixed earlier)
- Improved passphrase string handling by removing intermediate variables
- Note: String conversion for passphrases is unavoidable due to age library API
- All GPG decrypted data is now immediately protected in memory
2025-07-15 09:08:51 +02:00
ff17b9b107 Update TODO.md - DecryptWithPassphrase already fixed
- DecryptWithPassphrase was automatically fixed when we updated DecryptWithIdentity
- It now returns LockedBuffer since it calls DecryptWithIdentity internally
2025-07-15 09:04:59 +02:00
63cc06b93c Fix DecryptWithIdentity to return LockedBuffer
- Changed DecryptWithIdentity to return *memguard.LockedBuffer instead of []byte
- Updated all callers throughout the codebase to handle LockedBuffer
- This ensures decrypted data is protected in memory immediately after decryption
- Fixed all usages in vault, secret, version, and unlocker implementations
- Removed duplicate buffer creation and unnecessary memory clearing
2025-07-15 09:04:34 +02:00
8ec3fc877d Fix GetValue methods to return LockedBuffer internally
- Changed Secret.GetValue and Version.GetValue to return *memguard.LockedBuffer
- Updated all internal callers to handle LockedBuffer properly
- For backward compatibility, vault.GetSecret still returns []byte but makes a copy
- This ensures secret values are protected in memory during decryption
- Updated tests to handle LockedBuffer returns
- Fixed CLI getSecretValue to use LockedBuffer throughout
2025-07-15 08:59:23 +02:00
819902f385 Fix gpgEncryptDefault to accept LockedBuffer for data parameter
- Changed GPGEncryptFunc signature to accept *memguard.LockedBuffer instead of []byte
- Updated gpgEncryptDefault implementation to use LockedBuffer
- Updated all callers including tests to pass LockedBuffer
- This ensures GPG encryption data is protected in memory
- Fixed linter issue with line length
2025-07-15 08:46:33 +02:00
292564c6e7 Fix storeInKeychain to accept LockedBuffer for data parameter
- Changed storeInKeychain to accept *memguard.LockedBuffer instead of []byte
- Updated caller in CreateKeychainUnlocker to create LockedBuffer before storing
- This ensures keychain data is protected in memory before being stored
- Added proper buffer cleanup with defer Destroy()
2025-07-15 08:44:09 +02:00
eef2332823 Fix EncryptWithPassphrase to accept LockedBuffer for data parameter
- Changed EncryptWithPassphrase to accept *memguard.LockedBuffer instead of []byte
- Updated all callers to pass LockedBuffer:
  - CreatePassphraseUnlocker in vault/unlockers.go
  - Keychain unlocker in keychainunlocker.go
  - Tests in passphrase_test.go
- Removed intermediate dataBuffer creation since data is now already protected
- This ensures sensitive data is protected in memory throughout encryption
2025-07-15 08:42:46 +02:00
e82d428b05 Remove deprecated Secret.Save function
- Removed unused deprecated Save(value []byte, force bool) function
- This function accepted unprotected secret data which was a security issue
- All code now uses vault.AddSecret directly with LockedBuffer
- Updated TODO.md to reflect completion of this security fix
2025-07-15 08:40:35 +02:00
9cbe055791 fmt 2025-07-15 08:33:16 +02:00
17 changed files with 600 additions and 210 deletions

23
TODO.md
View File

@ -4,6 +4,29 @@ This document outlines the bugs, issues, and improvements that need to be
addressed before the 1.0 release of the secret manager. Items are
prioritized from most critical (top) to least critical (bottom).
## CRITICAL MEMORY SECURITY ISSUES
### Functions accepting bare []byte for sensitive data
- [x] **1. Secret.Save accepts unprotected data**: `internal/secret/secret.go:67` - `Save(value []byte, force bool)` - ✓ REMOVED - deprecated function deleted
- [x] **2. EncryptWithPassphrase accepts unprotected data**: `internal/secret/crypto.go:73` - `EncryptWithPassphrase(data []byte, passphrase *memguard.LockedBuffer)` - ✓ FIXED - now accepts LockedBuffer for data
- [x] **3. storeInKeychain accepts unprotected data**: `internal/secret/keychainunlocker.go:469` - `storeInKeychain(itemName string, data []byte)` - ✓ FIXED - now accepts LockedBuffer for data
- [x] **4. gpgEncryptDefault accepts unprotected data**: `internal/secret/pgpunlocker.go:351` - `gpgEncryptDefault(data []byte, keyID string)` - ✓ FIXED - now accepts LockedBuffer for data
### Functions returning unprotected secrets
- [x] **5. GetValue returns unprotected secret**: `internal/secret/secret.go:93` - `GetValue(unlocker Unlocker) ([]byte, error)` - ✓ FIXED - now returns LockedBuffer internally
- [x] **6. DecryptWithIdentity returns unprotected data**: `internal/secret/crypto.go:57` - `DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, error)` - ✓ FIXED - now returns LockedBuffer
- [x] **7. DecryptWithPassphrase returns unprotected data**: `internal/secret/crypto.go:94` - `DecryptWithPassphrase(encryptedData []byte, passphrase *memguard.LockedBuffer) ([]byte, error)` - ✓ FIXED - now returns LockedBuffer
- [x] **8. gpgDecryptDefault returns unprotected data**: `internal/secret/pgpunlocker.go:368` - `gpgDecryptDefault(encryptedData []byte) ([]byte, error)` - ✓ FIXED - now returns LockedBuffer
- [x] **9. getSecretValue returns unprotected data**: `internal/cli/crypto.go:269` - `getSecretValue()` returns bare []byte - ✓ ALREADY FIXED - returns LockedBuffer
### Intermediate string variables for passphrases
- [x] **10. Passphrase extracted to string**: `internal/secret/crypto.go:79,100` - `passphraseStr := passphrase.String()` - ✓ UNAVOIDABLE - age library requires string parameter
- [ ] **11. Age secret key in plain string**: `internal/cli/crypto.go:86,91,113` - Age secret key stored in plain string variable before conversion back to secure buffer
### Unprotected buffer.Bytes() usage
- [ ] **12. GPG encrypt exposes private key**: `internal/secret/pgpunlocker.go:256` - `GPGEncryptFunc(agePrivateKeyBuffer.Bytes(), gpgKeyID)` - private key exposed to external function
- [ ] **13. Keychain encrypt exposes private key**: `internal/secret/keychainunlocker.go:371` - `EncryptWithPassphrase(agePrivKeyBuffer.Bytes(), passphraseBuffer)` - private key passed as bare bytes
## Code Cleanups
* we shouldn't be passing around a statedir, it should be read from the

View File

@ -96,21 +96,13 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
}
} else {
// Secret exists, get the age secret key from it
secretValue, err := cli.getSecretValue(vlt, secretObj)
secretBuffer, err := cli.getSecretValue(vlt, secretObj)
if err != nil {
return fmt.Errorf("failed to get secret value: %w", err)
}
defer secretBuffer.Destroy()
// Create secure buffer for the secret value
secureBuffer := memguard.NewBufferFromBytes(secretValue)
defer secureBuffer.Destroy()
// Clear the original secret value
for i := range secretValue {
secretValue[i] = 0
}
ageSecretKey = secureBuffer.String()
ageSecretKey = secretBuffer.String()
// Validate that it's a valid age secret key
if !isValidAgeSecretKey(ageSecretKey) {
@ -189,36 +181,28 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
}
// Get the age secret key from the secret
var secretValue []byte
var secretBuffer *memguard.LockedBuffer
if os.Getenv(secret.EnvMnemonic) != "" {
secretValue, err = secretObj.GetValue(nil)
secretBuffer, err = secretObj.GetValue(nil)
} else {
unlocker, unlockErr := vlt.GetCurrentUnlocker()
if unlockErr != nil {
return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
}
secretValue, err = secretObj.GetValue(unlocker)
secretBuffer, err = secretObj.GetValue(unlocker)
}
if err != nil {
return fmt.Errorf("failed to get secret value: %w", err)
}
// Create secure buffer for the secret value
secureBuffer := memguard.NewBufferFromBytes(secretValue)
defer secureBuffer.Destroy()
// Clear the original secret value
for i := range secretValue {
secretValue[i] = 0
}
defer secretBuffer.Destroy()
// Validate that it's a valid age secret key
if !isValidAgeSecretKey(secureBuffer.String()) {
if !isValidAgeSecretKey(secretBuffer.String()) {
return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
}
// Parse the age secret key to get the identity
identity, err := age.ParseX25519Identity(secureBuffer.String())
identity, err := age.ParseX25519Identity(secretBuffer.String())
if err != nil {
return fmt.Errorf("failed to parse age secret key: %w", err)
}
@ -266,7 +250,7 @@ func isValidAgeSecretKey(key string) bool {
}
// getSecretValue retrieves the value of a secret using the appropriate unlocker
func (cli *Instance) getSecretValue(vlt *vault.Vault, secretObj *secret.Secret) ([]byte, error) {
func (cli *Instance) getSecretValue(vlt *vault.Vault, secretObj *secret.Secret) (*memguard.LockedBuffer, error) {
if os.Getenv(secret.EnvMnemonic) != "" {
return secretObj.GetValue(nil)
}

313
internal/macse/enclave.go Normal file
View File

@ -0,0 +1,313 @@
//go:build darwin
// +build darwin
package macse
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Security -framework LocalAuthentication
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#import <LocalAuthentication/LocalAuthentication.h>
typedef struct {
const void* data;
int len;
int error;
} DataResult;
typedef struct {
SecKeyRef privateKey;
const void* salt;
int saltLen;
int error;
} KeyResult;
KeyResult createEnclaveKey(bool requireBiometric) {
KeyResult result = {NULL, NULL, 0, 0};
// Create authentication context
LAContext* authContext = [[LAContext alloc] init];
authContext.localizedReason = @"Create Secure Enclave key";
CFMutableDictionaryRef attributes = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(attributes, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom);
CFDictionarySetValue(attributes, kSecAttrKeySizeInBits, (__bridge CFNumberRef)@256);
CFDictionarySetValue(attributes, kSecAttrTokenID, kSecAttrTokenIDSecureEnclave);
CFMutableDictionaryRef privateKeyAttrs = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(privateKeyAttrs, kSecAttrIsPermanent, kCFBooleanFalse);
SecAccessControlCreateFlags flags = kSecAccessControlPrivateKeyUsage;
if (requireBiometric) {
flags |= kSecAccessControlBiometryCurrentSet;
}
SecAccessControlRef access = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
flags,
NULL);
if (!access) {
result.error = -1;
return result;
}
CFDictionarySetValue(privateKeyAttrs, kSecAttrAccessControl, access);
CFDictionarySetValue(privateKeyAttrs, kSecUseAuthenticationContext, (__bridge CFTypeRef)authContext);
CFDictionarySetValue(attributes, kSecPrivateKeyAttrs, privateKeyAttrs);
CFErrorRef error = NULL;
SecKeyRef privateKey = SecKeyCreateRandomKey(attributes, &error);
CFRelease(attributes);
CFRelease(privateKeyAttrs);
CFRelease(access);
if (error || !privateKey) {
if (error) {
result.error = (int)CFErrorGetCode(error);
CFRelease(error);
} else {
result.error = -3;
}
return result;
}
// Generate random salt
uint8_t* saltBytes = malloc(64);
if (SecRandomCopyBytes(kSecRandomDefault, 64, saltBytes) != 0) {
result.error = -2;
free(saltBytes);
if (privateKey) CFRelease(privateKey);
return result;
}
result.privateKey = privateKey;
result.salt = saltBytes;
result.saltLen = 64;
// Retain the key so it's not released
CFRetain(privateKey);
return result;
}
DataResult encryptData(SecKeyRef privateKey, const void* saltData, int saltLen, const void* plainData, int plainLen) {
DataResult result = {NULL, 0, 0};
// Get public key from private key
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
if (!publicKey) {
result.error = -1;
return result;
}
// Perform ECDH key agreement with self
CFErrorRef error = NULL;
CFMutableDictionaryRef params = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDataRef sharedSecret = SecKeyCopyKeyExchangeResult(privateKey, kSecKeyAlgorithmECDHKeyExchangeStandard, publicKey, params, &error);
CFRelease(params);
if (error) {
result.error = (int)CFErrorGetCode(error);
CFRelease(error);
CFRelease(publicKey);
return result;
}
// For simplicity, we'll use the shared secret directly as a symmetric key
// In production, you'd want to use HKDF as shown in the Swift code
// Create encryption key from shared secret
const uint8_t* secretBytes = CFDataGetBytePtr(sharedSecret);
size_t secretLen = CFDataGetLength(sharedSecret);
// Simple XOR encryption for demonstration (NOT SECURE - use proper encryption in production)
uint8_t* encrypted = malloc(plainLen);
for (int i = 0; i < plainLen; i++) {
encrypted[i] = ((uint8_t*)plainData)[i] ^ secretBytes[i % secretLen];
}
result.data = encrypted;
result.len = plainLen;
CFRelease(publicKey);
CFRelease(sharedSecret);
return result;
}
DataResult decryptData(SecKeyRef privateKey, const void* saltData, int saltLen, const void* encData, int encLen, void* context) {
DataResult result = {NULL, 0, 0};
// Set up authentication context
LAContext* authContext = [[LAContext alloc] init];
NSError* authError = nil;
// Check if biometric authentication is available
if ([authContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authError]) {
// Evaluate biometric authentication synchronously
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
__block BOOL authSuccess = NO;
[authContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:@"Decrypt data using Secure Enclave"
reply:^(BOOL success, NSError * _Nullable error) {
authSuccess = success;
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
if (!authSuccess) {
result.error = -3;
return result;
}
}
// Get public key from private key
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
if (!publicKey) {
result.error = -1;
return result;
}
// Create algorithm parameters with authentication context
CFMutableDictionaryRef params = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(params, kSecUseAuthenticationContext, (__bridge CFTypeRef)authContext);
// Perform ECDH key agreement with self
CFErrorRef error = NULL;
CFDataRef sharedSecret = SecKeyCopyKeyExchangeResult(privateKey, kSecKeyAlgorithmECDHKeyExchangeStandard, publicKey, params, &error);
CFRelease(params);
if (error) {
result.error = (int)CFErrorGetCode(error);
CFRelease(error);
CFRelease(publicKey);
return result;
}
// Decrypt using shared secret
const uint8_t* secretBytes = CFDataGetBytePtr(sharedSecret);
size_t secretLen = CFDataGetLength(sharedSecret);
// Simple XOR decryption for demonstration
uint8_t* decrypted = malloc(encLen);
for (int i = 0; i < encLen; i++) {
decrypted[i] = ((uint8_t*)encData)[i] ^ secretBytes[i % secretLen];
}
result.data = decrypted;
result.len = encLen;
CFRelease(publicKey);
CFRelease(sharedSecret);
return result;
}
void freeKeyResult(KeyResult* result) {
if (result->privateKey) {
CFRelease(result->privateKey);
}
if (result->salt) {
free((void*)result->salt);
}
}
void freeDataResult(DataResult* result) {
if (result->data) {
free((void*)result->data);
}
}
*/
import "C"
import (
"errors"
"unsafe"
)
type EnclaveKey struct {
privateKey C.SecKeyRef
salt []byte
}
func NewEnclaveKey(requireBiometric bool) (*EnclaveKey, error) {
result := C.createEnclaveKey(C.bool(requireBiometric))
defer C.freeKeyResult(&result)
if result.error != 0 {
return nil, errors.New("failed to create enclave key")
}
salt := make([]byte, result.saltLen)
copy(salt, (*[1 << 30]byte)(unsafe.Pointer(result.salt))[:result.saltLen:result.saltLen])
return &EnclaveKey{
privateKey: result.privateKey,
salt: salt,
}, nil
}
func (k *EnclaveKey) Encrypt(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("empty data")
}
if len(k.salt) == 0 {
return nil, errors.New("empty salt")
}
result := C.encryptData(
k.privateKey,
unsafe.Pointer(&k.salt[0]),
C.int(len(k.salt)),
unsafe.Pointer(&data[0]),
C.int(len(data)),
)
defer C.freeDataResult(&result)
if result.error != 0 {
return nil, errors.New("encryption failed")
}
encrypted := make([]byte, result.len)
copy(encrypted, (*[1 << 30]byte)(unsafe.Pointer(result.data))[:result.len:result.len])
return encrypted, nil
}
func (k *EnclaveKey) Decrypt(data []byte) ([]byte, error) {
if len(data) == 0 {
return nil, errors.New("empty data")
}
if len(k.salt) == 0 {
return nil, errors.New("empty salt")
}
result := C.decryptData(
k.privateKey,
unsafe.Pointer(&k.salt[0]),
C.int(len(k.salt)),
unsafe.Pointer(&data[0]),
C.int(len(data)),
nil,
)
defer C.freeDataResult(&result)
if result.error != 0 {
return nil, errors.New("decryption failed")
}
decrypted := make([]byte, result.len)
copy(decrypted, (*[1 << 30]byte)(unsafe.Pointer(result.data))[:result.len:result.len])
return decrypted, nil
}
func (k *EnclaveKey) Close() {
if k.privateKey != 0 {
C.CFRelease(C.CFTypeRef(k.privateKey))
k.privateKey = 0
}
}

View File

@ -0,0 +1,77 @@
//go:build darwin
// +build darwin
package macse
import (
"bytes"
"testing"
)
func TestEnclaveKeyEncryption(t *testing.T) {
// Create a new enclave key without requiring biometric
key, err := NewEnclaveKey(false)
if err != nil {
t.Fatalf("Failed to create enclave key: %v", err)
}
defer key.Close()
// Test data
plaintext := []byte("Hello, Secure Enclave!")
// Encrypt
encrypted, err := key.Encrypt(plaintext)
if err != nil {
t.Fatalf("Failed to encrypt: %v", err)
}
// Verify encrypted data is different from plaintext
if bytes.Equal(plaintext, encrypted) {
t.Error("Encrypted data should not equal plaintext")
}
// Decrypt
decrypted, err := key.Decrypt(encrypted)
if err != nil {
t.Fatalf("Failed to decrypt: %v", err)
}
// Verify decrypted data matches original
if !bytes.Equal(plaintext, decrypted) {
t.Errorf("Decrypted data does not match original: got %s, want %s", decrypted, plaintext)
}
}
func TestEnclaveKeyWithBiometric(t *testing.T) {
// This test requires user interaction
// Run with: CGO_ENABLED=1 go test -v -run TestEnclaveKeyWithBiometric
if testing.Short() {
t.Skip("Skipping biometric test in short mode")
}
key, err := NewEnclaveKey(true)
if err != nil {
t.Logf("Expected failure creating biometric key in test environment: %v", err)
return
}
defer key.Close()
plaintext := []byte("Biometric protected data")
encrypted, err := key.Encrypt(plaintext)
if err != nil {
t.Fatalf("Failed to encrypt with biometric key: %v", err)
}
// Decryption would require biometric authentication
decrypted, err := key.Decrypt(encrypted)
if err != nil {
// This is expected without proper biometric authentication
t.Logf("Expected decryption failure without biometric auth: %v", err)
return
}
if !bytes.Equal(plaintext, decrypted) {
t.Errorf("Decrypted data does not match original")
}
}

View File

@ -54,7 +54,7 @@ func EncryptToRecipient(data *memguard.LockedBuffer, recipient age.Recipient) ([
}
// DecryptWithIdentity decrypts data with an identity using age
func DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
func DecryptWithIdentity(data []byte, identity age.Identity) (*memguard.LockedBuffer, error) {
r, err := age.Decrypt(bytes.NewReader(data), identity)
if err != nil {
return nil, fmt.Errorf("failed to create decryptor: %w", err)
@ -65,40 +65,40 @@ func DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, error) {
return nil, fmt.Errorf("failed to read decrypted data: %w", err)
}
return result, nil
// Create a secure buffer for the decrypted data
resultBuffer := memguard.NewBufferFromBytes(result)
return resultBuffer, nil
}
// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption
// The passphrase parameter should be a LockedBuffer for secure memory handling
func EncryptWithPassphrase(data []byte, passphrase *memguard.LockedBuffer) ([]byte, error) {
// Both data and passphrase parameters should be LockedBuffers for secure memory handling
func EncryptWithPassphrase(data *memguard.LockedBuffer, passphrase *memguard.LockedBuffer) ([]byte, error) {
if data == nil {
return nil, fmt.Errorf("data buffer is nil")
}
if passphrase == nil {
return nil, fmt.Errorf("passphrase buffer is nil")
}
// Get the passphrase string temporarily
passphraseStr := passphrase.String()
recipient, err := age.NewScryptRecipient(passphraseStr)
// Create recipient directly from passphrase - unavoidable string conversion due to age API
recipient, err := age.NewScryptRecipient(passphrase.String())
if err != nil {
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
}
// Create a secure buffer for the data
dataBuffer := memguard.NewBufferFromBytes(data)
defer dataBuffer.Destroy()
return EncryptToRecipient(dataBuffer, recipient)
return EncryptToRecipient(data, recipient)
}
// DecryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
// The passphrase parameter should be a LockedBuffer for secure memory handling
func DecryptWithPassphrase(encryptedData []byte, passphrase *memguard.LockedBuffer) ([]byte, error) {
func DecryptWithPassphrase(encryptedData []byte, passphrase *memguard.LockedBuffer) (*memguard.LockedBuffer, error) {
if passphrase == nil {
return nil, fmt.Errorf("passphrase buffer is nil")
}
// Get the passphrase string temporarily
passphraseStr := passphrase.String()
identity, err := age.NewScryptIdentity(passphraseStr)
// Create identity directly from passphrase - unavoidable string conversion due to age API
identity, err := age.NewScryptIdentity(passphrase.String())
if err != nil {
return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
}

View File

@ -107,30 +107,22 @@ func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
passphraseBuffer := memguard.NewBufferFromBytes([]byte(keychainData.AgePrivKeyPassphrase))
defer passphraseBuffer.Destroy()
agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, passphraseBuffer)
agePrivKeyBuffer, err := DecryptWithPassphrase(encryptedAgePrivKeyData, passphraseBuffer)
if err != nil {
Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "unlocker_id", k.GetID())
return nil, fmt.Errorf("failed to decrypt age private key with keychain passphrase: %w", err)
}
defer agePrivKeyBuffer.Destroy()
DebugWith("Successfully decrypted age private key with keychain passphrase",
slog.String("unlocker_id", k.GetID()),
slog.Int("decrypted_length", len(agePrivKeyData)),
slog.Int("decrypted_length", agePrivKeyBuffer.Size()),
)
// Step 6: Parse the decrypted age private key
Debug("Parsing decrypted age private key", "unlocker_id", k.GetID())
// Create a secure buffer for the private key data
agePrivKeyBuffer := memguard.NewBufferFromBytes(agePrivKeyData)
defer agePrivKeyBuffer.Destroy()
// Clear the original private key data
for i := range agePrivKeyData {
agePrivKeyData[i] = 0
}
ageIdentity, err := age.ParseX25519Identity(agePrivKeyBuffer.String())
if err != nil {
Debug("Failed to parse age private key", "error", err, "unlocker_id", k.GetID())
@ -301,13 +293,13 @@ func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) (*memguard.LockedB
}
// Decrypt long-term private key using current unlocker
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
ltPrivKeyBuffer, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
// Return the decrypted key in a secure buffer
return memguard.NewBufferFromBytes(ltPrivKeyData), nil
// Return the decrypted key buffer
return ltPrivKeyBuffer, nil
}
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
@ -368,7 +360,7 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
passphraseBuffer := memguard.NewBufferFromBytes([]byte(agePrivKeyPassphrase))
defer passphraseBuffer.Destroy()
encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivKeyBuffer.Bytes(), passphraseBuffer)
encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivKeyBuffer, passphraseBuffer)
if err != nil {
return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
}
@ -409,8 +401,12 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
return nil, fmt.Errorf("failed to marshal keychain data: %w", err)
}
// Create a secure buffer for keychain data
keychainDataBuffer := memguard.NewBufferFromBytes(keychainDataBytes)
defer keychainDataBuffer.Destroy()
// Step 8: Store data in keychain
if err := storeInKeychain(keychainItemName, keychainDataBytes); err != nil {
if err := storeInKeychain(keychainItemName, keychainDataBuffer); err != nil {
return nil, fmt.Errorf("failed to store data in keychain: %w", err)
}
@ -466,14 +462,17 @@ func validateKeychainItemName(itemName string) error {
}
// storeInKeychain stores data in the macOS keychain using the security command
func storeInKeychain(itemName string, data []byte) error {
func storeInKeychain(itemName string, data *memguard.LockedBuffer) error {
if data == nil {
return fmt.Errorf("data buffer is nil")
}
if err := validateKeychainItemName(itemName); err != nil {
return fmt.Errorf("invalid keychain item name: %w", err)
}
cmd := exec.Command("/usr/bin/security", "add-generic-password", //nolint:gosec
"-a", itemName,
"-s", itemName,
"-w", string(data),
"-w", data.String(),
"-U") // Update if exists
if err := cmd.Run(); err != nil {

View File

@ -76,10 +76,11 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) {
// Test encrypting private key with passphrase
t.Run("EncryptPrivateKey", func(t *testing.T) {
privKeyData := []byte(agePrivateKey)
privKeyBuffer := memguard.NewBufferFromBytes([]byte(agePrivateKey))
defer privKeyBuffer.Destroy()
passphraseBuffer := memguard.NewBufferFromBytes([]byte(testPassphrase))
defer passphraseBuffer.Destroy()
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphraseBuffer)
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyBuffer, passphraseBuffer)
if err != nil {
t.Fatalf("Failed to encrypt private key: %v", err)
}

View File

@ -84,30 +84,22 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
// Decrypt the unlocker private key with passphrase
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseBuffer)
privKeyBuffer, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseBuffer)
if err != nil {
Debug("Failed to decrypt unlocker private key", "error", err, "unlocker_id", p.GetID())
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
}
defer privKeyBuffer.Destroy()
DebugWith("Successfully decrypted unlocker private key",
slog.String("unlocker_id", p.GetID()),
slog.Int("decrypted_length", len(privKeyData)),
slog.Int("decrypted_length", privKeyBuffer.Size()),
)
// Parse the decrypted private key
Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID())
// Create a secure buffer for the private key data
privKeyBuffer := memguard.NewBufferFromBytes(privKeyData)
defer privKeyBuffer.Destroy()
// Clear the original private key data
for i := range privKeyData {
privKeyData[i] = 0
}
identity, err := age.ParseX25519Identity(privKeyBuffer.String())
if err != nil {
Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())

View File

@ -45,7 +45,10 @@ pinentry-mode loopback
origDecryptFunc := secret.GPGDecryptFunc
// Set custom GPG functions for this test
secret.GPGEncryptFunc = func(data []byte, keyID string) ([]byte, error) {
secret.GPGEncryptFunc = func(data *memguard.LockedBuffer, keyID string) ([]byte, error) {
if data == nil {
return nil, fmt.Errorf("data buffer is nil")
}
cmd := exec.Command("gpg",
"--homedir", gnupgHomeDir,
"--batch",
@ -60,7 +63,7 @@ pinentry-mode loopback
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = bytes.NewReader(data)
cmd.Stdin = bytes.NewReader(data.Bytes())
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("GPG encryption failed: %w\nStderr: %s", err, stderr.String())
@ -69,7 +72,7 @@ pinentry-mode loopback
return stdout.Bytes(), nil
}
secret.GPGDecryptFunc = func(encryptedData []byte) ([]byte, error) {
secret.GPGDecryptFunc = func(encryptedData []byte) (*memguard.LockedBuffer, error) {
cmd := exec.Command("gpg",
"--homedir", gnupgHomeDir,
"--batch",
@ -88,7 +91,8 @@ pinentry-mode loopback
return nil, fmt.Errorf("GPG decryption failed: %w\nStderr: %s", err, stderr.String())
}
return stdout.Bytes(), nil
// Create a secure buffer for the decrypted data
return memguard.NewBufferFromBytes(stdout.Bytes()), nil
}
// Restore original functions after test
@ -444,8 +448,9 @@ Passphrase: ` + testPassphrase + `
}
// GPG encrypt the private key using our custom encrypt function
privKeyData := []byte(ageIdentity.String())
encryptedOutput, err := secret.GPGEncryptFunc(privKeyData, keyID)
privKeyBuffer := memguard.NewBufferFromBytes([]byte(ageIdentity.String()))
defer privKeyBuffer.Destroy()
encryptedOutput, err := secret.GPGEncryptFunc(privKeyBuffer, keyID)
if err != nil {
t.Fatalf("Failed to encrypt with GPG: %v", err)
}

View File

@ -20,11 +20,13 @@ import (
var (
// GPGEncryptFunc is the function used for GPG encryption
// Can be overridden in tests to provide a non-interactive implementation
GPGEncryptFunc = gpgEncryptDefault //nolint:gochecknoglobals // Required for test mocking
//nolint:gochecknoglobals // Required for test mocking
GPGEncryptFunc func(data *memguard.LockedBuffer, keyID string) ([]byte, error) = gpgEncryptDefault
// GPGDecryptFunc is the function used for GPG decryption
// Can be overridden in tests to provide a non-interactive implementation
GPGDecryptFunc = gpgDecryptDefault //nolint:gochecknoglobals // Required for test mocking
//nolint:gochecknoglobals // Required for test mocking
GPGDecryptFunc func(encryptedData []byte) (*memguard.LockedBuffer, error) = gpgDecryptDefault
// gpgKeyIDRegex validates GPG key IDs
// Allows either:
@ -79,21 +81,22 @@ func (p *PGPUnlocker) GetIdentity() (*age.X25519Identity, error) {
// Step 2: Decrypt the age private key using GPG
Debug("Decrypting age private key with GPG", "unlocker_id", p.GetID())
agePrivKeyData, err := GPGDecryptFunc(encryptedAgePrivKeyData)
agePrivKeyBuffer, err := GPGDecryptFunc(encryptedAgePrivKeyData)
if err != nil {
Debug("Failed to decrypt age private key with GPG", "error", err, "unlocker_id", p.GetID())
return nil, fmt.Errorf("failed to decrypt age private key with GPG: %w", err)
}
defer agePrivKeyBuffer.Destroy()
DebugWith("Successfully decrypted age private key with GPG",
slog.String("unlocker_id", p.GetID()),
slog.Int("decrypted_length", len(agePrivKeyData)),
slog.Int("decrypted_length", agePrivKeyBuffer.Size()),
)
// Step 3: Parse the decrypted age private key
Debug("Parsing decrypted age private key", "unlocker_id", p.GetID())
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
ageIdentity, err := age.ParseX25519Identity(agePrivKeyBuffer.String())
if err != nil {
Debug("Failed to parse age private key", "error", err, "unlocker_id", p.GetID())
@ -253,7 +256,7 @@ func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnloc
agePrivateKeyBuffer := memguard.NewBufferFromBytes([]byte(ageIdentity.String()))
defer agePrivateKeyBuffer.Destroy()
encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBuffer.Bytes(), gpgKeyID)
encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBuffer, gpgKeyID)
if err != nil {
return nil, fmt.Errorf("failed to encrypt age private key with GPG: %w", err)
}
@ -348,13 +351,16 @@ func checkGPGAvailable() error {
}
// gpgEncryptDefault is the default implementation of GPG encryption
func gpgEncryptDefault(data []byte, keyID string) ([]byte, error) {
func gpgEncryptDefault(data *memguard.LockedBuffer, keyID string) ([]byte, error) {
if data == nil {
return nil, fmt.Errorf("data buffer is nil")
}
if err := validateGPGKeyID(keyID); err != nil {
return nil, fmt.Errorf("invalid GPG key ID: %w", err)
}
cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID)
cmd.Stdin = strings.NewReader(string(data))
cmd.Stdin = strings.NewReader(data.String())
output, err := cmd.Output()
if err != nil {
@ -365,7 +371,7 @@ func gpgEncryptDefault(data []byte, keyID string) ([]byte, error) {
}
// gpgDecryptDefault is the default implementation of GPG decryption
func gpgDecryptDefault(encryptedData []byte) ([]byte, error) {
func gpgDecryptDefault(encryptedData []byte) (*memguard.LockedBuffer, error) {
cmd := exec.Command("gpg", "--quiet", "--decrypt")
cmd.Stdin = strings.NewReader(string(encryptedData))
@ -374,5 +380,8 @@ func gpgDecryptDefault(encryptedData []byte) ([]byte, error) {
return nil, fmt.Errorf("GPG decryption failed: %w", err)
}
return output, nil
// Create a secure buffer for the decrypted data
outputBuffer := memguard.NewBufferFromBytes(output)
return outputBuffer, nil
}

View File

@ -62,35 +62,8 @@ func NewSecret(vault VaultInterface, name string) *Secret {
}
}
// Save is deprecated - use vault.AddSecret directly which creates versions
// Kept for backward compatibility
func (s *Secret) Save(value []byte, force bool) error {
DebugWith("Saving secret (deprecated method)",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
slog.Int("value_length", len(value)),
slog.Bool("force", force),
)
// Create a secure buffer for the value - note that the caller
// should ideally pass a LockedBuffer directly to vault.AddSecret
valueBuffer := memguard.NewBufferFromBytes(value)
defer valueBuffer.Destroy()
err := s.vault.AddSecret(s.Name, valueBuffer, force)
if err != nil {
Debug("Failed to save secret", "error", err, "secret_name", s.Name)
return err
}
Debug("Successfully saved secret", "secret_name", s.Name)
return nil
}
// GetValue retrieves and decrypts the current version's value using the provided unlocker
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
func (s *Secret) GetValue(unlocker Unlocker) (*memguard.LockedBuffer, error) {
DebugWith("Getting secret value",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
@ -206,16 +179,17 @@ func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
// Decrypt the encrypted long-term private key using the unlocker
Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
ltPrivKeyBuffer, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
if err != nil {
Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
defer ltPrivKeyBuffer.Destroy()
// Parse the long-term private key
Debug("Parsing long-term private key", "secret_name", s.Name)
ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
ltIdentity, err := age.ParseX25519Identity(ltPrivKeyBuffer.String())
if err != nil {
Debug("Failed to parse long-term private key", "error", err, "secret_name", s.Name)

View File

@ -277,15 +277,16 @@ func (sv *Version) LoadMetadata(ltIdentity *age.X25519Identity) error {
}
// Step 2: Decrypt version private key using long-term key
versionPrivKeyData, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
versionPrivKeyBuffer, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
if err != nil {
Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
return fmt.Errorf("failed to decrypt version private key: %w", err)
}
defer versionPrivKeyBuffer.Destroy()
// Step 3: Parse version private key
versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
versionIdentity, err := age.ParseX25519Identity(versionPrivKeyBuffer.String())
if err != nil {
Debug("Failed to parse version private key", "error", err, "version", sv.Version)
@ -302,16 +303,17 @@ func (sv *Version) LoadMetadata(ltIdentity *age.X25519Identity) error {
}
// Step 5: Decrypt metadata using version key
metadataBytes, err := DecryptWithIdentity(encryptedMetadata, versionIdentity)
metadataBuffer, err := DecryptWithIdentity(encryptedMetadata, versionIdentity)
if err != nil {
Debug("Failed to decrypt version metadata", "error", err, "version", sv.Version)
return fmt.Errorf("failed to decrypt version metadata: %w", err)
}
defer metadataBuffer.Destroy()
// Step 6: Unmarshal metadata
var metadata VersionMetadata
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
if err := json.Unmarshal(metadataBuffer.Bytes(), &metadata); err != nil {
Debug("Failed to unmarshal version metadata", "error", err, "version", sv.Version)
return fmt.Errorf("failed to unmarshal version metadata: %w", err)
@ -324,7 +326,7 @@ func (sv *Version) LoadMetadata(ltIdentity *age.X25519Identity) error {
}
// GetValue retrieves and decrypts the version value
func (sv *Version) GetValue(ltIdentity *age.X25519Identity) ([]byte, error) {
func (sv *Version) GetValue(ltIdentity *age.X25519Identity) (*memguard.LockedBuffer, error) {
DebugWith("Getting version value",
slog.String("secret_name", sv.SecretName),
slog.String("version", sv.Version),
@ -352,16 +354,17 @@ func (sv *Version) GetValue(ltIdentity *age.X25519Identity) ([]byte, error) {
// Step 2: Decrypt version private key using long-term key
Debug("Decrypting version private key with long-term identity", "version", sv.Version)
versionPrivKeyData, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
versionPrivKeyBuffer, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
if err != nil {
Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
return nil, fmt.Errorf("failed to decrypt version private key: %w", err)
}
Debug("Successfully decrypted version private key", "version", sv.Version, "size", len(versionPrivKeyData))
defer versionPrivKeyBuffer.Destroy()
Debug("Successfully decrypted version private key", "version", sv.Version, "size", versionPrivKeyBuffer.Size())
// Step 3: Parse version private key
versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
versionIdentity, err := age.ParseX25519Identity(versionPrivKeyBuffer.String())
if err != nil {
Debug("Failed to parse version private key", "error", err, "version", sv.Version)
@ -381,7 +384,7 @@ func (sv *Version) GetValue(ltIdentity *age.X25519Identity) ([]byte, error) {
// Step 5: Decrypt value using version key
Debug("Decrypting value with version identity", "version", sv.Version)
value, err := DecryptWithIdentity(encryptedValue, versionIdentity)
valueBuffer, err := DecryptWithIdentity(encryptedValue, versionIdentity)
if err != nil {
Debug("Failed to decrypt version value", "error", err, "version", sv.Version)
@ -390,10 +393,10 @@ func (sv *Version) GetValue(ltIdentity *age.X25519Identity) ([]byte, error) {
Debug("Successfully retrieved version value",
"version", sv.Version,
"value_length", len(value),
"is_empty", len(value) == 0)
"value_length", valueBuffer.Size(),
"is_empty", valueBuffer.Size() == 0)
return value, nil
return valueBuffer, nil
}
// ListVersions lists all versions of a secret

View File

@ -255,10 +255,11 @@ func TestSecretVersionGetValue(t *testing.T) {
require.NoError(t, err)
// Retrieve the value
retrievedValue, err := sv.GetValue(ltIdentity)
retrievedBuffer, err := sv.GetValue(ltIdentity)
require.NoError(t, err)
defer retrievedBuffer.Destroy()
assert.Equal(t, expectedValue, retrievedValue)
assert.Equal(t, expectedValue, retrievedBuffer.Bytes())
}
func TestListVersions(t *testing.T) {

View File

@ -259,13 +259,14 @@ func updateVersionMetadata(fs afero.Fs, version *secret.Version, ltIdentity *age
}
// Decrypt version private key using long-term key
versionPrivKeyData, err := secret.DecryptWithIdentity(encryptedPrivKey, ltIdentity)
versionPrivKeyBuffer, err := secret.DecryptWithIdentity(encryptedPrivKey, ltIdentity)
if err != nil {
return fmt.Errorf("failed to decrypt version private key: %w", err)
}
defer versionPrivKeyBuffer.Destroy()
// Parse version private key
versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
versionIdentity, err := age.ParseX25519Identity(versionPrivKeyBuffer.String())
if err != nil {
return fmt.Errorf("failed to parse version private key: %w", err)
}
@ -393,21 +394,26 @@ func (v *Vault) GetSecretVersion(name string, version string) ([]byte, error) {
return nil, fmt.Errorf("failed to decrypt version: %w", err)
}
// Create a copy to return since the buffer will be destroyed
result := make([]byte, decryptedValue.Size())
copy(result, decryptedValue.Bytes())
decryptedValue.Destroy()
secret.DebugWith("Successfully decrypted secret version",
slog.String("secret_name", name),
slog.String("version", version),
slog.String("vault_name", v.Name),
slog.Int("decrypted_length", len(decryptedValue)),
slog.Int("decrypted_length", len(result)),
)
// Debug: Log metadata about the decrypted value without exposing the actual secret
secret.Debug("Vault secret decryption debug info",
"secret_name", name,
"version", version,
"decrypted_value_length", len(decryptedValue),
"is_empty", len(decryptedValue) == 0)
"decrypted_value_length", len(result),
"is_empty", len(result) == 0)
return decryptedValue, nil
return result, nil
}
// UnlockVault unlocks the vault and returns the long-term private key

View File

@ -346,7 +346,9 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
// Encrypt private key with passphrase
privKeyStr := unlockerIdentity.String()
encryptedPrivKey, err := secret.EncryptWithPassphrase([]byte(privKeyStr), passphrase)
privKeyBuffer := memguard.NewBufferFromBytes([]byte(privKeyStr))
defer privKeyBuffer.Destroy()
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyBuffer, passphrase)
if err != nil {
return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
}

View File

@ -157,22 +157,23 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
// Decrypt long-term private key using unlocker
secret.Debug("Decrypting long-term private key with unlocker", "unlocker_type", unlocker.GetType())
ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockerIdentity)
ltPrivKeyBuffer, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockerIdentity)
if err != nil {
secret.Debug("Failed to decrypt long-term private key", "error", err, "unlocker_type", unlocker.GetType())
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
defer ltPrivKeyBuffer.Destroy()
secret.DebugWith("Successfully decrypted long-term private key",
slog.String("vault_name", v.Name),
slog.String("unlocker_type", unlocker.GetType()),
slog.Int("decrypted_length", len(ltPrivKeyData)),
slog.Int("decrypted_length", ltPrivKeyBuffer.Size()),
)
// Parse long-term private key
secret.Debug("Parsing long-term private key", "vault_name", v.Name)
ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
ltIdentity, err := age.ParseX25519Identity(ltPrivKeyBuffer.String())
if err != nil {
secret.Debug("Failed to parse long-term private key", "error", err, "vault_name", v.Name)