Compare commits

..

No commits in common. "d4f557631b526c5653b63382c33f170f94f86e0a" and "7596049828bd40b017ca72922be09d009517baed" have entirely different histories.

17 changed files with 210 additions and 600 deletions

23
TODO.md
View File

@ -4,29 +4,6 @@ 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,13 +96,21 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
}
} else {
// Secret exists, get the age secret key from it
secretBuffer, err := cli.getSecretValue(vlt, secretObj)
secretValue, err := cli.getSecretValue(vlt, secretObj)
if err != nil {
return fmt.Errorf("failed to get secret value: %w", err)
}
defer secretBuffer.Destroy()
ageSecretKey = secretBuffer.String()
// 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()
// Validate that it's a valid age secret key
if !isValidAgeSecretKey(ageSecretKey) {
@ -181,28 +189,36 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
}
// Get the age secret key from the secret
var secretBuffer *memguard.LockedBuffer
var secretValue []byte
if os.Getenv(secret.EnvMnemonic) != "" {
secretBuffer, err = secretObj.GetValue(nil)
secretValue, err = secretObj.GetValue(nil)
} else {
unlocker, unlockErr := vlt.GetCurrentUnlocker()
if unlockErr != nil {
return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
}
secretBuffer, err = secretObj.GetValue(unlocker)
secretValue, err = secretObj.GetValue(unlocker)
}
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
}
// Validate that it's a valid age secret key
if !isValidAgeSecretKey(secretBuffer.String()) {
if !isValidAgeSecretKey(secureBuffer.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(secretBuffer.String())
identity, err := age.ParseX25519Identity(secureBuffer.String())
if err != nil {
return fmt.Errorf("failed to parse age secret key: %w", err)
}
@ -250,7 +266,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) (*memguard.LockedBuffer, error) {
func (cli *Instance) getSecretValue(vlt *vault.Vault, secretObj *secret.Secret) ([]byte, error) {
if os.Getenv(secret.EnvMnemonic) != "" {
return secretObj.GetValue(nil)
}

View File

@ -74,60 +74,60 @@ func TestAddSecretVariousSizes(t *testing.T) {
// Set up test environment
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Set test mnemonic
t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
// Create vault
vaultName := "test-vault"
_, err := vault.CreateVault(fs, stateDir, vaultName)
require.NoError(t, err)
// Set current vault
currentVaultPath := filepath.Join(stateDir, "currentvault")
vaultPath := filepath.Join(stateDir, "vaults.d", vaultName)
err = afero.WriteFile(fs, currentVaultPath, []byte(vaultPath), 0o600)
require.NoError(t, err)
// Get vault and set up long-term key
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
ltIdentity, err := agehd.DeriveIdentity("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", 0)
require.NoError(t, err)
vlt.Unlock(ltIdentity)
// Generate test data of specified size
testData := make([]byte, tt.size)
_, err = rand.Read(testData)
require.NoError(t, err)
// Add newline that will be stripped
testDataWithNewline := append(testData, '\n')
// Create fake stdin
stdin := bytes.NewReader(testDataWithNewline)
// Create command with fake stdin
cmd := &cobra.Command{}
cmd.SetIn(stdin)
// Create CLI instance
cli := NewCLIInstance()
cli.fs = fs
cli.stateDir = stateDir
cli.cmd = cmd
// Test adding the secret
secretName := fmt.Sprintf("test-secret-%d", tt.size)
err = cli.AddSecret(secretName, false)
if tt.shouldError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
require.NoError(t, err)
// Verify the secret was stored correctly
retrievedValue, err := vlt.GetSecret(secretName)
require.NoError(t, err)
@ -193,57 +193,57 @@ func TestImportSecretVariousSizes(t *testing.T) {
// Set up test environment
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Set test mnemonic
t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
// Create vault
vaultName := "test-vault"
_, err := vault.CreateVault(fs, stateDir, vaultName)
require.NoError(t, err)
// Set current vault
currentVaultPath := filepath.Join(stateDir, "currentvault")
vaultPath := filepath.Join(stateDir, "vaults.d", vaultName)
err = afero.WriteFile(fs, currentVaultPath, []byte(vaultPath), 0o600)
require.NoError(t, err)
// Get vault and set up long-term key
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
ltIdentity, err := agehd.DeriveIdentity("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", 0)
require.NoError(t, err)
vlt.Unlock(ltIdentity)
// Generate test data of specified size
testData := make([]byte, tt.size)
_, err = rand.Read(testData)
require.NoError(t, err)
// Write test data to file
testFile := fmt.Sprintf("/test/secret-%d.bin", tt.size)
err = afero.WriteFile(fs, testFile, testData, 0o600)
require.NoError(t, err)
// Create command
cmd := &cobra.Command{}
// Create CLI instance
cli := NewCLIInstance()
cli.fs = fs
cli.stateDir = stateDir
// Test importing the secret
secretName := fmt.Sprintf("imported-secret-%d", tt.size)
err = cli.ImportSecret(cmd, secretName, testFile, false)
if tt.shouldError {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errorMsg)
} else {
require.NoError(t, err)
// Verify the secret was stored correctly
retrievedValue, err := vlt.GetSecret(secretName)
require.NoError(t, err)
@ -257,22 +257,22 @@ func TestImportSecretVariousSizes(t *testing.T) {
func TestAddSecretBufferGrowth(t *testing.T) {
// Test various sizes that should trigger buffer growth
sizes := []int{
1, // Single byte
100, // Small
4095, // Just under initial 4KB
4096, // Exactly 4KB
4097, // Just over 4KB
8191, // Just under 8KB (first double)
8192, // Exactly 8KB
8193, // Just over 8KB
12288, // 12KB (should trigger second double)
16384, // 16KB
32768, // 32KB (after more doublings)
65536, // 64KB
131072, // 128KB
524288, // 512KB
1048576, // 1MB
2097152, // 2MB
1, // Single byte
100, // Small
4095, // Just under initial 4KB
4096, // Exactly 4KB
4097, // Just over 4KB
8191, // Just under 8KB (first double)
8192, // Exactly 8KB
8193, // Just over 8KB
12288, // 12KB (should trigger second double)
16384, // 16KB
32768, // 32KB (after more doublings)
65536, // 64KB
131072, // 128KB
524288, // 512KB
1048576, // 1MB
2097152, // 2MB
}
for _, size := range sizes {
@ -280,54 +280,54 @@ func TestAddSecretBufferGrowth(t *testing.T) {
// Set up test environment
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Set test mnemonic
t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
// Create vault
vaultName := "test-vault"
_, err := vault.CreateVault(fs, stateDir, vaultName)
require.NoError(t, err)
// Set current vault
currentVaultPath := filepath.Join(stateDir, "currentvault")
vaultPath := filepath.Join(stateDir, "vaults.d", vaultName)
err = afero.WriteFile(fs, currentVaultPath, []byte(vaultPath), 0o600)
require.NoError(t, err)
// Get vault and set up long-term key
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
ltIdentity, err := agehd.DeriveIdentity("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", 0)
require.NoError(t, err)
vlt.Unlock(ltIdentity)
// Create test data of exactly the specified size
// Use a pattern that's easy to verify
testData := make([]byte, size)
for i := range testData {
testData[i] = byte(i % 256)
}
// Create fake stdin without newline
stdin := bytes.NewReader(testData)
// Create command with fake stdin
cmd := &cobra.Command{}
cmd.SetIn(stdin)
// Create CLI instance
cli := NewCLIInstance()
cli.fs = fs
cli.stateDir = stateDir
cli.cmd = cmd
// Test adding the secret
secretName := fmt.Sprintf("buffer-test-%d", size)
err = cli.AddSecret(secretName, false)
require.NoError(t, err)
// Verify the secret was stored correctly
retrievedValue, err := vlt.GetSecret(secretName)
require.NoError(t, err)
@ -341,29 +341,29 @@ func TestAddSecretStreamingBehavior(t *testing.T) {
// Set up test environment
fs := afero.NewMemMapFs()
stateDir := "/test/state"
// Set test mnemonic
t.Setenv(secret.EnvMnemonic, "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")
// Create vault
vaultName := "test-vault"
_, err := vault.CreateVault(fs, stateDir, vaultName)
require.NoError(t, err)
// Set current vault
currentVaultPath := filepath.Join(stateDir, "currentvault")
vaultPath := filepath.Join(stateDir, "vaults.d", vaultName)
err = afero.WriteFile(fs, currentVaultPath, []byte(vaultPath), 0o600)
require.NoError(t, err)
// Get vault and set up long-term key
vlt, err := vault.GetCurrentVault(fs, stateDir)
require.NoError(t, err)
ltIdentity, err := agehd.DeriveIdentity("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", 0)
require.NoError(t, err)
vlt.Unlock(ltIdentity)
// Create a custom reader that simulates slow streaming input
// This will help verify our buffer handling works correctly with partial reads
testData := []byte(strings.Repeat("Hello, World! ", 1000)) // ~14KB
@ -371,21 +371,21 @@ func TestAddSecretStreamingBehavior(t *testing.T) {
data: testData,
chunkSize: 1000, // Read 1KB at a time
}
// Create command with slow reader as stdin
cmd := &cobra.Command{}
cmd.SetIn(slowReader)
// Create CLI instance
cli := NewCLIInstance()
cli.fs = fs
cli.stateDir = stateDir
cli.cmd = cmd
// Test adding the secret
err = cli.AddSecret("streaming-test", false)
require.NoError(t, err)
// Verify the secret was stored correctly
retrievedValue, err := vlt.GetSecret("streaming-test")
require.NoError(t, err)
@ -403,7 +403,7 @@ func (r *slowReader) Read(p []byte) (n int, err error) {
if r.offset >= len(r.data) {
return 0, io.EOF
}
// Read at most chunkSize bytes
remaining := len(r.data) - r.offset
toRead := r.chunkSize
@ -413,13 +413,13 @@ func (r *slowReader) Read(p []byte) (n int, err error) {
if toRead > len(p) {
toRead = len(p)
}
n = copy(p, r.data[r.offset:r.offset+toRead])
r.offset += n
if r.offset >= len(r.data) {
err = io.EOF
}
return n, err
}
}

View File

@ -1,313 +0,0 @@
//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

@ -1,77 +0,0 @@
//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) (*memguard.LockedBuffer, error) {
func DecryptWithIdentity(data []byte, identity age.Identity) ([]byte, 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) (*memguard.LockedBu
return nil, fmt.Errorf("failed to read decrypted data: %w", err)
}
// Create a secure buffer for the decrypted data
resultBuffer := memguard.NewBufferFromBytes(result)
return resultBuffer, nil
return result, nil
}
// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption
// 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")
}
// The passphrase parameter should be a LockedBuffer for secure memory handling
func EncryptWithPassphrase(data []byte, passphrase *memguard.LockedBuffer) ([]byte, error) {
if passphrase == nil {
return nil, fmt.Errorf("passphrase buffer is nil")
}
// Create recipient directly from passphrase - unavoidable string conversion due to age API
recipient, err := age.NewScryptRecipient(passphrase.String())
// Get the passphrase string temporarily
passphraseStr := passphrase.String()
recipient, err := age.NewScryptRecipient(passphraseStr)
if err != nil {
return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
}
return EncryptToRecipient(data, recipient)
// Create a secure buffer for the data
dataBuffer := memguard.NewBufferFromBytes(data)
defer dataBuffer.Destroy()
return EncryptToRecipient(dataBuffer, 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) (*memguard.LockedBuffer, error) {
func DecryptWithPassphrase(encryptedData []byte, passphrase *memguard.LockedBuffer) ([]byte, error) {
if passphrase == nil {
return nil, fmt.Errorf("passphrase buffer is nil")
}
// Create identity directly from passphrase - unavoidable string conversion due to age API
identity, err := age.NewScryptIdentity(passphrase.String())
// Get the passphrase string temporarily
passphraseStr := passphrase.String()
identity, err := age.NewScryptIdentity(passphraseStr)
if err != nil {
return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
}

View File

@ -107,22 +107,30 @@ func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
passphraseBuffer := memguard.NewBufferFromBytes([]byte(keychainData.AgePrivKeyPassphrase))
defer passphraseBuffer.Destroy()
agePrivKeyBuffer, err := DecryptWithPassphrase(encryptedAgePrivKeyData, passphraseBuffer)
agePrivKeyData, 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", agePrivKeyBuffer.Size()),
slog.Int("decrypted_length", len(agePrivKeyData)),
)
// 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())
@ -293,13 +301,13 @@ func getLongTermPrivateKey(fs afero.Fs, vault VaultInterface) (*memguard.LockedB
}
// Decrypt long-term private key using current unlocker
ltPrivKeyBuffer, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
}
// Return the decrypted key buffer
return ltPrivKeyBuffer, nil
// Return the decrypted key in a secure buffer
return memguard.NewBufferFromBytes(ltPrivKeyData), nil
}
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
@ -360,7 +368,7 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
passphraseBuffer := memguard.NewBufferFromBytes([]byte(agePrivKeyPassphrase))
defer passphraseBuffer.Destroy()
encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivKeyBuffer, passphraseBuffer)
encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivKeyBuffer.Bytes(), passphraseBuffer)
if err != nil {
return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
}
@ -401,12 +409,8 @@ 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, keychainDataBuffer); err != nil {
if err := storeInKeychain(keychainItemName, keychainDataBytes); err != nil {
return nil, fmt.Errorf("failed to store data in keychain: %w", err)
}
@ -462,17 +466,14 @@ func validateKeychainItemName(itemName string) error {
}
// storeInKeychain stores data in the macOS keychain using the security command
func storeInKeychain(itemName string, data *memguard.LockedBuffer) error {
if data == nil {
return fmt.Errorf("data buffer is nil")
}
func storeInKeychain(itemName string, data []byte) error {
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", data.String(),
"-w", string(data),
"-U") // Update if exists
if err := cmd.Run(); err != nil {

View File

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

View File

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

View File

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

View File

@ -62,8 +62,35 @@ 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) (*memguard.LockedBuffer, error) {
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
DebugWith("Getting secret value",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
@ -179,17 +206,16 @@ func (s *Secret) GetValue(unlocker Unlocker) (*memguard.LockedBuffer, error) {
// Decrypt the encrypted long-term private key using the unlocker
Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
ltPrivKeyBuffer, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
ltPrivKeyData, 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(ltPrivKeyBuffer.String())
ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
if err != nil {
Debug("Failed to parse long-term private key", "error", err, "secret_name", s.Name)

View File

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

View File

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

View File

@ -259,14 +259,13 @@ func updateVersionMetadata(fs afero.Fs, version *secret.Version, ltIdentity *age
}
// Decrypt version private key using long-term key
versionPrivKeyBuffer, err := secret.DecryptWithIdentity(encryptedPrivKey, ltIdentity)
versionPrivKeyData, 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(versionPrivKeyBuffer.String())
versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
if err != nil {
return fmt.Errorf("failed to parse version private key: %w", err)
}
@ -394,26 +393,21 @@ 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(result)),
slog.Int("decrypted_length", len(decryptedValue)),
)
// 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(result),
"is_empty", len(result) == 0)
"decrypted_value_length", len(decryptedValue),
"is_empty", len(decryptedValue) == 0)
return result, nil
return decryptedValue, nil
}
// UnlockVault unlocks the vault and returns the long-term private key

View File

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

View File

@ -157,23 +157,22 @@ 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())
ltPrivKeyBuffer, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockerIdentity)
ltPrivKeyData, 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", ltPrivKeyBuffer.Size()),
slog.Int("decrypted_length", len(ltPrivKeyData)),
)
// Parse long-term private key
secret.Debug("Parsing long-term private key", "vault_name", v.Name)
ltIdentity, err := age.ParseX25519Identity(ltPrivKeyBuffer.String())
ltIdentity, err := age.ParseX25519Identity(string(ltPrivKeyData))
if err != nil {
secret.Debug("Failed to parse long-term private key", "error", err, "vault_name", v.Name)