Compare commits

12 Commits

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

View File

@@ -23,7 +23,10 @@
"Bash(ls:*)", "Bash(ls:*)",
"WebFetch(domain:golangci-lint.run)", "WebFetch(domain:golangci-lint.run)",
"Bash(go:*)", "Bash(go:*)",
"WebFetch(domain:pkg.go.dev)" "WebFetch(domain:pkg.go.dev)",
"Bash(CGO_ENABLED=1 make fmt)",
"Bash(CGO_ENABLED=1 make test)",
"Bash(git merge:*)"
], ],
"deny": [] "deny": []
} }

View File

@@ -1,12 +1,12 @@
# Secret - Hierarchical Secret Manager # Secret - Hierarchical Secret Manager
Secret is a modern, secure command-line secret manager that implements a hierarchical key architecture for storing and managing sensitive data. It supports multiple vaults, various unlock mechanisms, and provides secure storage using the Age encryption library. Secret is a command-line secret manager that implements a hierarchical key architecture for storing and managing sensitive data. It supports multiple vaults, various unlock mechanisms, and provides secure storage using the Age encryption library.
## Core Architecture ## Core Architecture
### Three-Layer Key Hierarchy ### Three-Layer Key Hierarchy
Secret implements a sophisticated three-layer key architecture: Secret implements a three-layer key architecture:
1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide the foundation for all encryption 1. **Long-term Keys**: Derived from BIP39 mnemonic phrases, these provide the foundation for all encryption
2. **Unlockers**: Short-term keys that encrypt the long-term keys, supporting multiple authentication methods 2. **Unlockers**: Short-term keys that encrypt the long-term keys, supporting multiple authentication methods
@@ -16,7 +16,7 @@ Secret implements a sophisticated three-layer key architecture:
Each secret maintains a history of versions, with each version having: Each secret maintains a history of versions, with each version having:
- Its own encryption key pair - Its own encryption key pair
- Encrypted metadata including creation time and validity period - Metadata (unencrypted) including creation time and validity period
- Immutable value storage - Immutable value storage
- Atomic version switching via symlink updates - Atomic version switching via symlink updates
@@ -125,6 +125,7 @@ Creates a new unlocker of the specified type:
**Types:** **Types:**
- `passphrase`: Traditional passphrase-protected unlocker - `passphrase`: Traditional passphrase-protected unlocker
- `pgp`: Uses an existing GPG key for encryption/decryption - `pgp`: Uses an existing GPG key for encryption/decryption
- `keychain`: macOS Keychain integration (macOS only)
**Options:** **Options:**
- `--keyid <id>`: GPG key ID (required for PGP type) - `--keyid <id>`: GPG key ID (required for PGP type)
@@ -169,7 +170,7 @@ Decrypts data using an Age key stored as a secret.
│ │ │ │ │ │ ├── pub.age # Version public key │ │ │ │ │ │ ├── pub.age # Version public key
│ │ │ │ │ │ ├── priv.age # Version private key (encrypted) │ │ │ │ │ │ ├── priv.age # Version private key (encrypted)
│ │ │ │ │ │ ├── value.age # Encrypted value │ │ │ │ │ │ ├── value.age # Encrypted value
│ │ │ │ │ │ └── metadata.age # Encrypted metadata │ │ │ │ │ │ └── metadata.json # Unencrypted metadata
│ │ │ │ │ └── 20231216.001/ # Another version │ │ │ │ │ └── 20231216.001/ # Another version
│ │ │ │ └── current -> versions/20231216.001 │ │ │ │ └── current -> versions/20231216.001
│ │ │ └── database%password/ # Secret: database/password │ │ │ └── database%password/ # Secret: database/password
@@ -207,6 +208,18 @@ Unlockers provide different authentication methods to access the long-term keys:
- Leverages existing key management workflows - Leverages existing key management workflows
- Strong authentication through GPG - Strong authentication through GPG
3. **Keychain Unlockers** (macOS only):
- Stores unlock keys in macOS Keychain
- Protected by system authentication (Touch ID, password)
- Automatic unlocking when Keychain is unlocked
- Cross-application integration
4. **Secure Enclave Unlockers** (macOS - planned):
- Hardware-backed key storage using Apple Secure Enclave
- Currently partially implemented but non-functional
- Requires Apple Developer Program membership and code signing entitlements
- Full implementation blocked by entitlement requirements
Each vault maintains its own set of unlockers and one long-term key. The long-term key is encrypted to each unlocker, allowing any authorized unlocker to access vault secrets. Each vault maintains its own set of unlockers and one long-term key. The long-term key is encrypted to each unlocker, allowing any authorized unlocker to access vault secrets.
#### Secret-specific Keys #### Secret-specific Keys
@@ -241,6 +254,8 @@ Each vault maintains its own set of unlockers and one long-term key. The long-te
### Hardware Integration ### Hardware Integration
- Hardware token support via PGP/GPG integration - Hardware token support via PGP/GPG integration
- macOS Keychain integration for system-level security
- Secure Enclave support planned (requires Apple Developer Program)
## Examples ## Examples
@@ -285,6 +300,7 @@ secret vault list
# Add multiple unlock methods # Add multiple unlock methods
secret unlockers add passphrase # Password-based secret unlockers add passphrase # Password-based
secret unlockers add pgp --keyid ABCD1234 # GPG key secret unlockers add pgp --keyid ABCD1234 # GPG key
secret unlockers add keychain # macOS Keychain (macOS only)
# List unlockers # List unlockers
secret unlockers list secret unlockers list
@@ -316,7 +332,7 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
### File Formats ### File Formats
- **Age Files**: Standard Age encryption format (.age extension) - **Age Files**: Standard Age encryption format (.age extension)
- **Metadata**: JSON format with timestamps and type information - **Metadata**: Unencrypted JSON format with timestamps and type information
- **Vault Metadata**: JSON containing vault name, creation time, derivation index, and public key hash - **Vault Metadata**: JSON containing vault name, creation time, derivation index, and public key hash
### Vault Management ### Vault Management
@@ -325,8 +341,8 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
- **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived - **Automatic Key Derivation**: When creating vaults with a mnemonic, keys are automatically derived
### Cross-Platform Support ### Cross-Platform Support
- **macOS**: Full support including Keychain integration - **macOS**: Full support including Keychain and planned Secure Enclave integration
- **Linux**: Full support (excluding Keychain features) - **Linux**: Full support (excluding macOS-specific features)
- **Windows**: Basic support (filesystem operations only) - **Windows**: Basic support (filesystem operations only)
## Security Considerations ## Security Considerations
@@ -367,9 +383,18 @@ go test -tags=integration -v ./internal/cli # Integration tests
## Features ## Features
- **Multiple Authentication Methods**: Supports passphrase-based and PGP-based unlockers - **Multiple Authentication Methods**: Supports passphrase, PGP, and macOS Keychain unlockers
- **Vault Isolation**: Complete separation between different vaults - **Vault Isolation**: Complete separation between different vaults
- **Per-Secret Encryption**: Each secret has its own encryption key - **Per-Secret Encryption**: Each secret has its own encryption key
- **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases - **BIP39 Mnemonic Support**: Keyless operation using mnemonic phrases
- **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems - **Cross-Platform**: Works on macOS, Linux, and other Unix-like systems
# Author
Made with love and lots of expensive SOTA AI by [sneak](https://sneak.berlin) in Berlin in the summer of 2025.
Released as a free software gift to the world, no strings attached, under the [WTFPL](https://www.wtfpl.net/) license.
Contact: [sneak@sneak.berlin](mailto:sneak@sneak.berlin)
[https://keys.openpgp.org/vks/v1/by-fingerprint/5539AD00DE4C42F3AFE11575052443F4DF2A55C2](https://keys.openpgp.org/vks/v1/by-fingerprint/5539AD00DE4C42F3AFE11575052443F4DF2A55C2)

45
TODO.md
View File

@@ -4,6 +4,51 @@ 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 BLOCKERS FOR 1.0 RELEASE
### Command Injection Vulnerabilities
- [ ] **1. PGP command injection risk**: `internal/secret/pgpunlocker.go:323-327` - GPG key IDs passed directly to exec.Command without proper escaping
- [ ] **2. Keychain command injection risk**: `internal/secret/keychainunlocker.go:472-476` - data.String() passed to security command without escaping
### Memory Security Critical Issues
- [ ] **3. Plain text passphrase in memory**: `internal/secret/keychainunlocker.go:342,393-396` - KeychainData struct stores AgePrivKeyPassphrase as unprotected string
- [ ] **4. Sensitive string conversions**: `internal/secret/keychainunlocker.go:356`, `internal/secret/pgpunlocker.go:256`, `internal/secret/version.go:155` - Age identity .String() creates unprotected copies
### Race Conditions (Data Corruption Risk)
- [ ] **5. No file locking mechanism**: `internal/vault/secrets.go:142-176` - Multiple concurrent operations can corrupt vault state
- [ ] **6. Non-atomic file operations**: Various locations - Interrupted writes leave vault inconsistent
### Input Validation Vulnerabilities
- [ ] **7. Path traversal risk**: `internal/vault/secrets.go:75-99` - Secret names allow dots which could enable traversal attacks with encoding
- [ ] **8. Missing size limits**: `internal/vault/secrets.go:102` - No maximum secret size allows DoS via memory exhaustion
### Timing Attack Vulnerabilities
- [ ] **9. Non-constant-time passphrase comparison**: `internal/cli/init.go:209-216` - bytes.Equal() vulnerable to timing attacks
- [ ] **10. Non-constant-time key validation**: `internal/vault/vault.go:95-100` - Public key comparison leaks timing information
## 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

1
go.mod
View File

@@ -9,6 +9,7 @@ require (
github.com/btcsuite/btcd/btcec/v2 v2.1.3 github.com/btcsuite/btcd/btcec/v2 v2.1.3
github.com/btcsuite/btcd/btcutil v1.1.6 github.com/btcsuite/btcd/btcutil v1.1.6
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d
github.com/keybase/go-keychain v0.0.0-20230307172405-3e4884637dd1
github.com/oklog/ulid/v2 v2.1.1 github.com/oklog/ulid/v2 v2.1.1
github.com/spf13/afero v1.14.0 github.com/spf13/afero v1.14.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1

2
go.sum
View File

@@ -63,6 +63,8 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/keybase/go-keychain v0.0.0-20230307172405-3e4884637dd1 h1:yi1W8qcFJ2plmaGJFN1npm0KQviWPMCtQOYuwDT6Swk=
github.com/keybase/go-keychain v0.0.0-20230307172405-3e4884637dd1/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=

View File

@@ -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)
} }

View File

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

17
internal/macse/README.md Normal file
View File

@@ -0,0 +1,17 @@
# secure enclave
```
akrotiri:~/dev/secret/internal/macse$ CGO_ENABLED=1 go test ./...
--- FAIL: TestEnclaveKeyEncryption (0.04s)
enclave_test.go:16: Failed to create enclave key: failed to create enclave key: error code -34018
--- FAIL: TestEnclaveKeyPersistence (0.01s)
enclave_test.go:52: Failed to create enclave key: failed to create enclave key: error code -34018
```
This works with temporary keys. When you try to use persistent keys, you
get the above error, because to persist keys in the SE you must have the
appropriate entitlements from Apple, which is only possible with an Apple
Developer Program paid membership (which requires doxxing yourself, and
paying them).
So this is a dead end for now.

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

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

View File

@@ -0,0 +1,87 @@
//go:build darwin
// +build darwin
package macse
import (
"bytes"
"testing"
)
func TestEnclaveKeyEncryption(t *testing.T) {
// Skip: Secure Enclave access requires Apple Developer Enterprise (ADE) membership,
// proper code signing, and entitlements for non-ephemeral keys.
// Without these, only ephemeral keys work which are not suitable for our use case.
t.Skip("Skipping: Requires ADE membership, signing, and entitlements for non-ephemeral keys")
// 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) {
// Skip: Secure Enclave access requires Apple Developer Enterprise (ADE) membership,
// proper code signing, and entitlements for non-ephemeral keys.
// Without these, only ephemeral keys work which are not suitable for our use case.
t.Skip("Skipping: Requires ADE membership, signing, and entitlements for non-ephemeral keys")
// 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 // 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)
} }

View File

@@ -6,19 +6,21 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"time" "time"
"filippo.io/age" "filippo.io/age"
"git.eeqj.de/sneak/secret/pkg/agehd" "git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard" "github.com/awnumar/memguard"
keychain "github.com/keybase/go-keychain"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
const ( const (
agePrivKeyPassphraseLength = 64 agePrivKeyPassphraseLength = 64
KEYCHAIN_APP_IDENTIFIER = "berlin.sneak.app.secret"
) )
// keychainItemNameRegex validates keychain item names // keychainItemNameRegex validates keychain item names
@@ -107,30 +109,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 +295,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 +362,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 +403,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)
} }
@@ -442,13 +440,11 @@ func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, er
}, nil }, nil
} }
// checkMacOSAvailable verifies that we're running on macOS and security command is available // checkMacOSAvailable verifies that we're running on macOS
func checkMacOSAvailable() error { func checkMacOSAvailable() error {
cmd := exec.Command("/usr/bin/security", "help") if runtime.GOOS != "darwin" {
if err := cmd.Run(); err != nil { return fmt.Errorf("keychain unlockers are only supported on macOS, current OS: %s", runtime.GOOS)
return fmt.Errorf("macOS security command not available: %w (keychain unlockers are only supported on macOS)", err)
} }
return nil return nil
} }
@@ -465,59 +461,79 @@ func validateKeychainItemName(itemName string) error {
return nil return nil
} }
// storeInKeychain stores data in the macOS keychain using the security command // storeInKeychain stores data in the macOS keychain using keybase/go-keychain
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
"-a", itemName,
"-s", itemName,
"-w", string(data),
"-U") // Update if exists
if err := cmd.Run(); err != nil { item := keychain.NewItem()
item.SetSecClass(keychain.SecClassGenericPassword)
item.SetService(KEYCHAIN_APP_IDENTIFIER)
item.SetAccount(itemName)
item.SetLabel(fmt.Sprintf("%s - %s", KEYCHAIN_APP_IDENTIFIER, itemName))
item.SetDescription("Secret vault keychain data")
item.SetComment("This item stores encrypted key material for the secret vault")
item.SetData([]byte(data.String()))
item.SetSynchronizable(keychain.SynchronizableNo)
// Use AccessibleWhenUnlockedThisDeviceOnly for better security and to trigger auth
item.SetAccessible(keychain.AccessibleWhenUnlockedThisDeviceOnly)
// First try to delete any existing item
deleteItem := keychain.NewItem()
deleteItem.SetSecClass(keychain.SecClassGenericPassword)
deleteItem.SetService(KEYCHAIN_APP_IDENTIFIER)
deleteItem.SetAccount(itemName)
keychain.DeleteItem(deleteItem) // Ignore error as item might not exist
// Add the new item
if err := keychain.AddItem(item); err != nil {
return fmt.Errorf("failed to store item in keychain: %w", err) return fmt.Errorf("failed to store item in keychain: %w", err)
} }
return nil return nil
} }
// retrieveFromKeychain retrieves data from the macOS keychain using the security command // retrieveFromKeychain retrieves data from the macOS keychain using keybase/go-keychain
func retrieveFromKeychain(itemName string) ([]byte, error) { func retrieveFromKeychain(itemName string) ([]byte, error) {
if err := validateKeychainItemName(itemName); err != nil { if err := validateKeychainItemName(itemName); err != nil {
return nil, fmt.Errorf("invalid keychain item name: %w", err) return nil, fmt.Errorf("invalid keychain item name: %w", err)
} }
cmd := exec.Command("/usr/bin/security", "find-generic-password", //nolint:gosec query := keychain.NewItem()
"-a", itemName, query.SetSecClass(keychain.SecClassGenericPassword)
"-s", itemName, query.SetService(KEYCHAIN_APP_IDENTIFIER)
"-w") // Return password only query.SetAccount(itemName)
query.SetMatchLimit(keychain.MatchLimitOne)
query.SetReturnData(true)
output, err := cmd.Output() results, err := keychain.QueryItem(query)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve item from keychain: %w", err) return nil, fmt.Errorf("failed to retrieve item from keychain: %w", err)
} }
// Remove trailing newline if present if len(results) == 0 {
if len(output) > 0 && output[len(output)-1] == '\n' { return nil, fmt.Errorf("keychain item not found: %s", itemName)
output = output[:len(output)-1]
} }
return output, nil return results[0].Data, nil
} }
// deleteFromKeychain removes an item from the macOS keychain using the security command // deleteFromKeychain removes an item from the macOS keychain using keybase/go-keychain
func deleteFromKeychain(itemName string) error { func deleteFromKeychain(itemName string) error {
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", "delete-generic-password", //nolint:gosec item := keychain.NewItem()
"-a", itemName, item.SetSecClass(keychain.SecClassGenericPassword)
"-s", itemName) item.SetService(KEYCHAIN_APP_IDENTIFIER)
item.SetAccount(itemName)
if err := cmd.Run(); err != nil { if err := keychain.DeleteItem(item); err != nil {
return fmt.Errorf("failed to delete item from keychain: %w", err) return fmt.Errorf("failed to delete item from keychain: %w", err)
} }

View File

@@ -0,0 +1,167 @@
//go:build darwin
// +build darwin
package secret
import (
"encoding/hex"
"runtime"
"testing"
"github.com/awnumar/memguard"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKeychainStoreRetrieveDelete(t *testing.T) {
// Skip test if not on macOS
if runtime.GOOS != "darwin" {
t.Skip("Keychain tests only run on macOS")
}
// Test data
testItemName := "test-secret-keychain-item"
testData := "test-secret-data-12345"
testBuffer := memguard.NewBufferFromBytes([]byte(testData))
defer testBuffer.Destroy()
// Clean up any existing item first
_ = deleteFromKeychain(testItemName)
// Test 1: Store data in keychain
err := storeInKeychain(testItemName, testBuffer)
require.NoError(t, err, "Failed to store data in keychain")
// Test 2: Retrieve data from keychain
retrievedData, err := retrieveFromKeychain(testItemName)
require.NoError(t, err, "Failed to retrieve data from keychain")
assert.Equal(t, testData, string(retrievedData), "Retrieved data doesn't match stored data")
// Test 3: Update existing item (store again with different data)
newTestData := "updated-test-data-67890"
newTestBuffer := memguard.NewBufferFromBytes([]byte(newTestData))
defer newTestBuffer.Destroy()
err = storeInKeychain(testItemName, newTestBuffer)
require.NoError(t, err, "Failed to update data in keychain")
// Verify updated data
retrievedData, err = retrieveFromKeychain(testItemName)
require.NoError(t, err, "Failed to retrieve updated data from keychain")
assert.Equal(t, newTestData, string(retrievedData), "Retrieved data doesn't match updated data")
// Test 4: Delete from keychain
err = deleteFromKeychain(testItemName)
require.NoError(t, err, "Failed to delete data from keychain")
// Test 5: Verify item is deleted (should fail to retrieve)
_, err = retrieveFromKeychain(testItemName)
assert.Error(t, err, "Expected error when retrieving deleted item")
}
func TestKeychainInvalidItemName(t *testing.T) {
// Skip test if not on macOS
if runtime.GOOS != "darwin" {
t.Skip("Keychain tests only run on macOS")
}
testData := memguard.NewBufferFromBytes([]byte("test"))
defer testData.Destroy()
// Test invalid item names
invalidNames := []string{
"", // Empty name
"test space", // Contains space
"test/slash", // Contains slash
"test\\backslash", // Contains backslash
"test:colon", // Contains colon
"test;semicolon", // Contains semicolon
"test|pipe", // Contains pipe
"test@at", // Contains @
"test#hash", // Contains #
"test$dollar", // Contains $
"test&ampersand", // Contains &
"test*asterisk", // Contains *
"test?question", // Contains ?
"test!exclamation", // Contains !
"test'quote", // Contains single quote
"test\"doublequote", // Contains double quote
"test(paren", // Contains parenthesis
"test[bracket", // Contains bracket
}
for _, name := range invalidNames {
err := storeInKeychain(name, testData)
assert.Error(t, err, "Expected error for invalid name: %s", name)
assert.Contains(t, err.Error(), "invalid keychain item name", "Error should mention invalid name for: %s", name)
}
// Test valid names (should not error on validation)
validNames := []string{
"test-name",
"test_name",
"test.name",
"TestName123",
"TEST_NAME_123",
"com.example.test",
"secret-vault-hostname-2024-01-01",
}
for _, name := range validNames {
err := validateKeychainItemName(name)
assert.NoError(t, err, "Expected no error for valid name: %s", name)
// Clean up
_ = deleteFromKeychain(name)
}
}
func TestKeychainNilData(t *testing.T) {
// Skip test if not on macOS
if runtime.GOOS != "darwin" {
t.Skip("Keychain tests only run on macOS")
}
// Test storing nil data
err := storeInKeychain("test-item", nil)
assert.Error(t, err, "Expected error when storing nil data")
assert.Contains(t, err.Error(), "data buffer is nil")
}
func TestKeychainLargeData(t *testing.T) {
// Skip test if not on macOS
if runtime.GOOS != "darwin" {
t.Skip("Keychain tests only run on macOS")
}
// Test with larger hex-encoded data (512 bytes of binary data = 1KB hex)
largeData := make([]byte, 512)
for i := range largeData {
largeData[i] = byte(i % 256)
}
// Convert to hex string for storage
hexData := hex.EncodeToString(largeData)
testItemName := "test-large-data"
testBuffer := memguard.NewBufferFromBytes([]byte(hexData))
defer testBuffer.Destroy()
// Clean up first
_ = deleteFromKeychain(testItemName)
// Store hex data
err := storeInKeychain(testItemName, testBuffer)
require.NoError(t, err, "Failed to store large data")
// Retrieve and verify
retrievedData, err := retrieveFromKeychain(testItemName)
require.NoError(t, err, "Failed to retrieve large data")
// Decode hex and compare
decodedData, err := hex.DecodeString(string(retrievedData))
require.NoError(t, err, "Failed to decode hex data")
assert.Equal(t, largeData, decodedData, "Large data mismatch")
// Clean up
_ = deleteFromKeychain(testItemName)
}

View File

@@ -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)
} }

View File

@@ -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())

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -62,35 +62,8 @@ func NewSecret(vault VaultInterface, name string) *Secret {
} }
} }
// Save is deprecated - use vault.AddSecret directly which creates versions
// Kept for backward compatibility
func (s *Secret) Save(value []byte, force bool) error {
DebugWith("Saving secret (deprecated method)",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
slog.Int("value_length", len(value)),
slog.Bool("force", force),
)
// Create a secure buffer for the value - note that the caller
// should ideally pass a LockedBuffer directly to vault.AddSecret
valueBuffer := memguard.NewBufferFromBytes(value)
defer valueBuffer.Destroy()
err := s.vault.AddSecret(s.Name, valueBuffer, force)
if err != nil {
Debug("Failed to save secret", "error", err, "secret_name", s.Name)
return err
}
Debug("Successfully saved secret", "secret_name", s.Name)
return nil
}
// GetValue retrieves and decrypts the current version's value using the provided unlocker // 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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)
} }

View File

@@ -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)