Compare commits
	
		
			10 Commits
		
	
	
		
			7596049828
			...
			d4f557631b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| d4f557631b | |||
| e53161188c | |||
| ff17b9b107 | |||
| 63cc06b93c | |||
| 8ec3fc877d | |||
| 819902f385 | |||
| 292564c6e7 | |||
| eef2332823 | |||
| e82d428b05 | |||
| 9cbe055791 | 
							
								
								
									
										23
									
								
								TODO.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								TODO.md
									
									
									
									
									
								
							@ -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
 | 
					addressed before the 1.0 release of the secret manager. Items are
 | 
				
			||||||
prioritized from most critical (top) to least critical (bottom).
 | 
					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
 | 
					## Code Cleanups
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* we shouldn't be passing around a statedir, it should be read from the
 | 
					* we shouldn't be passing around a statedir, it should be read from the
 | 
				
			||||||
 | 
				
			|||||||
@ -96,21 +96,13 @@ func (cli *Instance) Encrypt(secretName, inputFile, outputFile string) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		// Secret exists, get the age secret key from it
 | 
							// Secret exists, get the age secret key from it
 | 
				
			||||||
		secretValue, err := cli.getSecretValue(vlt, secretObj)
 | 
							secretBuffer, err := cli.getSecretValue(vlt, secretObj)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return fmt.Errorf("failed to get secret value: %w", err)
 | 
								return fmt.Errorf("failed to get secret value: %w", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							defer secretBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create secure buffer for the secret value
 | 
							ageSecretKey = secretBuffer.String()
 | 
				
			||||||
		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
 | 
							// Validate that it's a valid age secret key
 | 
				
			||||||
		if !isValidAgeSecretKey(ageSecretKey) {
 | 
							if !isValidAgeSecretKey(ageSecretKey) {
 | 
				
			||||||
@ -189,36 +181,28 @@ func (cli *Instance) Decrypt(secretName, inputFile, outputFile string) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get the age secret key from the secret
 | 
						// Get the age secret key from the secret
 | 
				
			||||||
	var secretValue []byte
 | 
						var secretBuffer *memguard.LockedBuffer
 | 
				
			||||||
	if os.Getenv(secret.EnvMnemonic) != "" {
 | 
						if os.Getenv(secret.EnvMnemonic) != "" {
 | 
				
			||||||
		secretValue, err = secretObj.GetValue(nil)
 | 
							secretBuffer, err = secretObj.GetValue(nil)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		unlocker, unlockErr := vlt.GetCurrentUnlocker()
 | 
							unlocker, unlockErr := vlt.GetCurrentUnlocker()
 | 
				
			||||||
		if unlockErr != nil {
 | 
							if unlockErr != nil {
 | 
				
			||||||
			return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
 | 
								return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		secretValue, err = secretObj.GetValue(unlocker)
 | 
							secretBuffer, err = secretObj.GetValue(unlocker)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to get secret value: %w", err)
 | 
							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
 | 
						// 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)
 | 
							return fmt.Errorf("secret '%s' does not contain a valid age secret key", secretName)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse the age secret key to get the identity
 | 
						// Parse the age secret key to get the identity
 | 
				
			||||||
	identity, err := age.ParseX25519Identity(secureBuffer.String())
 | 
						identity, err := age.ParseX25519Identity(secretBuffer.String())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to parse age secret key: %w", err)
 | 
							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
 | 
					// 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) != "" {
 | 
						if os.Getenv(secret.EnvMnemonic) != "" {
 | 
				
			||||||
		return secretObj.GetValue(nil)
 | 
							return secretObj.GetValue(nil)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										313
									
								
								internal/macse/enclave.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										313
									
								
								internal/macse/enclave.go
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										77
									
								
								internal/macse/enclave_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								internal/macse/enclave_test.go
									
									
									
									
									
										Normal 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")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -54,7 +54,7 @@ func EncryptToRecipient(data *memguard.LockedBuffer, recipient age.Recipient) ([
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DecryptWithIdentity decrypts data with an identity using age
 | 
					// 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)
 | 
						r, err := age.Decrypt(bytes.NewReader(data), identity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create decryptor: %w", err)
 | 
							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 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
 | 
					// EncryptWithPassphrase encrypts data using a passphrase with age's scrypt-based encryption
 | 
				
			||||||
// The passphrase parameter should be a LockedBuffer for secure memory handling
 | 
					// Both data and passphrase parameters should be LockedBuffers for secure memory handling
 | 
				
			||||||
func EncryptWithPassphrase(data []byte, passphrase *memguard.LockedBuffer) ([]byte, error) {
 | 
					func EncryptWithPassphrase(data *memguard.LockedBuffer, passphrase *memguard.LockedBuffer) ([]byte, error) {
 | 
				
			||||||
 | 
						if data == nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("data buffer is nil")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	if passphrase == nil {
 | 
						if passphrase == nil {
 | 
				
			||||||
		return nil, fmt.Errorf("passphrase buffer is nil")
 | 
							return nil, fmt.Errorf("passphrase buffer is nil")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get the passphrase string temporarily
 | 
						// Create recipient directly from passphrase - unavoidable string conversion due to age API
 | 
				
			||||||
	passphraseStr := passphrase.String()
 | 
						recipient, err := age.NewScryptRecipient(passphrase.String())
 | 
				
			||||||
	recipient, err := age.NewScryptRecipient(passphraseStr)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
 | 
							return nil, fmt.Errorf("failed to create scrypt recipient: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create a secure buffer for the data
 | 
						return EncryptToRecipient(data, recipient)
 | 
				
			||||||
	dataBuffer := memguard.NewBufferFromBytes(data)
 | 
					 | 
				
			||||||
	defer dataBuffer.Destroy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return EncryptToRecipient(dataBuffer, recipient)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DecryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
 | 
					// DecryptWithPassphrase decrypts data using a passphrase with age's scrypt-based decryption
 | 
				
			||||||
// The passphrase parameter should be a LockedBuffer for secure memory handling
 | 
					// 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 {
 | 
						if passphrase == nil {
 | 
				
			||||||
		return nil, fmt.Errorf("passphrase buffer is nil")
 | 
							return nil, fmt.Errorf("passphrase buffer is nil")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Get the passphrase string temporarily
 | 
						// Create identity directly from passphrase - unavoidable string conversion due to age API
 | 
				
			||||||
	passphraseStr := passphrase.String()
 | 
						identity, err := age.NewScryptIdentity(passphrase.String())
 | 
				
			||||||
	identity, err := age.NewScryptIdentity(passphraseStr)
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
 | 
							return nil, fmt.Errorf("failed to create scrypt identity: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -107,30 +107,22 @@ func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			|||||||
	passphraseBuffer := memguard.NewBufferFromBytes([]byte(keychainData.AgePrivKeyPassphrase))
 | 
						passphraseBuffer := memguard.NewBufferFromBytes([]byte(keychainData.AgePrivKeyPassphrase))
 | 
				
			||||||
	defer passphraseBuffer.Destroy()
 | 
						defer passphraseBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, passphraseBuffer)
 | 
						agePrivKeyBuffer, err := DecryptWithPassphrase(encryptedAgePrivKeyData, passphraseBuffer)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "unlocker_id", k.GetID())
 | 
							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)
 | 
							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",
 | 
						DebugWith("Successfully decrypted age private key with keychain passphrase",
 | 
				
			||||||
		slog.String("unlocker_id", k.GetID()),
 | 
							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
 | 
						// Step 6: Parse the decrypted age private key
 | 
				
			||||||
	Debug("Parsing decrypted age private key", "unlocker_id", k.GetID())
 | 
						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())
 | 
						ageIdentity, err := age.ParseX25519Identity(agePrivKeyBuffer.String())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse age private key", "error", err, "unlocker_id", k.GetID())
 | 
							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
 | 
						// Decrypt long-term private key using current unlocker
 | 
				
			||||||
	ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
 | 
						ltPrivKeyBuffer, err := DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Return the decrypted key in a secure buffer
 | 
						// Return the decrypted key buffer
 | 
				
			||||||
	return memguard.NewBufferFromBytes(ltPrivKeyData), nil
 | 
						return ltPrivKeyBuffer, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
 | 
					// 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))
 | 
						passphraseBuffer := memguard.NewBufferFromBytes([]byte(agePrivKeyPassphrase))
 | 
				
			||||||
	defer passphraseBuffer.Destroy()
 | 
						defer passphraseBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivKeyBuffer.Bytes(), passphraseBuffer)
 | 
						encryptedAgePrivKey, err := EncryptWithPassphrase(agePrivKeyBuffer, passphraseBuffer)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt age private key with passphrase: %w", err)
 | 
							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)
 | 
							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
 | 
						// 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)
 | 
							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
 | 
					// 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 {
 | 
						if err := validateKeychainItemName(itemName); err != nil {
 | 
				
			||||||
		return fmt.Errorf("invalid keychain item name: %w", err)
 | 
							return fmt.Errorf("invalid keychain item name: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	cmd := exec.Command("/usr/bin/security", "add-generic-password", //nolint:gosec
 | 
						cmd := exec.Command("/usr/bin/security", "add-generic-password", //nolint:gosec
 | 
				
			||||||
		"-a", itemName,
 | 
							"-a", itemName,
 | 
				
			||||||
		"-s", itemName,
 | 
							"-s", itemName,
 | 
				
			||||||
		"-w", string(data),
 | 
							"-w", data.String(),
 | 
				
			||||||
		"-U") // Update if exists
 | 
							"-U") // Update if exists
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := cmd.Run(); err != nil {
 | 
						if err := cmd.Run(); err != nil {
 | 
				
			||||||
 | 
				
			|||||||
@ -76,10 +76,11 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Test encrypting private key with passphrase
 | 
						// Test encrypting private key with passphrase
 | 
				
			||||||
	t.Run("EncryptPrivateKey", func(t *testing.T) {
 | 
						t.Run("EncryptPrivateKey", func(t *testing.T) {
 | 
				
			||||||
		privKeyData := []byte(agePrivateKey)
 | 
							privKeyBuffer := memguard.NewBufferFromBytes([]byte(agePrivateKey))
 | 
				
			||||||
 | 
							defer privKeyBuffer.Destroy()
 | 
				
			||||||
		passphraseBuffer := memguard.NewBufferFromBytes([]byte(testPassphrase))
 | 
							passphraseBuffer := memguard.NewBufferFromBytes([]byte(testPassphrase))
 | 
				
			||||||
		defer passphraseBuffer.Destroy()
 | 
							defer passphraseBuffer.Destroy()
 | 
				
			||||||
		encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphraseBuffer)
 | 
							encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyBuffer, passphraseBuffer)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			t.Fatalf("Failed to encrypt private key: %v", err)
 | 
								t.Fatalf("Failed to encrypt private key: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -84,30 +84,22 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			|||||||
	Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
 | 
						Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Decrypt the unlocker private key with passphrase
 | 
						// Decrypt the unlocker private key with passphrase
 | 
				
			||||||
	privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseBuffer)
 | 
						privKeyBuffer, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseBuffer)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt unlocker private key", "error", err, "unlocker_id", p.GetID())
 | 
							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)
 | 
							return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer privKeyBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DebugWith("Successfully decrypted unlocker private key",
 | 
						DebugWith("Successfully decrypted unlocker private key",
 | 
				
			||||||
		slog.String("unlocker_id", p.GetID()),
 | 
							slog.String("unlocker_id", p.GetID()),
 | 
				
			||||||
		slog.Int("decrypted_length", len(privKeyData)),
 | 
							slog.Int("decrypted_length", privKeyBuffer.Size()),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse the decrypted private key
 | 
						// Parse the decrypted private key
 | 
				
			||||||
	Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID())
 | 
						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())
 | 
						identity, err := age.ParseX25519Identity(privKeyBuffer.String())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())
 | 
							Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,10 @@ pinentry-mode loopback
 | 
				
			|||||||
	origDecryptFunc := secret.GPGDecryptFunc
 | 
						origDecryptFunc := secret.GPGDecryptFunc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Set custom GPG functions for this test
 | 
						// 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",
 | 
							cmd := exec.Command("gpg",
 | 
				
			||||||
			"--homedir", gnupgHomeDir,
 | 
								"--homedir", gnupgHomeDir,
 | 
				
			||||||
			"--batch",
 | 
								"--batch",
 | 
				
			||||||
@ -60,7 +63,7 @@ pinentry-mode loopback
 | 
				
			|||||||
		var stdout, stderr bytes.Buffer
 | 
							var stdout, stderr bytes.Buffer
 | 
				
			||||||
		cmd.Stdout = &stdout
 | 
							cmd.Stdout = &stdout
 | 
				
			||||||
		cmd.Stderr = &stderr
 | 
							cmd.Stderr = &stderr
 | 
				
			||||||
		cmd.Stdin = bytes.NewReader(data)
 | 
							cmd.Stdin = bytes.NewReader(data.Bytes())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := cmd.Run(); err != nil {
 | 
							if err := cmd.Run(); err != nil {
 | 
				
			||||||
			return nil, fmt.Errorf("GPG encryption failed: %w\nStderr: %s", err, stderr.String())
 | 
								return nil, fmt.Errorf("GPG encryption failed: %w\nStderr: %s", err, stderr.String())
 | 
				
			||||||
@ -69,7 +72,7 @@ pinentry-mode loopback
 | 
				
			|||||||
		return stdout.Bytes(), nil
 | 
							return stdout.Bytes(), nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secret.GPGDecryptFunc = func(encryptedData []byte) ([]byte, error) {
 | 
						secret.GPGDecryptFunc = func(encryptedData []byte) (*memguard.LockedBuffer, error) {
 | 
				
			||||||
		cmd := exec.Command("gpg",
 | 
							cmd := exec.Command("gpg",
 | 
				
			||||||
			"--homedir", gnupgHomeDir,
 | 
								"--homedir", gnupgHomeDir,
 | 
				
			||||||
			"--batch",
 | 
								"--batch",
 | 
				
			||||||
@ -88,7 +91,8 @@ pinentry-mode loopback
 | 
				
			|||||||
			return nil, fmt.Errorf("GPG decryption failed: %w\nStderr: %s", err, stderr.String())
 | 
								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
 | 
						// Restore original functions after test
 | 
				
			||||||
@ -444,8 +448,9 @@ Passphrase: ` + testPassphrase + `
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// GPG encrypt the private key using our custom encrypt function
 | 
							// GPG encrypt the private key using our custom encrypt function
 | 
				
			||||||
		privKeyData := []byte(ageIdentity.String())
 | 
							privKeyBuffer := memguard.NewBufferFromBytes([]byte(ageIdentity.String()))
 | 
				
			||||||
		encryptedOutput, err := secret.GPGEncryptFunc(privKeyData, keyID)
 | 
							defer privKeyBuffer.Destroy()
 | 
				
			||||||
 | 
							encryptedOutput, err := secret.GPGEncryptFunc(privKeyBuffer, keyID)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			t.Fatalf("Failed to encrypt with GPG: %v", err)
 | 
								t.Fatalf("Failed to encrypt with GPG: %v", err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,11 +20,13 @@ import (
 | 
				
			|||||||
var (
 | 
					var (
 | 
				
			||||||
	// GPGEncryptFunc is the function used for GPG encryption
 | 
						// GPGEncryptFunc is the function used for GPG encryption
 | 
				
			||||||
	// Can be overridden in tests to provide a non-interactive implementation
 | 
						// 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
 | 
						// GPGDecryptFunc is the function used for GPG decryption
 | 
				
			||||||
	// Can be overridden in tests to provide a non-interactive implementation
 | 
						// 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
 | 
						// gpgKeyIDRegex validates GPG key IDs
 | 
				
			||||||
	// Allows either:
 | 
						// Allows either:
 | 
				
			||||||
@ -79,21 +81,22 @@ func (p *PGPUnlocker) GetIdentity() (*age.X25519Identity, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Step 2: Decrypt the age private key using GPG
 | 
						// Step 2: Decrypt the age private key using GPG
 | 
				
			||||||
	Debug("Decrypting age private key with GPG", "unlocker_id", p.GetID())
 | 
						Debug("Decrypting age private key with GPG", "unlocker_id", p.GetID())
 | 
				
			||||||
	agePrivKeyData, err := GPGDecryptFunc(encryptedAgePrivKeyData)
 | 
						agePrivKeyBuffer, err := GPGDecryptFunc(encryptedAgePrivKeyData)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt age private key with GPG", "error", err, "unlocker_id", p.GetID())
 | 
							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)
 | 
							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",
 | 
						DebugWith("Successfully decrypted age private key with GPG",
 | 
				
			||||||
		slog.String("unlocker_id", p.GetID()),
 | 
							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
 | 
						// Step 3: Parse the decrypted age private key
 | 
				
			||||||
	Debug("Parsing decrypted age private key", "unlocker_id", p.GetID())
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse age private key", "error", err, "unlocker_id", p.GetID())
 | 
							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()))
 | 
						agePrivateKeyBuffer := memguard.NewBufferFromBytes([]byte(ageIdentity.String()))
 | 
				
			||||||
	defer agePrivateKeyBuffer.Destroy()
 | 
						defer agePrivateKeyBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBuffer.Bytes(), gpgKeyID)
 | 
						encryptedAgePrivKey, err := GPGEncryptFunc(agePrivateKeyBuffer, gpgKeyID)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt age private key with GPG: %w", err)
 | 
							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
 | 
					// 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 {
 | 
						if err := validateGPGKeyID(keyID); err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("invalid GPG key ID: %w", err)
 | 
							return nil, fmt.Errorf("invalid GPG key ID: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cmd := exec.Command("gpg", "--trust-model", "always", "--armor", "--encrypt", "-r", keyID)
 | 
						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()
 | 
						output, err := cmd.Output()
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@ -365,7 +371,7 @@ func gpgEncryptDefault(data []byte, keyID string) ([]byte, error) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// gpgDecryptDefault is the default implementation of GPG decryption
 | 
					// 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 := exec.Command("gpg", "--quiet", "--decrypt")
 | 
				
			||||||
	cmd.Stdin = strings.NewReader(string(encryptedData))
 | 
						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 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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					// 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",
 | 
						DebugWith("Getting secret value",
 | 
				
			||||||
		slog.String("secret_name", s.Name),
 | 
							slog.String("secret_name", s.Name),
 | 
				
			||||||
		slog.String("vault_name", s.vault.GetName()),
 | 
							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
 | 
						// Decrypt the encrypted long-term private key using the unlocker
 | 
				
			||||||
	Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)
 | 
							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)
 | 
							return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer ltPrivKeyBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse the long-term private key
 | 
						// Parse the long-term private key
 | 
				
			||||||
	Debug("Parsing long-term private key", "secret_name", s.Name)
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse long-term private key", "error", err, "secret_name", s.Name)
 | 
							Debug("Failed to parse long-term private key", "error", err, "secret_name", s.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -277,15 +277,16 @@ func (sv *Version) LoadMetadata(ltIdentity *age.X25519Identity) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 2: Decrypt version private key using long-term key
 | 
						// Step 2: Decrypt version private key using long-term key
 | 
				
			||||||
	versionPrivKeyData, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
 | 
						versionPrivKeyBuffer, err := DecryptWithIdentity(encryptedPrivKey, ltIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
 | 
							Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return fmt.Errorf("failed to decrypt version private key: %w", err)
 | 
							return fmt.Errorf("failed to decrypt version private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer versionPrivKeyBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 3: Parse version private key
 | 
						// Step 3: Parse version private key
 | 
				
			||||||
	versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
 | 
						versionIdentity, err := age.ParseX25519Identity(versionPrivKeyBuffer.String())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse version private key", "error", err, "version", sv.Version)
 | 
							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
 | 
						// Step 5: Decrypt metadata using version key
 | 
				
			||||||
	metadataBytes, err := DecryptWithIdentity(encryptedMetadata, versionIdentity)
 | 
						metadataBuffer, err := DecryptWithIdentity(encryptedMetadata, versionIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt version metadata", "error", err, "version", sv.Version)
 | 
							Debug("Failed to decrypt version metadata", "error", err, "version", sv.Version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return fmt.Errorf("failed to decrypt version metadata: %w", err)
 | 
							return fmt.Errorf("failed to decrypt version metadata: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer metadataBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Step 6: Unmarshal metadata
 | 
						// Step 6: Unmarshal metadata
 | 
				
			||||||
	var metadata VersionMetadata
 | 
						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)
 | 
							Debug("Failed to unmarshal version metadata", "error", err, "version", sv.Version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return fmt.Errorf("failed to unmarshal version metadata: %w", err)
 | 
							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
 | 
					// 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",
 | 
						DebugWith("Getting version value",
 | 
				
			||||||
		slog.String("secret_name", sv.SecretName),
 | 
							slog.String("secret_name", sv.SecretName),
 | 
				
			||||||
		slog.String("version", sv.Version),
 | 
							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
 | 
						// Step 2: Decrypt version private key using long-term key
 | 
				
			||||||
	Debug("Decrypting version private key with long-term identity", "version", sv.Version)
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
 | 
							Debug("Failed to decrypt version private key", "error", err, "version", sv.Version)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return nil, fmt.Errorf("failed to decrypt version private key: %w", err)
 | 
							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
 | 
						// Step 3: Parse version private key
 | 
				
			||||||
	versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
 | 
						versionIdentity, err := age.ParseX25519Identity(versionPrivKeyBuffer.String())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to parse version private key", "error", err, "version", sv.Version)
 | 
							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
 | 
						// Step 5: Decrypt value using version key
 | 
				
			||||||
	Debug("Decrypting value with version identity", "version", sv.Version)
 | 
						Debug("Decrypting value with version identity", "version", sv.Version)
 | 
				
			||||||
	value, err := DecryptWithIdentity(encryptedValue, versionIdentity)
 | 
						valueBuffer, err := DecryptWithIdentity(encryptedValue, versionIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		Debug("Failed to decrypt version value", "error", err, "version", sv.Version)
 | 
							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",
 | 
						Debug("Successfully retrieved version value",
 | 
				
			||||||
		"version", sv.Version,
 | 
							"version", sv.Version,
 | 
				
			||||||
		"value_length", len(value),
 | 
							"value_length", valueBuffer.Size(),
 | 
				
			||||||
		"is_empty", len(value) == 0)
 | 
							"is_empty", valueBuffer.Size() == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return value, nil
 | 
						return valueBuffer, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ListVersions lists all versions of a secret
 | 
					// ListVersions lists all versions of a secret
 | 
				
			||||||
 | 
				
			|||||||
@ -255,10 +255,11 @@ func TestSecretVersionGetValue(t *testing.T) {
 | 
				
			|||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Retrieve the value
 | 
						// Retrieve the value
 | 
				
			||||||
	retrievedValue, err := sv.GetValue(ltIdentity)
 | 
						retrievedBuffer, err := sv.GetValue(ltIdentity)
 | 
				
			||||||
	require.NoError(t, err)
 | 
						require.NoError(t, err)
 | 
				
			||||||
 | 
						defer retrievedBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	assert.Equal(t, expectedValue, retrievedValue)
 | 
						assert.Equal(t, expectedValue, retrievedBuffer.Bytes())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestListVersions(t *testing.T) {
 | 
					func TestListVersions(t *testing.T) {
 | 
				
			||||||
 | 
				
			|||||||
@ -259,13 +259,14 @@ func updateVersionMetadata(fs afero.Fs, version *secret.Version, ltIdentity *age
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Decrypt version private key using long-term key
 | 
						// Decrypt version private key using long-term key
 | 
				
			||||||
	versionPrivKeyData, err := secret.DecryptWithIdentity(encryptedPrivKey, ltIdentity)
 | 
						versionPrivKeyBuffer, err := secret.DecryptWithIdentity(encryptedPrivKey, ltIdentity)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to decrypt version private key: %w", err)
 | 
							return fmt.Errorf("failed to decrypt version private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer versionPrivKeyBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse version private key
 | 
						// Parse version private key
 | 
				
			||||||
	versionIdentity, err := age.ParseX25519Identity(string(versionPrivKeyData))
 | 
						versionIdentity, err := age.ParseX25519Identity(versionPrivKeyBuffer.String())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return fmt.Errorf("failed to parse version private key: %w", err)
 | 
							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)
 | 
							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",
 | 
						secret.DebugWith("Successfully decrypted secret version",
 | 
				
			||||||
		slog.String("secret_name", name),
 | 
							slog.String("secret_name", name),
 | 
				
			||||||
		slog.String("version", version),
 | 
							slog.String("version", version),
 | 
				
			||||||
		slog.String("vault_name", v.Name),
 | 
							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
 | 
						// Debug: Log metadata about the decrypted value without exposing the actual secret
 | 
				
			||||||
	secret.Debug("Vault secret decryption debug info",
 | 
						secret.Debug("Vault secret decryption debug info",
 | 
				
			||||||
		"secret_name", name,
 | 
							"secret_name", name,
 | 
				
			||||||
		"version", version,
 | 
							"version", version,
 | 
				
			||||||
		"decrypted_value_length", len(decryptedValue),
 | 
							"decrypted_value_length", len(result),
 | 
				
			||||||
		"is_empty", len(decryptedValue) == 0)
 | 
							"is_empty", len(result) == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return decryptedValue, nil
 | 
						return result, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// UnlockVault unlocks the vault and returns the long-term private key
 | 
					// UnlockVault unlocks the vault and returns the long-term private key
 | 
				
			||||||
 | 
				
			|||||||
@ -346,7 +346,9 @@ func (v *Vault) CreatePassphraseUnlocker(passphrase *memguard.LockedBuffer) (*se
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Encrypt private key with passphrase
 | 
						// Encrypt private key with passphrase
 | 
				
			||||||
	privKeyStr := unlockerIdentity.String()
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
 | 
							return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -157,22 +157,23 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Decrypt long-term private key using unlocker
 | 
						// Decrypt long-term private key using unlocker
 | 
				
			||||||
	secret.Debug("Decrypting long-term private key with unlocker", "unlocker_type", unlocker.GetType())
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		secret.Debug("Failed to decrypt long-term private key", "error", err, "unlocker_type", unlocker.GetType())
 | 
							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)
 | 
							return nil, fmt.Errorf("failed to decrypt long-term private key: %w", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						defer ltPrivKeyBuffer.Destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	secret.DebugWith("Successfully decrypted long-term private key",
 | 
						secret.DebugWith("Successfully decrypted long-term private key",
 | 
				
			||||||
		slog.String("vault_name", v.Name),
 | 
							slog.String("vault_name", v.Name),
 | 
				
			||||||
		slog.String("unlocker_type", unlocker.GetType()),
 | 
							slog.String("unlocker_type", unlocker.GetType()),
 | 
				
			||||||
		slog.Int("decrypted_length", len(ltPrivKeyData)),
 | 
							slog.Int("decrypted_length", ltPrivKeyBuffer.Size()),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Parse long-term private key
 | 
						// Parse long-term private key
 | 
				
			||||||
	secret.Debug("Parsing long-term private key", "vault_name", v.Name)
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		secret.Debug("Failed to parse long-term private key", "error", err, "vault_name", v.Name)
 | 
							secret.Debug("Failed to parse long-term private key", "error", err, "vault_name", v.Name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user