'unlock keys' renamed to 'unlockers'
This commit is contained in:
parent
0bf8e71b52
commit
f59ee4d2d6
74
README.md
74
README.md
@ -9,12 +9,12 @@ Secret is a modern, secure command-line secret manager that implements a hierarc
|
|||||||
Secret implements a sophisticated three-layer key architecture:
|
Secret implements a sophisticated 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. **Unlock Keys**: 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
|
||||||
3. **Secret-specific Keys**: Per-secret keys that encrypt individual secret values
|
3. **Secret-specific Keys**: Per-secret keys that encrypt individual secret values
|
||||||
|
|
||||||
### Vault System
|
### Vault System
|
||||||
|
|
||||||
Vaults provide logical separation of secrets, each with its own long-term key and unlock key set. This allows for complete isolation between different contexts (work, personal, projects).
|
Vaults provide logical separation of secrets, each with its own long-term key and unlocker set. This allows for complete isolation between different contexts (work, personal, projects).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -97,26 +97,26 @@ Generates and stores a random secret.
|
|||||||
- `--type, -t`: Type of secret (`base58`, `alnum`)
|
- `--type, -t`: Type of secret (`base58`, `alnum`)
|
||||||
- `--force, -f`: Overwrite existing secret
|
- `--force, -f`: Overwrite existing secret
|
||||||
|
|
||||||
### Unlock Key Management
|
### Unlocker Management
|
||||||
|
|
||||||
#### `secret keys list [--json]`
|
#### `secret unlockers list [--json]`
|
||||||
Lists all unlock keys in the current vault with their metadata.
|
Lists all unlockers in the current vault with their metadata.
|
||||||
|
|
||||||
#### `secret keys add <type> [options]`
|
#### `secret unlockers add <type> [options]`
|
||||||
Creates a new unlock key of the specified type:
|
Creates a new unlocker of the specified type:
|
||||||
|
|
||||||
**Types:**
|
**Types:**
|
||||||
- `passphrase`: Traditional passphrase-protected unlock key
|
- `passphrase`: Traditional passphrase-protected unlocker
|
||||||
- `pgp`: Uses an existing GPG key for encryption/decryption
|
- `pgp`: Uses an existing GPG key for encryption/decryption
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
- `--keyid <id>`: GPG key ID (required for PGP type)
|
- `--keyid <id>`: GPG key ID (required for PGP type)
|
||||||
|
|
||||||
#### `secret keys rm <key-id>`
|
#### `secret unlockers rm <unlocker-id>`
|
||||||
Removes an unlock key.
|
Removes an unlocker.
|
||||||
|
|
||||||
#### `secret key select <key-id>`
|
#### `secret unlocker select <unlocker-id>`
|
||||||
Selects an unlock key as the current default for operations.
|
Selects an unlocker as the current default for operations.
|
||||||
|
|
||||||
### Import Operations
|
### Import Operations
|
||||||
|
|
||||||
@ -142,17 +142,17 @@ Decrypts data using an Age key stored as a secret.
|
|||||||
~/.local/share/secret/
|
~/.local/share/secret/
|
||||||
├── vaults.d/
|
├── vaults.d/
|
||||||
│ ├── default/
|
│ ├── default/
|
||||||
│ │ ├── unlock.d/
|
│ │ ├── unlockers.d/
|
||||||
│ │ │ ├── passphrase/ # Passphrase unlock key
|
│ │ │ ├── passphrase/ # Passphrase unlocker
|
||||||
│ │ │ └── pgp/ # PGP unlock key
|
│ │ │ └── pgp/ # PGP unlocker
|
||||||
│ │ ├── secrets.d/
|
│ │ ├── secrets.d/
|
||||||
│ │ │ ├── api%key/ # Secret: api/key
|
│ │ │ ├── api%key/ # Secret: api/key
|
||||||
│ │ │ └── database%password/ # Secret: database/password
|
│ │ │ └── database%password/ # Secret: database/password
|
||||||
│ │ └── current-unlock-key -> ../unlock.d/passphrase
|
│ │ └── current-unlocker -> ../unlockers.d/passphrase
|
||||||
│ └── work/
|
│ └── work/
|
||||||
│ ├── unlock.d/
|
│ ├── unlockers.d/
|
||||||
│ ├── secrets.d/
|
│ ├── secrets.d/
|
||||||
│ └── current-unlock-key
|
│ └── current-unlocker
|
||||||
├── currentvault -> vaults.d/default
|
├── currentvault -> vaults.d/default
|
||||||
└── configuration.json
|
└── configuration.json
|
||||||
```
|
```
|
||||||
@ -162,22 +162,22 @@ Decrypts data using an Age key stored as a secret.
|
|||||||
#### Long-term Keys
|
#### Long-term Keys
|
||||||
- **Source**: Derived from BIP39 mnemonic phrases using hierarchical deterministic (HD) key derivation
|
- **Source**: Derived from BIP39 mnemonic phrases using hierarchical deterministic (HD) key derivation
|
||||||
- **Purpose**: Master keys for each vault, used to encrypt secret-specific keys
|
- **Purpose**: Master keys for each vault, used to encrypt secret-specific keys
|
||||||
- **Storage**: Public key stored as `pub.age`, private key encrypted by unlock keys
|
- **Storage**: Public key stored as `pub.age`, private key encrypted by unlockers
|
||||||
|
|
||||||
#### Unlock Keys
|
#### Unlockers
|
||||||
Unlock keys provide different authentication methods to access the long-term keys:
|
Unlockers provide different authentication methods to access the long-term keys:
|
||||||
|
|
||||||
1. **Passphrase Keys**:
|
1. **Passphrase Unlockers**:
|
||||||
- Encrypted with user-provided passphrase
|
- Encrypted with user-provided passphrase
|
||||||
- Stored as encrypted Age keys
|
- Stored as encrypted Age keys
|
||||||
- Cross-platform compatible
|
- Cross-platform compatible
|
||||||
|
|
||||||
2. **PGP Keys**:
|
2. **PGP Unlockers**:
|
||||||
- Uses existing GPG key infrastructure
|
- Uses existing GPG key infrastructure
|
||||||
- Leverages existing key management workflows
|
- Leverages existing key management workflows
|
||||||
- Strong authentication through GPG
|
- Strong authentication through GPG
|
||||||
|
|
||||||
Each vault maintains its own set of unlock keys and one long-term key. The long-term key is encrypted to each unlock key, allowing any authorized unlock key 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
|
||||||
- Each secret has its own encryption key pair
|
- Each secret has its own encryption key pair
|
||||||
@ -189,7 +189,7 @@ Each vault maintains its own set of unlock keys and one long-term key. The long-
|
|||||||
- `SB_SECRET_STATE_DIR`: Custom state directory location
|
- `SB_SECRET_STATE_DIR`: Custom state directory location
|
||||||
- `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase (avoids interactive prompt)
|
- `SB_SECRET_MNEMONIC`: Pre-set mnemonic phrase (avoids interactive prompt)
|
||||||
- `SB_UNLOCK_PASSPHRASE`: Pre-set unlock passphrase (avoids interactive prompt)
|
- `SB_UNLOCK_PASSPHRASE`: Pre-set unlock passphrase (avoids interactive prompt)
|
||||||
- `SB_GPG_KEY_ID`: GPG key ID for PGP unlock keys
|
- `SB_GPG_KEY_ID`: GPG key ID for PGP unlockers
|
||||||
|
|
||||||
## Security Features
|
## Security Features
|
||||||
|
|
||||||
@ -205,7 +205,7 @@ Each vault maintains its own set of unlock keys and one long-term key. The long-
|
|||||||
|
|
||||||
### Forward Secrecy
|
### Forward Secrecy
|
||||||
- Per-secret encryption keys limit exposure if compromised
|
- Per-secret encryption keys limit exposure if compromised
|
||||||
- Long-term keys protected by multiple unlock key layers
|
- Long-term keys protected by multiple unlocker layers
|
||||||
|
|
||||||
### Hardware Integration
|
### Hardware Integration
|
||||||
- Hardware token support via PGP/GPG integration
|
- Hardware token support via PGP/GPG integration
|
||||||
@ -238,7 +238,7 @@ secret vault create personal
|
|||||||
# Work with work vault
|
# Work with work vault
|
||||||
secret vault select work
|
secret vault select work
|
||||||
echo "work-db-pass" | secret add database/password
|
echo "work-db-pass" | secret add database/password
|
||||||
secret keys add passphrase # Add passphrase authentication
|
secret unlockers add passphrase # Add passphrase authentication
|
||||||
|
|
||||||
# Switch to personal vault
|
# Switch to personal vault
|
||||||
secret vault select personal
|
secret vault select personal
|
||||||
@ -251,14 +251,14 @@ secret vault list
|
|||||||
### Advanced Authentication
|
### Advanced Authentication
|
||||||
```bash
|
```bash
|
||||||
# Add multiple unlock methods
|
# Add multiple unlock methods
|
||||||
secret keys add passphrase # Password-based
|
secret unlockers add passphrase # Password-based
|
||||||
secret keys add pgp --keyid ABCD1234 # GPG key
|
secret unlockers add pgp --keyid ABCD1234 # GPG key
|
||||||
|
|
||||||
# List unlock keys
|
# List unlockers
|
||||||
secret keys list
|
secret unlockers list
|
||||||
|
|
||||||
# Select a specific unlock key
|
# Select a specific unlocker
|
||||||
secret key select <key-id>
|
secret unlocker select <unlocker-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Encryption/Decryption with Age Keys
|
### Encryption/Decryption with Age Keys
|
||||||
@ -299,14 +299,14 @@ secret decrypt encryption/mykey --input document.txt.age --output document.txt
|
|||||||
- Supports hardware-backed authentication where available
|
- Supports hardware-backed authentication where available
|
||||||
|
|
||||||
### Best Practices
|
### Best Practices
|
||||||
1. Use strong, unique passphrases for unlock keys
|
1. Use strong, unique passphrases for unlockers
|
||||||
2. Enable hardware authentication (Keychain, hardware tokens) when available
|
2. Enable hardware authentication (Keychain, hardware tokens) when available
|
||||||
3. Regularly audit unlock keys and remove unused ones
|
3. Regularly audit unlockers and remove unused ones
|
||||||
4. Keep mnemonic phrases securely backed up offline
|
4. Keep mnemonic phrases securely backed up offline
|
||||||
5. Use separate vaults for different security contexts
|
5. Use separate vaults for different security contexts
|
||||||
|
|
||||||
### Limitations
|
### Limitations
|
||||||
- Requires access to unlock keys for secret retrieval
|
- Requires access to unlockers for secret retrieval
|
||||||
- Mnemonic phrases must be securely stored and backed up
|
- Mnemonic phrases must be securely stored and backed up
|
||||||
- Hardware features limited to supported platforms
|
- Hardware features limited to supported platforms
|
||||||
|
|
||||||
@ -328,7 +328,7 @@ go test ./... # Unit tests
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Multiple Authentication Methods**: Supports passphrase-based and PGP-based unlock keys
|
- **Multiple Authentication Methods**: Supports passphrase-based and PGP-based 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
|
||||||
|
13
TODO.md
13
TODO.md
@ -19,7 +19,7 @@ This document outlines the bugs, issues, and improvements that need to be addres
|
|||||||
- [x] **5. Multiple vaults using the same mnemonic will derive the same long-term keys**: Adding additional vaults with the same mnemonic should increment the index value used. The mnemonic should be double sha256 hashed and the hash value stored in the vault metadata along with the index value (starting at zero) and when additional vaults are added with the same mnemonic (as determined by hash) then the index value should be incremented. The README should be updated to document this behavior.
|
- [x] **5. Multiple vaults using the same mnemonic will derive the same long-term keys**: Adding additional vaults with the same mnemonic should increment the index value used. The mnemonic should be double sha256 hashed and the hash value stored in the vault metadata along with the index value (starting at zero) and when additional vaults are added with the same mnemonic (as determined by hash) then the index value should be incremented. The README should be updated to document this behavior.
|
||||||
|
|
||||||
- [x] **6. Directory structure inconsistency**: The README and test script reference different directory structures:
|
- [x] **6. Directory structure inconsistency**: The README and test script reference different directory structures:
|
||||||
- Current code uses `unlock.d/` but documentation shows `unlock-keys.d/`
|
- Current code uses `unlockers.d/` but documentation shows `unlock-keys.d/`
|
||||||
- Secret files use inconsistent naming (`secret.age` vs `value.age`)
|
- Secret files use inconsistent naming (`secret.age` vs `value.age`)
|
||||||
|
|
||||||
- [x] **7. Symlink handling on non-Unix systems**: The symlink resolution in `resolveVaultSymlink()` may fail on Windows or in certain environments.
|
- [x] **7. Symlink handling on non-Unix systems**: The symlink resolution in `resolveVaultSymlink()` may fail on Windows or in certain environments.
|
||||||
@ -140,7 +140,7 @@ This document outlines the bugs, issues, and improvements that need to be addres
|
|||||||
|
|
||||||
### Code Structure
|
### Code Structure
|
||||||
|
|
||||||
- [ ] **51. Consistent interface implementation**: Ensure all unlock key types properly implement the UnlockKey interface.
|
- [ ] **51. Consistent interface implementation**: Ensure all unlocker types properly implement the Unlocker interface.
|
||||||
|
|
||||||
- [ ] **52. Better separation of concerns**: Some functions in CLI do too much and should be split.
|
- [ ] **52. Better separation of concerns**: Some functions in CLI do too much and should be split.
|
||||||
|
|
||||||
@ -193,4 +193,11 @@ This document outlines the bugs, issues, and improvements that need to be addres
|
|||||||
- Trivial (31-50): Ongoing post-1.0
|
- Trivial (31-50): Ongoing post-1.0
|
||||||
- Architecture/Infrastructure (51-64): Ongoing post-1.0
|
- Architecture/Infrastructure (51-64): Ongoing post-1.0
|
||||||
|
|
||||||
Total estimated time to 1.0: 5-7 weeks with focused development effort.
|
Total estimated time to 1.0: 5-7 weeks with focused development effort.
|
||||||
|
|
||||||
|
### Architecture Issues
|
||||||
|
- **Need to refactor unlock key hierarchy**: Current implementation has confusion between the top-level concepts. Fix in progress.
|
||||||
|
- Current code uses `unlockers.d/` but documentation shows `unlock-keys.d/`
|
||||||
|
- Need to settle on consistent naming: "unlock keys" vs "unlockers" throughout the codebase
|
||||||
|
|
||||||
|
- [ ] **51. Consistent interface implementation**: Ensure all unlocker types properly implement the Unlocker interface.
|
@ -74,11 +74,11 @@ func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error
|
|||||||
if os.Getenv(secret.EnvMnemonic) != "" {
|
if os.Getenv(secret.EnvMnemonic) != "" {
|
||||||
secretValue, err = secretObj.GetValue(nil)
|
secretValue, err = secretObj.GetValue(nil)
|
||||||
} else {
|
} else {
|
||||||
unlockKey, unlockErr := vlt.GetCurrentUnlockKey()
|
unlocker, unlockErr := vlt.GetCurrentUnlocker()
|
||||||
if unlockErr != nil {
|
if unlockErr != nil {
|
||||||
return fmt.Errorf("failed to get current unlock key: %w", unlockErr)
|
return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
|
||||||
}
|
}
|
||||||
secretValue, err = secretObj.GetValue(unlockKey)
|
secretValue, 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)
|
||||||
@ -178,11 +178,11 @@ func (cli *CLIInstance) Decrypt(secretName, inputFile, outputFile string) error
|
|||||||
if os.Getenv(secret.EnvMnemonic) != "" {
|
if os.Getenv(secret.EnvMnemonic) != "" {
|
||||||
secretValue, err = secretObj.GetValue(nil)
|
secretValue, err = secretObj.GetValue(nil)
|
||||||
} else {
|
} else {
|
||||||
unlockKey, unlockErr := vlt.GetCurrentUnlockKey()
|
unlocker, unlockErr := vlt.GetCurrentUnlocker()
|
||||||
if unlockErr != nil {
|
if unlockErr != nil {
|
||||||
return fmt.Errorf("failed to get current unlock key: %w", unlockErr)
|
return fmt.Errorf("failed to get current unlocker: %w", unlockErr)
|
||||||
}
|
}
|
||||||
secretValue, err = secretObj.GetValue(unlockKey)
|
secretValue, 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)
|
||||||
|
@ -140,7 +140,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
|||||||
// Unlock the vault with the derived long-term key
|
// Unlock the vault with the derived long-term key
|
||||||
vlt.Unlock(ltIdentity)
|
vlt.Unlock(ltIdentity)
|
||||||
|
|
||||||
// Prompt for passphrase for unlock key
|
// Prompt for passphrase for unlocker
|
||||||
var passphraseStr string
|
var passphraseStr string
|
||||||
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
|
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
|
||||||
secret.Debug("Using unlock passphrase from environment variable")
|
secret.Debug("Using unlock passphrase from environment variable")
|
||||||
@ -148,61 +148,61 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
|
|||||||
} else {
|
} else {
|
||||||
secret.Debug("Prompting user for unlock passphrase")
|
secret.Debug("Prompting user for unlock passphrase")
|
||||||
// Use secure passphrase input with confirmation
|
// Use secure passphrase input with confirmation
|
||||||
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
|
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to read unlock passphrase", "error", err)
|
secret.Debug("Failed to read unlock passphrase", "error", err)
|
||||||
return fmt.Errorf("failed to read passphrase: %w", err)
|
return fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create passphrase-protected unlock key
|
// Create passphrase-protected unlocker
|
||||||
secret.Debug("Creating passphrase-protected unlock key")
|
secret.Debug("Creating passphrase-protected unlocker")
|
||||||
passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr)
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to create unlock key", "error", err)
|
secret.Debug("Failed to create unlocker", "error", err)
|
||||||
return fmt.Errorf("failed to create unlock key: %w", err)
|
return fmt.Errorf("failed to create unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt long-term private key to the unlock key
|
// Encrypt long-term private key to the unlocker
|
||||||
unlockKeyDir := passphraseKey.GetDirectory()
|
unlockerDir := passphraseUnlocker.GetDirectory()
|
||||||
|
|
||||||
// Read unlock key public key
|
// Read unlocker public key
|
||||||
unlockPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockKeyDir, "pub.age"))
|
unlockerPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockerDir, "pub.age"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read unlock key public key: %w", err)
|
return fmt.Errorf("failed to read unlocker public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unlockRecipient, err := age.ParseX25519Recipient(string(unlockPubKeyData))
|
unlockerRecipient, err := age.ParseX25519Recipient(string(unlockerPubKeyData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse unlock key public key: %w", err)
|
return fmt.Errorf("failed to parse unlocker public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt long-term private key to unlock key
|
// Encrypt long-term private key to unlocker
|
||||||
ltPrivKeyData := []byte(ltIdentity.String())
|
ltPrivKeyData := []byte(ltIdentity.String())
|
||||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, unlockRecipient)
|
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, unlockerRecipient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
return fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write encrypted long-term private key
|
// Write encrypted long-term private key
|
||||||
if err := afero.WriteFile(cli.fs, filepath.Join(unlockKeyDir, "longterm.age"), encryptedLtPrivKey, secret.FilePerms); err != nil {
|
if err := afero.WriteFile(cli.fs, filepath.Join(unlockerDir, "longterm.age"), encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||||
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
return fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
cmd.Printf("\nDefault vault created and configured\n")
|
cmd.Printf("\nDefault vault created and configured\n")
|
||||||
cmd.Printf("Long-term public key: %s\n", ltPubKey)
|
cmd.Printf("Long-term public key: %s\n", ltPubKey)
|
||||||
cmd.Printf("Unlock key ID: %s\n", passphraseKey.GetID())
|
cmd.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
||||||
cmd.Println("\nYour secret manager is ready to use!")
|
cmd.Println("\nYour secret manager is ready to use!")
|
||||||
cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,")
|
cmd.Println("Note: When using SB_SECRET_MNEMONIC environment variable,")
|
||||||
cmd.Println("unlock keys are not required for secret operations.")
|
cmd.Println("unlockers are not required for secret operations.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// readSecurePassphrase reads a passphrase securely from the terminal without echoing
|
// readSecurePassphrase reads a passphrase securely from the terminal without echoing
|
||||||
// This version adds confirmation (read twice) for creating new unlock keys
|
// This version adds confirmation (read twice) for creating new unlockers
|
||||||
func readSecurePassphrase(prompt string) (string, error) {
|
func readSecurePassphrase(prompt string) (string, error) {
|
||||||
// Get the first passphrase
|
// Get the first passphrase
|
||||||
passphrase1, err := secret.ReadPassphrase(prompt)
|
passphrase1, err := secret.ReadPassphrase(prompt)
|
||||||
|
@ -35,8 +35,8 @@ func newRootCmd() *cobra.Command {
|
|||||||
cmd.AddCommand(newAddCmd())
|
cmd.AddCommand(newAddCmd())
|
||||||
cmd.AddCommand(newGetCmd())
|
cmd.AddCommand(newGetCmd())
|
||||||
cmd.AddCommand(newListCmd())
|
cmd.AddCommand(newListCmd())
|
||||||
cmd.AddCommand(newKeysCmd())
|
cmd.AddCommand(newUnlockersCmd())
|
||||||
cmd.AddCommand(newKeyCmd())
|
cmd.AddCommand(newUnlockerCmd())
|
||||||
cmd.AddCommand(newImportCmd())
|
cmd.AddCommand(newImportCmd())
|
||||||
cmd.AddCommand(newEncryptCmd())
|
cmd.AddCommand(newEncryptCmd())
|
||||||
cmd.AddCommand(newDecryptCmd())
|
cmd.AddCommand(newDecryptCmd())
|
||||||
|
@ -18,29 +18,29 @@ import (
|
|||||||
|
|
||||||
// ... existing imports ...
|
// ... existing imports ...
|
||||||
|
|
||||||
func newKeysCmd() *cobra.Command {
|
func newUnlockersCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "keys",
|
Use: "unlockers",
|
||||||
Short: "Manage unlock keys",
|
Short: "Manage unlockers",
|
||||||
Long: `Create, list, and remove unlock keys for the current vault.`,
|
Long: `Create, list, and remove unlockers for the current vault.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(newKeysListCmd())
|
cmd.AddCommand(newUnlockersListCmd())
|
||||||
cmd.AddCommand(newKeysAddCmd())
|
cmd.AddCommand(newUnlockersAddCmd())
|
||||||
cmd.AddCommand(newKeysRmCmd())
|
cmd.AddCommand(newUnlockersRmCmd())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeysListCmd() *cobra.Command {
|
func newUnlockersListCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List unlock keys in the current vault",
|
Short: "List unlockers in the current vault",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
jsonOutput, _ := cmd.Flags().GetBool("json")
|
jsonOutput, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.KeysList(jsonOutput)
|
return cli.UnlockersList(jsonOutput)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,60 +48,60 @@ func newKeysListCmd() *cobra.Command {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeysAddCmd() *cobra.Command {
|
func newUnlockersAddCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "add <type>",
|
Use: "add <type>",
|
||||||
Short: "Add a new unlock key",
|
Short: "Add a new unlocker",
|
||||||
Long: `Add a new unlock key of the specified type (passphrase, keychain, pgp).`,
|
Long: `Add a new unlocker of the specified type (passphrase, keychain, pgp).`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.KeysAdd(args[0], cmd)
|
return cli.UnlockersAdd(args[0], cmd)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Flags().String("keyid", "", "GPG key ID for PGP unlock keys")
|
cmd.Flags().String("keyid", "", "GPG key ID for PGP unlockers")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeysRmCmd() *cobra.Command {
|
func newUnlockersRmCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "rm <key-id>",
|
Use: "rm <unlocker-id>",
|
||||||
Short: "Remove an unlock key",
|
Short: "Remove an unlocker",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.KeysRemove(args[0])
|
return cli.UnlockersRemove(args[0])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeyCmd() *cobra.Command {
|
func newUnlockerCmd() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "key",
|
Use: "unlocker",
|
||||||
Short: "Manage current unlock key",
|
Short: "Manage current unlocker",
|
||||||
Long: `Select the current unlock key for operations.`,
|
Long: `Select the current unlocker for operations.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(newKeySelectSubCmd())
|
cmd.AddCommand(newUnlockerSelectSubCmd())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func newKeySelectSubCmd() *cobra.Command {
|
func newUnlockerSelectSubCmd() *cobra.Command {
|
||||||
return &cobra.Command{
|
return &cobra.Command{
|
||||||
Use: "select <key-id>",
|
Use: "select <unlocker-id>",
|
||||||
Short: "Select an unlock key as current",
|
Short: "Select an unlocker as current",
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
cli := NewCLIInstance()
|
cli := NewCLIInstance()
|
||||||
return cli.KeySelect(args[0])
|
return cli.UnlockerSelect(args[0])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeysList lists unlock keys in the current vault
|
// UnlockersList lists unlockers in the current vault
|
||||||
func (cli *CLIInstance) KeysList(jsonOutput bool) error {
|
func (cli *CLIInstance) UnlockersList(jsonOutput bool) error {
|
||||||
// Get current vault
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -109,90 +109,90 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get the metadata first
|
// Get the metadata first
|
||||||
keyMetadataList, err := vlt.ListUnlockKeys()
|
unlockerMetadataList, err := vlt.ListUnlockers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load actual unlock key objects to get the proper IDs
|
// Load actual unlocker objects to get the proper IDs
|
||||||
type KeyInfo struct {
|
type UnlockerInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
Flags []string `json:"flags,omitempty"`
|
Flags []string `json:"flags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys []KeyInfo
|
var unlockers []UnlockerInfo
|
||||||
for _, metadata := range keyMetadataList {
|
for _, metadata := range unlockerMetadataList {
|
||||||
// Create unlock key instance to get the proper ID
|
// Create unlocker instance to get the proper ID
|
||||||
vaultDir, err := vlt.GetDirectory()
|
vaultDir, err := vlt.GetDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the key directory by type and created time
|
// Find the unlocker directory by type and created time
|
||||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||||
files, err := afero.ReadDir(cli.fs, unlockKeysDir)
|
files, err := afero.ReadDir(cli.fs, unlockersDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var unlockKey secret.UnlockKey
|
var unlocker secret.Unlocker
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
if !file.IsDir() {
|
if !file.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
keyDir := filepath.Join(unlockKeysDir, file.Name())
|
unlockerDir := filepath.Join(unlockersDir, file.Name())
|
||||||
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
|
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
||||||
|
|
||||||
// Check if this is the right key by comparing metadata
|
// Check if this is the right unlocker by comparing metadata
|
||||||
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
|
metadataBytes, err := afero.ReadFile(cli.fs, metadataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var diskMetadata secret.UnlockKeyMetadata
|
var diskMetadata secret.UnlockerMetadata
|
||||||
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
|
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match by type and creation time
|
// Match by type and creation time
|
||||||
if diskMetadata.Type == metadata.Type && diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
if diskMetadata.Type == metadata.Type && diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
|
||||||
// Create the appropriate unlock key instance
|
// Create the appropriate unlocker instance
|
||||||
switch metadata.Type {
|
switch metadata.Type {
|
||||||
case "passphrase":
|
case "passphrase":
|
||||||
unlockKey = secret.NewPassphraseUnlockKey(cli.fs, keyDir, diskMetadata)
|
unlocker = secret.NewPassphraseUnlocker(cli.fs, unlockerDir, diskMetadata)
|
||||||
case "keychain":
|
case "keychain":
|
||||||
unlockKey = secret.NewKeychainUnlockKey(cli.fs, keyDir, diskMetadata)
|
unlocker = secret.NewKeychainUnlocker(cli.fs, unlockerDir, diskMetadata)
|
||||||
case "pgp":
|
case "pgp":
|
||||||
unlockKey = secret.NewPGPUnlockKey(cli.fs, keyDir, diskMetadata)
|
unlocker = secret.NewPGPUnlocker(cli.fs, unlockerDir, diskMetadata)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the proper ID using the unlock key's ID() method
|
// Get the proper ID using the unlocker's ID() method
|
||||||
var properID string
|
var properID string
|
||||||
if unlockKey != nil {
|
if unlocker != nil {
|
||||||
properID = unlockKey.GetID()
|
properID = unlocker.GetID()
|
||||||
} else {
|
} else {
|
||||||
properID = metadata.ID // fallback to metadata ID
|
properID = metadata.ID // fallback to metadata ID
|
||||||
}
|
}
|
||||||
|
|
||||||
keyInfo := KeyInfo{
|
unlockerInfo := UnlockerInfo{
|
||||||
ID: properID,
|
ID: properID,
|
||||||
Type: metadata.Type,
|
Type: metadata.Type,
|
||||||
CreatedAt: metadata.CreatedAt,
|
CreatedAt: metadata.CreatedAt,
|
||||||
Flags: metadata.Flags,
|
Flags: metadata.Flags,
|
||||||
}
|
}
|
||||||
keys = append(keys, keyInfo)
|
unlockers = append(unlockers, unlockerInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
// JSON output
|
// JSON output
|
||||||
output := map[string]interface{}{
|
output := map[string]interface{}{
|
||||||
"keys": keys,
|
"unlockers": unlockers,
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonBytes, err := json.MarshalIndent(output, "", " ")
|
jsonBytes, err := json.MarshalIndent(output, "", " ")
|
||||||
@ -203,36 +203,36 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
|
|||||||
fmt.Println(string(jsonBytes))
|
fmt.Println(string(jsonBytes))
|
||||||
} else {
|
} else {
|
||||||
// Pretty table output
|
// Pretty table output
|
||||||
if len(keys) == 0 {
|
if len(unlockers) == 0 {
|
||||||
fmt.Println("No unlock keys found in current vault.")
|
fmt.Println("No unlockers found in current vault.")
|
||||||
fmt.Println("Run 'secret keys add passphrase' to create one.")
|
fmt.Println("Run 'secret unlockers add passphrase' to create one.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%-18s %-12s %-20s %s\n", "KEY ID", "TYPE", "CREATED", "FLAGS")
|
fmt.Printf("%-18s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
|
||||||
fmt.Printf("%-18s %-12s %-20s %s\n", "------", "----", "-------", "-----")
|
fmt.Printf("%-18s %-12s %-20s %s\n", "-----------", "----", "-------", "-----")
|
||||||
|
|
||||||
for _, key := range keys {
|
for _, unlocker := range unlockers {
|
||||||
flags := ""
|
flags := ""
|
||||||
if len(key.Flags) > 0 {
|
if len(unlocker.Flags) > 0 {
|
||||||
flags = strings.Join(key.Flags, ",")
|
flags = strings.Join(unlocker.Flags, ",")
|
||||||
}
|
}
|
||||||
fmt.Printf("%-18s %-12s %-20s %s\n",
|
fmt.Printf("%-18s %-12s %-20s %s\n",
|
||||||
key.ID,
|
unlocker.ID,
|
||||||
key.Type,
|
unlocker.Type,
|
||||||
key.CreatedAt.Format("2006-01-02 15:04:05"),
|
unlocker.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||||
flags)
|
flags)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("\nTotal: %d unlock key(s)\n", len(keys))
|
fmt.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeysAdd adds a new unlock key
|
// UnlockersAdd adds a new unlocker
|
||||||
func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
func (cli *CLIInstance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error {
|
||||||
switch keyType {
|
switch unlockerType {
|
||||||
case "passphrase":
|
case "passphrase":
|
||||||
// Get current vault
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
@ -254,28 +254,28 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
|||||||
passphraseStr = envPassphrase
|
passphraseStr = envPassphrase
|
||||||
} else {
|
} else {
|
||||||
// Use secure passphrase input with confirmation
|
// Use secure passphrase input with confirmation
|
||||||
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
|
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read passphrase: %w", err)
|
return fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr)
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Created passphrase unlock key: %s\n", passphraseKey.GetID())
|
cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID())
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case "keychain":
|
case "keychain":
|
||||||
keychainKey, err := secret.CreateKeychainUnlockKey(cli.fs, cli.stateDir)
|
keychainUnlocker, err := secret.CreateKeychainUnlocker(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create macOS Keychain unlock key: %w", err)
|
return fmt.Errorf("failed to create macOS Keychain unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Created macOS Keychain unlock key: %s\n", keychainKey.GetID())
|
cmd.Printf("Created macOS Keychain unlocker: %s\n", keychainUnlocker.GetID())
|
||||||
if keyName, err := keychainKey.GetKeychainItemName(); err == nil {
|
if keyName, err := keychainUnlocker.GetKeychainItemName(); err == nil {
|
||||||
cmd.Printf("Keychain Item Name: %s\n", keyName)
|
cmd.Printf("Keychain Item Name: %s\n", keyName)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -291,38 +291,38 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
|
|||||||
return fmt.Errorf("GPG key ID required: use --keyid flag or set SB_GPG_KEY_ID environment variable")
|
return fmt.Errorf("GPG key ID required: use --keyid flag or set SB_GPG_KEY_ID environment variable")
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpKey, err := secret.CreatePGPUnlockKey(cli.fs, cli.stateDir, gpgKeyID)
|
pgpUnlocker, err := secret.CreatePGPUnlocker(cli.fs, cli.stateDir, gpgKeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Created PGP unlock key: %s\n", pgpKey.GetID())
|
cmd.Printf("Created PGP unlocker: %s\n", pgpUnlocker.GetID())
|
||||||
cmd.Printf("GPG Key ID: %s\n", gpgKeyID)
|
cmd.Printf("GPG Key ID: %s\n", gpgKeyID)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported key type: %s (supported: passphrase, keychain, pgp)", keyType)
|
return fmt.Errorf("unsupported unlocker type: %s (supported: passphrase, keychain, pgp)", unlockerType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeysRemove removes an unlock key
|
// UnlockersRemove removes an unlocker
|
||||||
func (cli *CLIInstance) KeysRemove(keyID string) error {
|
func (cli *CLIInstance) UnlockersRemove(unlockerID string) error {
|
||||||
// Get current vault
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return vlt.RemoveUnlockKey(keyID)
|
return vlt.RemoveUnlocker(unlockerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeySelect selects an unlock key as current
|
// UnlockerSelect selects an unlocker as current
|
||||||
func (cli *CLIInstance) KeySelect(keyID string) error {
|
func (cli *CLIInstance) UnlockerSelect(unlockerID string) error {
|
||||||
// Get current vault
|
// Get current vault
|
||||||
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return vlt.SelectUnlockKey(keyID)
|
return vlt.SelectUnlocker(unlockerID)
|
||||||
}
|
}
|
@ -251,17 +251,17 @@ func (cli *CLIInstance) VaultImport(vaultName string) error {
|
|||||||
// Unlock the vault with the derived long-term key
|
// Unlock the vault with the derived long-term key
|
||||||
vlt.Unlock(ltIdentity)
|
vlt.Unlock(ltIdentity)
|
||||||
|
|
||||||
// Create passphrase-protected unlock key
|
// Create passphrase-protected unlocker
|
||||||
secret.Debug("Creating passphrase-protected unlock key")
|
secret.Debug("Creating passphrase-protected unlocker")
|
||||||
passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr)
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to create unlock key", "error", err)
|
secret.Debug("Failed to create unlocker", "error", err)
|
||||||
return fmt.Errorf("failed to create unlock key: %w", err)
|
return fmt.Errorf("failed to create unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
|
||||||
fmt.Printf("Long-term public key: %s\n", ltPublicKey)
|
fmt.Printf("Long-term public key: %s\n", ltPublicKey)
|
||||||
fmt.Printf("Unlock key ID: %s\n", passphraseKey.GetID())
|
fmt.Printf("Unlocker ID: %s\n", passphraseUnlocker.GetID())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -15,19 +15,19 @@ import (
|
|||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeychainUnlockKeyMetadata extends UnlockKeyMetadata with keychain-specific data
|
// KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data
|
||||||
type KeychainUnlockKeyMetadata struct {
|
type KeychainUnlockerMetadata struct {
|
||||||
UnlockKeyMetadata
|
UnlockerMetadata
|
||||||
// Age keypair information
|
// Age keypair information
|
||||||
AgePublicKey string `json:"age_public_key"`
|
AgePublicKey string `json:"age_public_key"`
|
||||||
// Keychain item name
|
// Keychain item name
|
||||||
KeychainItemName string `json:"keychain_item_name"`
|
KeychainItemName string `json:"keychain_item_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeychainUnlockKey represents a macOS Keychain-protected unlock key
|
// KeychainUnlocker represents a macOS Keychain-protected unlocker
|
||||||
type KeychainUnlockKey struct {
|
type KeychainUnlocker struct {
|
||||||
Directory string
|
Directory string
|
||||||
Metadata UnlockKeyMetadata
|
Metadata UnlockerMetadata
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,17 +38,17 @@ type KeychainData struct {
|
|||||||
EncryptedLongtermKey string `json:"encrypted_longterm_key"`
|
EncryptedLongtermKey string `json:"encrypted_longterm_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIdentity implements UnlockKey interface for Keychain-based unlock keys
|
// GetIdentity implements Unlocker interface for Keychain-based unlockers
|
||||||
func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||||
DebugWith("Getting keychain unlock key identity",
|
DebugWith("Getting keychain unlocker identity",
|
||||||
slog.String("key_id", k.GetID()),
|
slog.String("unlocker_id", k.GetID()),
|
||||||
slog.String("key_type", k.GetType()),
|
slog.String("unlocker_type", k.GetType()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 1: Get keychain item name
|
// Step 1: Get keychain item name
|
||||||
keychainItemName, err := k.GetKeychainItemName()
|
keychainItemName, err := k.GetKeychainItemName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to get keychain item name", "error", err, "key_id", k.GetID())
|
Debug("Failed to get keychain item name", "error", err, "unlocker_id", k.GetID())
|
||||||
return nil, fmt.Errorf("failed to get keychain item name: %w", err)
|
return nil, fmt.Errorf("failed to get keychain item name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,18 +61,18 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Retrieved data from keychain",
|
DebugWith("Retrieved data from keychain",
|
||||||
slog.String("key_id", k.GetID()),
|
slog.String("unlocker_id", k.GetID()),
|
||||||
slog.Int("data_length", len(keychainDataBytes)),
|
slog.Int("data_length", len(keychainDataBytes)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 3: Parse keychain data
|
// Step 3: Parse keychain data
|
||||||
var keychainData KeychainData
|
var keychainData KeychainData
|
||||||
if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil {
|
if err := json.Unmarshal(keychainDataBytes, &keychainData); err != nil {
|
||||||
Debug("Failed to parse keychain data", "error", err, "key_id", k.GetID())
|
Debug("Failed to parse keychain data", "error", err, "unlocker_id", k.GetID())
|
||||||
return nil, fmt.Errorf("failed to parse keychain data: %w", err)
|
return nil, fmt.Errorf("failed to parse keychain data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug("Parsed keychain data successfully", "key_id", k.GetID())
|
Debug("Parsed keychain data successfully", "unlocker_id", k.GetID())
|
||||||
|
|
||||||
// Step 4: Read the encrypted age private key from filesystem
|
// Step 4: Read the encrypted age private key from filesystem
|
||||||
agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
|
agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
|
||||||
@ -85,61 +85,61 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Read encrypted age private key",
|
DebugWith("Read encrypted age private key",
|
||||||
slog.String("key_id", k.GetID()),
|
slog.String("unlocker_id", k.GetID()),
|
||||||
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 5: Decrypt the age private key using the passphrase from keychain
|
// Step 5: Decrypt the age private key using the passphrase from keychain
|
||||||
Debug("Decrypting age private key with keychain passphrase", "key_id", k.GetID())
|
Debug("Decrypting age private key with keychain passphrase", "unlocker_id", k.GetID())
|
||||||
agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
|
agePrivKeyData, err := DecryptWithPassphrase(encryptedAgePrivKeyData, keychainData.AgePrivKeyPassphrase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to decrypt age private key with keychain passphrase", "error", err, "key_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)
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Successfully decrypted age private key with keychain passphrase",
|
DebugWith("Successfully decrypted age private key with keychain passphrase",
|
||||||
slog.String("key_id", k.GetID()),
|
slog.String("unlocker_id", k.GetID()),
|
||||||
slog.Int("decrypted_length", len(agePrivKeyData)),
|
slog.Int("decrypted_length", len(agePrivKeyData)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 6: Parse the decrypted age private key
|
// Step 6: Parse the decrypted age private key
|
||||||
Debug("Parsing decrypted age private key", "key_id", k.GetID())
|
Debug("Parsing decrypted age private key", "unlocker_id", k.GetID())
|
||||||
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
|
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to parse age private key", "error", err, "key_id", k.GetID())
|
Debug("Failed to parse age private key", "error", err, "unlocker_id", k.GetID())
|
||||||
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Successfully parsed keychain age identity",
|
DebugWith("Successfully parsed keychain age identity",
|
||||||
slog.String("key_id", k.GetID()),
|
slog.String("unlocker_id", k.GetID()),
|
||||||
slog.String("public_key", ageIdentity.Recipient().String()),
|
slog.String("public_key", ageIdentity.Recipient().String()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return ageIdentity, nil
|
return ageIdentity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType implements UnlockKey interface
|
// GetType implements Unlocker interface
|
||||||
func (k *KeychainUnlockKey) GetType() string {
|
func (k *KeychainUnlocker) GetType() string {
|
||||||
return "keychain"
|
return "keychain"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMetadata implements UnlockKey interface
|
// GetMetadata implements Unlocker interface
|
||||||
func (k *KeychainUnlockKey) GetMetadata() UnlockKeyMetadata {
|
func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata {
|
||||||
return k.Metadata
|
return k.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDirectory implements UnlockKey interface
|
// GetDirectory implements Unlocker interface
|
||||||
func (k *KeychainUnlockKey) GetDirectory() string {
|
func (k *KeychainUnlocker) GetDirectory() string {
|
||||||
return k.Directory
|
return k.Directory
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID implements UnlockKey interface
|
// GetID implements Unlocker interface
|
||||||
func (k *KeychainUnlockKey) GetID() string {
|
func (k *KeychainUnlocker) GetID() string {
|
||||||
return k.Metadata.ID
|
return k.Metadata.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID implements UnlockKey interface - generates ID from keychain item name
|
// ID implements Unlocker interface - generates ID from keychain item name
|
||||||
func (k *KeychainUnlockKey) ID() string {
|
func (k *KeychainUnlocker) ID() string {
|
||||||
// Generate ID using keychain item name
|
// Generate ID using keychain item name
|
||||||
keychainItemName, err := k.GetKeychainItemName()
|
keychainItemName, err := k.GetKeychainItemName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -149,12 +149,12 @@ func (k *KeychainUnlockKey) ID() string {
|
|||||||
return fmt.Sprintf("%s-keychain", keychainItemName)
|
return fmt.Sprintf("%s-keychain", keychainItemName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove implements UnlockKey interface - removes the keychain unlock key
|
// Remove implements Unlocker interface - removes the keychain unlocker
|
||||||
func (k *KeychainUnlockKey) Remove() error {
|
func (k *KeychainUnlocker) Remove() error {
|
||||||
// Step 1: Get keychain item name
|
// Step 1: Get keychain item name
|
||||||
keychainItemName, err := k.GetKeychainItemName()
|
keychainItemName, err := k.GetKeychainItemName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to get keychain item name during removal", "error", err, "key_id", k.GetID())
|
Debug("Failed to get keychain item name during removal", "error", err, "unlocker_id", k.GetID())
|
||||||
return fmt.Errorf("failed to get keychain item name: %w", err)
|
return fmt.Errorf("failed to get keychain item name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,19 +166,19 @@ func (k *KeychainUnlockKey) Remove() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Remove directory
|
// Step 3: Remove directory
|
||||||
Debug("Removing keychain unlock key directory", "directory", k.Directory)
|
Debug("Removing keychain unlocker directory", "directory", k.Directory)
|
||||||
if err := k.fs.RemoveAll(k.Directory); err != nil {
|
if err := k.fs.RemoveAll(k.Directory); err != nil {
|
||||||
Debug("Failed to remove keychain unlock key directory", "error", err, "directory", k.Directory)
|
Debug("Failed to remove keychain unlocker directory", "error", err, "directory", k.Directory)
|
||||||
return fmt.Errorf("failed to remove keychain unlock key directory: %w", err)
|
return fmt.Errorf("failed to remove keychain unlocker directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug("Successfully removed keychain unlock key", "key_id", k.GetID(), "keychain_item", keychainItemName)
|
Debug("Successfully removed keychain unlocker", "unlocker_id", k.GetID(), "keychain_item", keychainItemName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKeychainUnlockKey creates a new KeychainUnlockKey instance
|
// NewKeychainUnlocker creates a new KeychainUnlocker instance
|
||||||
func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *KeychainUnlockKey {
|
func NewKeychainUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *KeychainUnlocker {
|
||||||
return &KeychainUnlockKey{
|
return &KeychainUnlocker{
|
||||||
Directory: directory,
|
Directory: directory,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
@ -186,7 +186,7 @@ func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetad
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetKeychainItemName returns the keychain item name from metadata
|
// GetKeychainItemName returns the keychain item name from metadata
|
||||||
func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
|
func (k *KeychainUnlocker) GetKeychainItemName() (string, error) {
|
||||||
// Load the metadata
|
// Load the metadata
|
||||||
metadataPath := filepath.Join(k.Directory, "unlock-metadata.json")
|
metadataPath := filepath.Join(k.Directory, "unlock-metadata.json")
|
||||||
metadataData, err := afero.ReadFile(k.fs, metadataPath)
|
metadataData, err := afero.ReadFile(k.fs, metadataPath)
|
||||||
@ -194,7 +194,7 @@ func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
|
|||||||
return "", fmt.Errorf("failed to read keychain metadata: %w", err)
|
return "", fmt.Errorf("failed to read keychain metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keychainMetadata KeychainUnlockKeyMetadata
|
var keychainMetadata KeychainUnlockerMetadata
|
||||||
if err := json.Unmarshal(metadataData, &keychainMetadata); err != nil {
|
if err := json.Unmarshal(metadataData, &keychainMetadata); err != nil {
|
||||||
return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
|
return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
|
||||||
}
|
}
|
||||||
@ -202,8 +202,8 @@ func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
|
|||||||
return keychainMetadata.KeychainItemName, nil
|
return keychainMetadata.KeychainItemName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateKeychainUnlockKeyName generates a unique name for the keychain unlock key
|
// generateKeychainUnlockerName generates a unique name for the keychain unlocker
|
||||||
func generateKeychainUnlockKeyName(vaultName string) (string, error) {
|
func generateKeychainUnlockerName(vaultName string) (string, error) {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get hostname: %w", err)
|
return "", fmt.Errorf("failed to get hostname: %w", err)
|
||||||
@ -214,8 +214,8 @@ func generateKeychainUnlockKeyName(vaultName string) (string, error) {
|
|||||||
return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentDate), nil
|
return fmt.Sprintf("secret-%s-%s-%s", vaultName, hostname, enrollmentDate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateKeychainUnlockKey creates a new keychain unlock key and stores it in the vault
|
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
|
||||||
func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, error) {
|
func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, error) {
|
||||||
// Check if we're on macOS
|
// Check if we're on macOS
|
||||||
if err := checkMacOSAvailable(); err != nil {
|
if err := checkMacOSAvailable(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -228,23 +228,23 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate the keychain item name
|
// Generate the keychain item name
|
||||||
keychainItemName, err := generateKeychainUnlockKeyName(vault.GetName())
|
keychainItemName, err := generateKeychainUnlockerName(vault.GetName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate keychain item name: %w", err)
|
return nil, fmt.Errorf("failed to generate keychain item name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unlock key directory using the keychain item name as the directory name
|
// Create unlocker directory using the keychain item name as the directory name
|
||||||
vaultDir, err := vault.GetDirectory()
|
vaultDir, err := vault.GetDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", keychainItemName)
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", keychainItemName)
|
||||||
if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
|
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Generate a new age keypair for the keychain unlock key
|
// Step 1: Generate a new age keypair for the keychain unlocker
|
||||||
ageIdentity, err := age.GenerateX25519Identity()
|
ageIdentity, err := age.GenerateX25519Identity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
||||||
@ -258,7 +258,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
|
|
||||||
// Step 3: Store age public key as plaintext
|
// Step 3: Store age public key as plaintext
|
||||||
agePublicKeyString := ageIdentity.Recipient().String()
|
agePublicKeyString := ageIdentity.Recipient().String()
|
||||||
agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
agePubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
|
if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write age public key: %w", err)
|
return nil, fmt.Errorf("failed to write age public key: %w", err)
|
||||||
}
|
}
|
||||||
@ -270,7 +270,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age")
|
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||||
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
|
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
|
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
|
||||||
}
|
}
|
||||||
@ -287,61 +287,61 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
}
|
}
|
||||||
ltPrivKeyData = []byte(ltIdentity.String())
|
ltPrivKeyData = []byte(ltIdentity.String())
|
||||||
} else {
|
} else {
|
||||||
// Get the vault to access current unlock key
|
// Get the vault to access current unlocker
|
||||||
currentUnlockKey, err := vault.GetCurrentUnlockKey()
|
currentUnlocker, err := vault.GetCurrentUnlocker()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get current unlock key: %w", err)
|
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current unlock key identity
|
// Get the current unlocker identity
|
||||||
currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
|
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
|
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get encrypted long-term key from current unlock key, handling different types
|
// Get encrypted long-term key from current unlocker, handling different types
|
||||||
var encryptedLtPrivKey []byte
|
var encryptedLtPrivKey []byte
|
||||||
switch currentUnlockKey := currentUnlockKey.(type) {
|
switch currentUnlocker := currentUnlocker.(type) {
|
||||||
case *PassphraseUnlockKey:
|
case *PassphraseUnlocker:
|
||||||
// Read the encrypted long-term private key from passphrase unlock key
|
// Read the encrypted long-term private key from passphrase unlocker
|
||||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
|
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case *PGPUnlockKey:
|
case *PGPUnlocker:
|
||||||
// Read the encrypted long-term private key from PGP unlock key
|
// Read the encrypted long-term private key from PGP unlocker
|
||||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
|
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case *KeychainUnlockKey:
|
case *KeychainUnlocker:
|
||||||
// Read the encrypted long-term private key from another keychain unlock key
|
// Read the encrypted long-term private key from another keychain unlocker
|
||||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlock key: %w", err)
|
return nil, fmt.Errorf("failed to read encrypted long-term key from current keychain unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported current unlock key type for keychain unlock key creation")
|
return nil, fmt.Errorf("unsupported current unlocker type for keychain unlocker creation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt long-term private key using current unlock key
|
// Decrypt long-term private key using current unlocker
|
||||||
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
|
ltPrivKeyData, 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Encrypt long-term private key to the new age unlock key
|
// Step 6: Encrypt long-term private key to the new age unlocker
|
||||||
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
|
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write encrypted long-term private key
|
// Write encrypted long-term private key
|
||||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||||
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
|
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
@ -367,8 +367,8 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
// Generate the key ID directly using the keychain item name
|
// Generate the key ID directly using the keychain item name
|
||||||
keyID := fmt.Sprintf("%s-keychain", keychainItemName)
|
keyID := fmt.Sprintf("%s-keychain", keychainItemName)
|
||||||
|
|
||||||
keychainMetadata := KeychainUnlockKeyMetadata{
|
keychainMetadata := KeychainUnlockerMetadata{
|
||||||
UnlockKeyMetadata: UnlockKeyMetadata{
|
UnlockerMetadata: UnlockerMetadata{
|
||||||
ID: keyID,
|
ID: keyID,
|
||||||
Type: "keychain",
|
Type: "keychain",
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
@ -380,16 +380,16 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
|
|
||||||
metadataBytes, err := json.MarshalIndent(keychainMetadata, "", " ")
|
metadataBytes, err := json.MarshalIndent(keychainMetadata, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
|
return nil, fmt.Errorf("failed to marshal unlocker metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
if err := afero.WriteFile(fs, filepath.Join(unlockerDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &KeychainUnlockKey{
|
return &KeychainUnlocker{
|
||||||
Directory: unlockKeyDir,
|
Directory: unlockerDir,
|
||||||
Metadata: keychainMetadata.UnlockKeyMetadata,
|
Metadata: keychainMetadata.UnlockerMetadata,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@ -398,7 +398,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
|
|||||||
func checkMacOSAvailable() error {
|
func checkMacOSAvailable() error {
|
||||||
cmd := exec.Command("/usr/bin/security", "help")
|
cmd := exec.Command("/usr/bin/security", "help")
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("macOS security command not available: %w (keychain unlock keys are only supported on macOS)", err)
|
return fmt.Errorf("macOS security command not available: %w (keychain unlockers are only supported on macOS)", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
@ -14,8 +14,8 @@ type VaultMetadata struct {
|
|||||||
MnemonicHash string `json:"mnemonic_hash"` // Double SHA256 hash of mnemonic for index tracking
|
MnemonicHash string `json:"mnemonic_hash"` // Double SHA256 hash of mnemonic for index tracking
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnlockKeyMetadata contains information about an unlock key
|
// UnlockerMetadata contains information about an unlocker
|
||||||
type UnlockKeyMetadata struct {
|
type UnlockerMetadata struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"` // passphrase, pgp, keychain
|
Type string `json:"type"` // passphrase, pgp, keychain
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
func TestPassphraseUnlockerWithRealFS(t *testing.T) {
|
||||||
// Skip this test if CI=true is set, as it uses real filesystem
|
// Skip this test if CI=true is set, as it uses real filesystem
|
||||||
if os.Getenv("CI") == "true" {
|
if os.Getenv("CI") == "true" {
|
||||||
t.Skip("Skipping test with real filesystem in CI environment")
|
t.Skip("Skipping test with real filesystem in CI environment")
|
||||||
@ -33,21 +33,21 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
testPassphrase := "test-passphrase-123"
|
testPassphrase := "test-passphrase-123"
|
||||||
|
|
||||||
// Create the directory structure
|
// Create the directory structure
|
||||||
keyDir := filepath.Join(tempDir, "unlock-key")
|
unlockerDir := filepath.Join(tempDir, "unlocker")
|
||||||
if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
|
if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||||
t.Fatalf("Failed to create key directory: %v", err)
|
t.Fatalf("Failed to create unlocker directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up test metadata
|
// Set up test metadata
|
||||||
metadata := secret.UnlockKeyMetadata{
|
metadata := secret.UnlockerMetadata{
|
||||||
ID: "test-passphrase",
|
ID: "test-passphrase",
|
||||||
Type: "passphrase",
|
Type: "passphrase",
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
Flags: []string{},
|
Flags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create passphrase unlock key
|
// Create passphrase unlocker
|
||||||
unlockKey := secret.NewPassphraseUnlockKey(fs, keyDir, metadata)
|
unlocker := secret.NewPassphraseUnlocker(fs, unlockerDir, metadata)
|
||||||
|
|
||||||
// Generate a test age identity
|
// Generate a test age identity
|
||||||
ageIdentity, err := age.GenerateX25519Identity()
|
ageIdentity, err := age.GenerateX25519Identity()
|
||||||
@ -59,7 +59,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
|
|
||||||
// Test writing public key
|
// Test writing public key
|
||||||
t.Run("WritePublicKey", func(t *testing.T) {
|
t.Run("WritePublicKey", func(t *testing.T) {
|
||||||
pubKeyPath := filepath.Join(keyDir, "pub.age")
|
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
if err := afero.WriteFile(fs, pubKeyPath, []byte(agePublicKey), secret.FilePerms); err != nil {
|
if err := afero.WriteFile(fs, pubKeyPath, []byte(agePublicKey), secret.FilePerms); err != nil {
|
||||||
t.Fatalf("Failed to write public key: %v", err)
|
t.Fatalf("Failed to write public key: %v", err)
|
||||||
}
|
}
|
||||||
@ -82,7 +82,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
t.Fatalf("Failed to encrypt private key: %v", err)
|
t.Fatalf("Failed to encrypt private key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
privKeyPath := filepath.Join(keyDir, "priv.age")
|
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||||
if err := afero.WriteFile(fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
if err := afero.WriteFile(fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||||
t.Fatalf("Failed to write encrypted private key: %v", err)
|
t.Fatalf("Failed to write encrypted private key: %v", err)
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
t.Fatalf("Failed to derive long-term identity: %v", err)
|
t.Fatalf("Failed to derive long-term identity: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encrypt long-term private key to the unlock key's recipient
|
// Encrypt long-term private key to the unlocker's recipient
|
||||||
recipient, err := age.ParseX25519Recipient(agePublicKey)
|
recipient, err := age.ParseX25519Recipient(agePublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to parse recipient: %v", err)
|
t.Fatalf("Failed to parse recipient: %v", err)
|
||||||
@ -117,7 +117,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
t.Fatalf("Failed to encrypt long-term private key: %v", err)
|
t.Fatalf("Failed to encrypt long-term private key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ltPrivKeyPath := filepath.Join(keyDir, "longterm.age")
|
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||||
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||||
t.Fatalf("Failed to write encrypted long-term private key: %v", err)
|
t.Fatalf("Failed to write encrypted long-term private key: %v", err)
|
||||||
}
|
}
|
||||||
@ -147,7 +147,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
|
|
||||||
// Test getting identity from environment variable
|
// Test getting identity from environment variable
|
||||||
t.Run("GetIdentityFromEnv", func(t *testing.T) {
|
t.Run("GetIdentityFromEnv", func(t *testing.T) {
|
||||||
identity, err := unlockKey.GetIdentity()
|
identity, err := unlocker.GetIdentity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get identity from env: %v", err)
|
t.Fatalf("Failed to get identity from env: %v", err)
|
||||||
}
|
}
|
||||||
@ -168,26 +168,26 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
|
|||||||
// Here we'll just verify the error is what we expect when no passphrase is available
|
// Here we'll just verify the error is what we expect when no passphrase is available
|
||||||
t.Run("GetIdentityWithoutEnv", func(t *testing.T) {
|
t.Run("GetIdentityWithoutEnv", func(t *testing.T) {
|
||||||
// This should fail since we're not in an interactive terminal
|
// This should fail since we're not in an interactive terminal
|
||||||
_, err := unlockKey.GetIdentity()
|
_, err := unlocker.GetIdentity()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Should have failed to get identity without passphrase env var")
|
t.Errorf("Should have failed to get identity without passphrase env var")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test removing the unlock key
|
// Test removing the unlocker
|
||||||
t.Run("RemoveUnlockKey", func(t *testing.T) {
|
t.Run("RemoveUnlocker", func(t *testing.T) {
|
||||||
err := unlockKey.Remove()
|
err := unlocker.Remove()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to remove unlock key: %v", err)
|
t.Fatalf("Failed to remove unlocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the directory is gone
|
// Verify the directory is gone
|
||||||
exists, err := afero.DirExists(fs, keyDir)
|
exists, err := afero.DirExists(fs, unlockerDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if key directory exists: %v", err)
|
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
||||||
}
|
}
|
||||||
if exists {
|
if exists {
|
||||||
t.Errorf("Key directory should not exist after removal")
|
t.Errorf("Unlocker directory should not exist after removal")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,150 +0,0 @@
|
|||||||
package secret
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"filippo.io/age"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PassphraseUnlockKey represents a passphrase-protected unlock key
|
|
||||||
type PassphraseUnlockKey struct {
|
|
||||||
Directory string
|
|
||||||
Metadata UnlockKeyMetadata
|
|
||||||
fs afero.Fs
|
|
||||||
Passphrase string
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIdentity implements UnlockKey interface for passphrase-based unlock keys
|
|
||||||
func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
|
||||||
DebugWith("Getting passphrase unlock key identity",
|
|
||||||
slog.String("key_id", p.GetID()),
|
|
||||||
slog.String("key_type", p.GetType()),
|
|
||||||
)
|
|
||||||
|
|
||||||
// First check if we already have the passphrase
|
|
||||||
passphraseStr := p.Passphrase
|
|
||||||
if passphraseStr == "" {
|
|
||||||
Debug("No passphrase in memory, checking environment")
|
|
||||||
// Check environment variable for passphrase
|
|
||||||
passphraseStr = os.Getenv(EnvUnlockPassphrase)
|
|
||||||
if passphraseStr == "" {
|
|
||||||
Debug("No passphrase in environment, prompting user")
|
|
||||||
// Prompt for passphrase
|
|
||||||
var err error
|
|
||||||
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
|
|
||||||
if err != nil {
|
|
||||||
Debug("Failed to read passphrase", "error", err, "key_id", p.GetID())
|
|
||||||
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Debug("Using passphrase from environment", "key_id", p.GetID())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Debug("Using in-memory passphrase", "key_id", p.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read encrypted private key of unlock key
|
|
||||||
unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age")
|
|
||||||
Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath)
|
|
||||||
|
|
||||||
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockKeyPrivPath)
|
|
||||||
if err != nil {
|
|
||||||
Debug("Failed to read passphrase unlock key private key", "error", err, "path", unlockKeyPrivPath)
|
|
||||||
return nil, fmt.Errorf("failed to read unlock key private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugWith("Read encrypted passphrase unlock key",
|
|
||||||
slog.String("key_id", p.GetID()),
|
|
||||||
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
|
||||||
)
|
|
||||||
|
|
||||||
Debug("Decrypting unlock key private key with passphrase", "key_id", p.GetID())
|
|
||||||
|
|
||||||
// Decrypt the unlock key private key with passphrase
|
|
||||||
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
|
||||||
if err != nil {
|
|
||||||
Debug("Failed to decrypt unlock key private key", "error", err, "key_id", p.GetID())
|
|
||||||
return nil, fmt.Errorf("failed to decrypt unlock key private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugWith("Successfully decrypted unlock key private key",
|
|
||||||
slog.String("key_id", p.GetID()),
|
|
||||||
slog.Int("decrypted_length", len(privKeyData)),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Parse the decrypted private key
|
|
||||||
Debug("Parsing decrypted unlock key identity", "key_id", p.GetID())
|
|
||||||
identity, err := age.ParseX25519Identity(string(privKeyData))
|
|
||||||
if err != nil {
|
|
||||||
Debug("Failed to parse unlock key private key", "error", err, "key_id", p.GetID())
|
|
||||||
return nil, fmt.Errorf("failed to parse unlock key private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
DebugWith("Successfully parsed passphrase unlock key identity",
|
|
||||||
slog.String("key_id", p.GetID()),
|
|
||||||
slog.String("public_key", identity.Recipient().String()),
|
|
||||||
)
|
|
||||||
|
|
||||||
return identity, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetType implements UnlockKey interface
|
|
||||||
func (p *PassphraseUnlockKey) GetType() string {
|
|
||||||
return "passphrase"
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMetadata implements UnlockKey interface
|
|
||||||
func (p *PassphraseUnlockKey) GetMetadata() UnlockKeyMetadata {
|
|
||||||
return p.Metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDirectory implements UnlockKey interface
|
|
||||||
func (p *PassphraseUnlockKey) GetDirectory() string {
|
|
||||||
return p.Directory
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetID implements UnlockKey interface
|
|
||||||
func (p *PassphraseUnlockKey) GetID() string {
|
|
||||||
return p.Metadata.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID implements UnlockKey interface - generates ID from creation timestamp
|
|
||||||
func (p *PassphraseUnlockKey) ID() string {
|
|
||||||
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
|
||||||
createdAt := p.Metadata.CreatedAt
|
|
||||||
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove implements UnlockKey interface - removes the passphrase unlock key
|
|
||||||
func (p *PassphraseUnlockKey) Remove() error {
|
|
||||||
// For passphrase keys, we just need to remove the directory
|
|
||||||
// No external resources (like keychain items) to clean up
|
|
||||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
|
||||||
return fmt.Errorf("failed to remove passphrase unlock key directory: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPassphraseUnlockKey creates a new PassphraseUnlockKey instance
|
|
||||||
func NewPassphraseUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PassphraseUnlockKey {
|
|
||||||
return &PassphraseUnlockKey{
|
|
||||||
Directory: directory,
|
|
||||||
Metadata: metadata,
|
|
||||||
fs: fs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
|
||||||
func CreatePassphraseKey(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlockKey, error) {
|
|
||||||
// Get current vault
|
|
||||||
currentVault, err := GetCurrentVault(fs, stateDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentVault.CreatePassphraseKey(passphrase)
|
|
||||||
}
|
|
150
internal/secret/passphraseunlocker.go
Normal file
150
internal/secret/passphraseunlocker.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PassphraseUnlocker represents a passphrase-protected unlocker
|
||||||
|
type PassphraseUnlocker struct {
|
||||||
|
Directory string
|
||||||
|
Metadata UnlockerMetadata
|
||||||
|
fs afero.Fs
|
||||||
|
Passphrase string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIdentity implements Unlocker interface for passphrase-based unlockers
|
||||||
|
func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||||
|
DebugWith("Getting passphrase unlocker identity",
|
||||||
|
slog.String("unlocker_id", p.GetID()),
|
||||||
|
slog.String("unlocker_type", p.GetType()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// First check if we already have the passphrase
|
||||||
|
passphraseStr := p.Passphrase
|
||||||
|
if passphraseStr == "" {
|
||||||
|
Debug("No passphrase in memory, checking environment")
|
||||||
|
// Check environment variable for passphrase
|
||||||
|
passphraseStr = os.Getenv(EnvUnlockPassphrase)
|
||||||
|
if passphraseStr == "" {
|
||||||
|
Debug("No passphrase in environment, prompting user")
|
||||||
|
// Prompt for passphrase
|
||||||
|
var err error
|
||||||
|
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Debug("Using in-memory passphrase", "unlocker_id", p.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read encrypted private key of unlocker
|
||||||
|
unlockerPrivPath := filepath.Join(p.Directory, "priv.age")
|
||||||
|
Debug("Reading encrypted passphrase unlocker", "path", unlockerPrivPath)
|
||||||
|
|
||||||
|
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockerPrivPath)
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to read passphrase unlocker private key", "error", err, "path", unlockerPrivPath)
|
||||||
|
return nil, fmt.Errorf("failed to read unlocker private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Read encrypted passphrase unlocker",
|
||||||
|
slog.String("unlocker_id", p.GetID()),
|
||||||
|
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
||||||
|
)
|
||||||
|
|
||||||
|
Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
|
||||||
|
|
||||||
|
// Decrypt the unlocker private key with passphrase
|
||||||
|
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to decrypt unlocker private key", "error", err, "unlocker_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Successfully decrypted unlocker private key",
|
||||||
|
slog.String("unlocker_id", p.GetID()),
|
||||||
|
slog.Int("decrypted_length", len(privKeyData)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse the decrypted private key
|
||||||
|
Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID())
|
||||||
|
identity, err := age.ParseX25519Identity(string(privKeyData))
|
||||||
|
if err != nil {
|
||||||
|
Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())
|
||||||
|
return nil, fmt.Errorf("failed to parse unlocker private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugWith("Successfully parsed passphrase unlocker identity",
|
||||||
|
slog.String("unlocker_id", p.GetID()),
|
||||||
|
slog.String("public_key", identity.Recipient().String()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return identity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetType implements Unlocker interface
|
||||||
|
func (p *PassphraseUnlocker) GetType() string {
|
||||||
|
return "passphrase"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadata implements Unlocker interface
|
||||||
|
func (p *PassphraseUnlocker) GetMetadata() UnlockerMetadata {
|
||||||
|
return p.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDirectory implements Unlocker interface
|
||||||
|
func (p *PassphraseUnlocker) GetDirectory() string {
|
||||||
|
return p.Directory
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID implements Unlocker interface
|
||||||
|
func (p *PassphraseUnlocker) GetID() string {
|
||||||
|
return p.Metadata.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID implements Unlocker interface - generates ID from creation timestamp
|
||||||
|
func (p *PassphraseUnlocker) ID() string {
|
||||||
|
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
||||||
|
createdAt := p.Metadata.CreatedAt
|
||||||
|
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove implements Unlocker interface - removes the passphrase unlocker
|
||||||
|
func (p *PassphraseUnlocker) Remove() error {
|
||||||
|
// For passphrase unlockers, we just need to remove the directory
|
||||||
|
// No external resources (like keychain items) to clean up
|
||||||
|
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove passphrase unlocker directory: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPassphraseUnlocker creates a new PassphraseUnlocker instance
|
||||||
|
func NewPassphraseUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *PassphraseUnlocker {
|
||||||
|
return &PassphraseUnlocker{
|
||||||
|
Directory: directory,
|
||||||
|
Metadata: metadata,
|
||||||
|
fs: fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||||
|
func CreatePassphraseUnlocker(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlocker, error) {
|
||||||
|
// Get current vault
|
||||||
|
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentVault.CreatePassphraseUnlocker(passphrase)
|
||||||
|
}
|
@ -123,7 +123,7 @@ func runGPGWithPassphrase(gnupgHome, passphrase string, args []string, input io.
|
|||||||
return stdout.Bytes(), nil
|
return stdout.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPGPUnlockKeyWithRealFS(t *testing.T) {
|
func TestPGPUnlockerWithRealFS(t *testing.T) {
|
||||||
// Skip tests if gpg is not available
|
// Skip tests if gpg is not available
|
||||||
if _, err := exec.LookPath("gpg"); err != nil {
|
if _, err := exec.LookPath("gpg"); err != nil {
|
||||||
t.Skip("GPG not available, skipping PGP unlock key tests")
|
t.Skip("GPG not available, skipping PGP unlock key tests")
|
||||||
@ -258,7 +258,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
vaultName := "test-vault"
|
vaultName := "test-vault"
|
||||||
|
|
||||||
// Test creation of a PGP unlock key through a vault
|
// Test creation of a PGP unlock key through a vault
|
||||||
t.Run("CreatePGPUnlockKey", func(t *testing.T) {
|
t.Run("CreatePGPUnlocker", func(t *testing.T) {
|
||||||
// Set a limited test timeout to avoid hanging
|
// Set a limited test timeout to avoid hanging
|
||||||
timer := time.AfterFunc(30*time.Second, func() {
|
timer := time.AfterFunc(30*time.Second, func() {
|
||||||
t.Fatalf("Test timed out after 30 seconds")
|
t.Fatalf("Test timed out after 30 seconds")
|
||||||
@ -298,50 +298,50 @@ Passphrase: ` + testPassphrase + `
|
|||||||
// Unlock the vault
|
// Unlock the vault
|
||||||
vlt.Unlock(ltIdentity)
|
vlt.Unlock(ltIdentity)
|
||||||
|
|
||||||
// Create a passphrase unlock key first (to have current unlock key)
|
// Create a passphrase unlocker first (to have current unlocker)
|
||||||
passKey, err := vlt.CreatePassphraseKey("test-passphrase")
|
passUnlocker, err := vlt.CreatePassphraseUnlocker("test-passphrase")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create passphrase key: %v", err)
|
t.Fatalf("Failed to create passphrase unlocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify passphrase key was created
|
// Verify passphrase unlocker was created
|
||||||
if passKey == nil {
|
if passUnlocker == nil {
|
||||||
t.Fatal("Passphrase key is nil")
|
t.Fatal("Passphrase unlocker is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
|
// Now create a PGP unlock key (this will use our custom GPGEncryptFunc)
|
||||||
pgpKey, err := secret.CreatePGPUnlockKey(fs, stateDir, keyID)
|
pgpUnlocker, err := secret.CreatePGPUnlocker(fs, stateDir, keyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create PGP unlock key: %v", err)
|
t.Fatalf("Failed to create PGP unlock key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the PGP unlock key was created
|
// Verify the PGP unlock key was created
|
||||||
if pgpKey == nil {
|
if pgpUnlocker == nil {
|
||||||
t.Fatal("PGP unlock key is nil")
|
t.Fatal("PGP unlock key is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the key has the correct type
|
// Check if the key has the correct type
|
||||||
if pgpKey.GetType() != "pgp" {
|
if pgpUnlocker.GetType() != "pgp" {
|
||||||
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpKey.GetType())
|
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpUnlocker.GetType())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the key ID includes the GPG key ID
|
// Check if the key ID includes the GPG key ID
|
||||||
if !strings.Contains(pgpKey.GetID(), keyID) {
|
if !strings.Contains(pgpUnlocker.GetID(), keyID) {
|
||||||
t.Errorf("PGP unlock key ID '%s' does not contain GPG key ID '%s'", pgpKey.GetID(), keyID)
|
t.Errorf("PGP unlock key ID '%s' does not contain GPG key ID '%s'", pgpUnlocker.GetID(), keyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the key directory exists
|
// Check if the key directory exists
|
||||||
keyDir := pgpKey.GetDirectory()
|
unlockerDir := pgpUnlocker.GetDirectory()
|
||||||
keyExists, err := afero.DirExists(fs, keyDir)
|
keyExists, err := afero.DirExists(fs, unlockerDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if PGP key directory exists: %v", err)
|
t.Fatalf("Failed to check if PGP key directory exists: %v", err)
|
||||||
}
|
}
|
||||||
if !keyExists {
|
if !keyExists {
|
||||||
t.Errorf("PGP unlock key directory does not exist: %s", keyDir)
|
t.Errorf("PGP unlock key directory does not exist: %s", unlockerDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if required files exist
|
// Check if required files exist
|
||||||
pubKeyPath := filepath.Join(keyDir, "pub.age")
|
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
pubKeyExists, err := afero.Exists(fs, pubKeyPath)
|
pubKeyExists, err := afero.Exists(fs, pubKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if public key file exists: %v", err)
|
t.Fatalf("Failed to check if public key file exists: %v", err)
|
||||||
@ -350,7 +350,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
t.Errorf("PGP unlock key public key file does not exist: %s", pubKeyPath)
|
t.Errorf("PGP unlock key public key file does not exist: %s", pubKeyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
privKeyPath := filepath.Join(keyDir, "priv.age.gpg")
|
privKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
||||||
privKeyExists, err := afero.Exists(fs, privKeyPath)
|
privKeyExists, err := afero.Exists(fs, privKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if private key file exists: %v", err)
|
t.Fatalf("Failed to check if private key file exists: %v", err)
|
||||||
@ -359,7 +359,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
t.Errorf("PGP unlock key private key file does not exist: %s", privKeyPath)
|
t.Errorf("PGP unlock key private key file does not exist: %s", privKeyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
|
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
||||||
metadataExists, err := afero.Exists(fs, metadataPath)
|
metadataExists, err := afero.Exists(fs, metadataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if metadata file exists: %v", err)
|
t.Fatalf("Failed to check if metadata file exists: %v", err)
|
||||||
@ -368,7 +368,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
t.Errorf("PGP unlock key metadata file does not exist: %s", metadataPath)
|
t.Errorf("PGP unlock key metadata file does not exist: %s", metadataPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
longtermPath := filepath.Join(keyDir, "longterm.age")
|
longtermPath := filepath.Join(unlockerDir, "longterm.age")
|
||||||
longtermExists, err := afero.Exists(fs, longtermPath)
|
longtermExists, err := afero.Exists(fs, longtermPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if longterm key file exists: %v", err)
|
t.Fatalf("Failed to check if longterm key file exists: %v", err)
|
||||||
@ -405,37 +405,37 @@ Passphrase: ` + testPassphrase + `
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Set up key directory for individual tests
|
// Set up key directory for individual tests
|
||||||
keyDir := filepath.Join(tempDir, "unlock-key")
|
unlockerDir := filepath.Join(tempDir, "unlocker")
|
||||||
if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
|
if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||||
t.Fatalf("Failed to create key directory: %v", err)
|
t.Fatalf("Failed to create unlocker directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up test metadata
|
// Set up test metadata
|
||||||
metadata := secret.UnlockKeyMetadata{
|
metadata := secret.UnlockerMetadata{
|
||||||
ID: fmt.Sprintf("%s-pgp", keyID),
|
ID: fmt.Sprintf("%s-pgp", keyID),
|
||||||
Type: "pgp",
|
Type: "pgp",
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
Flags: []string{"gpg", "encrypted"},
|
Flags: []string{"gpg", "encrypted"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a PGP unlock key for the remaining tests
|
// Create a PGP unlocker for the remaining tests
|
||||||
unlockKey := secret.NewPGPUnlockKey(fs, keyDir, metadata)
|
unlocker := secret.NewPGPUnlocker(fs, unlockerDir, metadata)
|
||||||
|
|
||||||
// Test getting GPG key ID
|
// Test getting GPG key ID
|
||||||
t.Run("GetGPGKeyID", func(t *testing.T) {
|
t.Run("GetGPGKeyID", func(t *testing.T) {
|
||||||
// Create PGP metadata with GPG key ID
|
// Create PGP metadata with GPG key ID
|
||||||
type PGPUnlockKeyMetadata struct {
|
type PGPUnlockerMetadata struct {
|
||||||
secret.UnlockKeyMetadata
|
secret.UnlockerMetadata
|
||||||
GPGKeyID string `json:"gpg_key_id"`
|
GPGKeyID string `json:"gpg_key_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
pgpMetadata := PGPUnlockKeyMetadata{
|
pgpMetadata := PGPUnlockerMetadata{
|
||||||
UnlockKeyMetadata: metadata,
|
UnlockerMetadata: metadata,
|
||||||
GPGKeyID: keyID,
|
GPGKeyID: keyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write metadata file
|
// Write metadata file
|
||||||
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
|
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
|
||||||
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to marshal metadata: %v", err)
|
t.Fatalf("Failed to marshal metadata: %v", err)
|
||||||
@ -445,7 +445,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get GPG key ID
|
// Get GPG key ID
|
||||||
retrievedKeyID, err := unlockKey.GetGPGKeyID()
|
retrievedKeyID, err := unlocker.GetGPGKeyID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get GPG key ID: %v", err)
|
t.Fatalf("Failed to get GPG key ID: %v", err)
|
||||||
}
|
}
|
||||||
@ -456,7 +456,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test getting identity from PGP unlock key
|
// Test getting identity from PGP unlocker
|
||||||
t.Run("GetIdentity", func(t *testing.T) {
|
t.Run("GetIdentity", func(t *testing.T) {
|
||||||
// Generate an age identity for testing
|
// Generate an age identity for testing
|
||||||
ageIdentity, err := age.GenerateX25519Identity()
|
ageIdentity, err := age.GenerateX25519Identity()
|
||||||
@ -465,7 +465,7 @@ Passphrase: ` + testPassphrase + `
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the public key
|
// Write the public key
|
||||||
pubKeyPath := filepath.Join(keyDir, "pub.age")
|
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
if err := afero.WriteFile(fs, pubKeyPath, []byte(ageIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
if err := afero.WriteFile(fs, pubKeyPath, []byte(ageIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||||
t.Fatalf("Failed to write public key: %v", err)
|
t.Fatalf("Failed to write public key: %v", err)
|
||||||
}
|
}
|
||||||
@ -478,13 +478,13 @@ Passphrase: ` + testPassphrase + `
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the encrypted data to a file
|
// Write the encrypted data to a file
|
||||||
encryptedPath := filepath.Join(keyDir, "priv.age.gpg")
|
encryptedPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
||||||
if err := afero.WriteFile(fs, encryptedPath, encryptedOutput, secret.FilePerms); err != nil {
|
if err := afero.WriteFile(fs, encryptedPath, encryptedOutput, secret.FilePerms); err != nil {
|
||||||
t.Fatalf("Failed to write encrypted private key: %v", err)
|
t.Fatalf("Failed to write encrypted private key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now try to get the identity - this will use our custom GPGDecryptFunc
|
// Now try to get the identity - this will use our custom GPGDecryptFunc
|
||||||
identity, err := unlockKey.GetIdentity()
|
identity, err := unlocker.GetIdentity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get identity: %v", err)
|
t.Fatalf("Failed to get identity: %v", err)
|
||||||
}
|
}
|
||||||
@ -497,30 +497,30 @@ Passphrase: ` + testPassphrase + `
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test removing the unlock key
|
// Test removing the unlocker
|
||||||
t.Run("RemoveUnlockKey", func(t *testing.T) {
|
t.Run("RemoveUnlocker", func(t *testing.T) {
|
||||||
// Ensure key directory exists before removal
|
// Ensure unlocker directory exists before removal
|
||||||
keyExists, err := afero.DirExists(fs, keyDir)
|
keyExists, err := afero.DirExists(fs, unlockerDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if key directory exists: %v", err)
|
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
||||||
}
|
}
|
||||||
if !keyExists {
|
if !keyExists {
|
||||||
t.Fatalf("Key directory does not exist: %s", keyDir)
|
t.Fatalf("Unlocker directory does not exist: %s", unlockerDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove unlock key
|
// Remove unlocker
|
||||||
err = unlockKey.Remove()
|
err = unlocker.Remove()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to remove unlock key: %v", err)
|
t.Fatalf("Failed to remove unlocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify directory is gone
|
// Verify directory is gone
|
||||||
keyExists, err = afero.DirExists(fs, keyDir)
|
keyExists, err = afero.DirExists(fs, unlockerDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to check if key directory exists: %v", err)
|
t.Fatalf("Failed to check if unlocker directory exists: %v", err)
|
||||||
}
|
}
|
||||||
if keyExists {
|
if keyExists {
|
||||||
t.Errorf("Key directory still exists after removal: %s", keyDir)
|
t.Errorf("Unlocker directory still exists after removal: %s", unlockerDir)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -26,9 +26,9 @@ var (
|
|||||||
GPGDecryptFunc = gpgDecryptDefault
|
GPGDecryptFunc = gpgDecryptDefault
|
||||||
)
|
)
|
||||||
|
|
||||||
// PGPUnlockKeyMetadata extends UnlockKeyMetadata with PGP-specific data
|
// PGPUnlockerMetadata extends UnlockerMetadata with PGP-specific data
|
||||||
type PGPUnlockKeyMetadata struct {
|
type PGPUnlockerMetadata struct {
|
||||||
UnlockKeyMetadata
|
UnlockerMetadata
|
||||||
// GPG key ID used for encryption
|
// GPG key ID used for encryption
|
||||||
GPGKeyID string `json:"gpg_key_id"`
|
GPGKeyID string `json:"gpg_key_id"`
|
||||||
// Age keypair information
|
// Age keypair information
|
||||||
@ -36,18 +36,18 @@ type PGPUnlockKeyMetadata struct {
|
|||||||
AgeRecipient string `json:"age_recipient"`
|
AgeRecipient string `json:"age_recipient"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PGPUnlockKey represents a PGP-protected unlock key
|
// PGPUnlocker represents a PGP-protected unlocker
|
||||||
type PGPUnlockKey struct {
|
type PGPUnlocker struct {
|
||||||
Directory string
|
Directory string
|
||||||
Metadata UnlockKeyMetadata
|
Metadata UnlockerMetadata
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIdentity implements UnlockKey interface for PGP-based unlock keys
|
// GetIdentity implements Unlocker interface for PGP-based unlockers
|
||||||
func (p *PGPUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
func (p *PGPUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||||
DebugWith("Getting PGP unlock key identity",
|
DebugWith("Getting PGP unlocker identity",
|
||||||
slog.String("key_id", p.GetID()),
|
slog.String("unlocker_id", p.GetID()),
|
||||||
slog.String("key_type", p.GetType()),
|
slog.String("unlocker_type", p.GetType()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 1: Read the encrypted age private key from filesystem
|
// Step 1: Read the encrypted age private key from filesystem
|
||||||
@ -61,61 +61,61 @@ func (p *PGPUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Read PGP-encrypted age private key",
|
DebugWith("Read PGP-encrypted age private key",
|
||||||
slog.String("key_id", p.GetID()),
|
slog.String("unlocker_id", p.GetID()),
|
||||||
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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", "key_id", p.GetID())
|
Debug("Decrypting age private key with GPG", "unlocker_id", p.GetID())
|
||||||
agePrivKeyData, err := GPGDecryptFunc(encryptedAgePrivKeyData)
|
agePrivKeyData, err := GPGDecryptFunc(encryptedAgePrivKeyData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to decrypt age private key with GPG", "error", err, "key_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)
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Successfully decrypted age private key with GPG",
|
DebugWith("Successfully decrypted age private key with GPG",
|
||||||
slog.String("key_id", p.GetID()),
|
slog.String("unlocker_id", p.GetID()),
|
||||||
slog.Int("decrypted_length", len(agePrivKeyData)),
|
slog.Int("decrypted_length", len(agePrivKeyData)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 3: Parse the decrypted age private key
|
// Step 3: Parse the decrypted age private key
|
||||||
Debug("Parsing decrypted age private key", "key_id", p.GetID())
|
Debug("Parsing decrypted age private key", "unlocker_id", p.GetID())
|
||||||
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
|
ageIdentity, err := age.ParseX25519Identity(string(agePrivKeyData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to parse age private key", "error", err, "key_id", p.GetID())
|
Debug("Failed to parse age private key", "error", err, "unlocker_id", p.GetID())
|
||||||
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
return nil, fmt.Errorf("failed to parse age private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Successfully parsed PGP age identity",
|
DebugWith("Successfully parsed PGP age identity",
|
||||||
slog.String("key_id", p.GetID()),
|
slog.String("unlocker_id", p.GetID()),
|
||||||
slog.String("public_key", ageIdentity.Recipient().String()),
|
slog.String("public_key", ageIdentity.Recipient().String()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return ageIdentity, nil
|
return ageIdentity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType implements UnlockKey interface
|
// GetType implements Unlocker interface
|
||||||
func (p *PGPUnlockKey) GetType() string {
|
func (p *PGPUnlocker) GetType() string {
|
||||||
return "pgp"
|
return "pgp"
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMetadata implements UnlockKey interface
|
// GetMetadata implements Unlocker interface
|
||||||
func (p *PGPUnlockKey) GetMetadata() UnlockKeyMetadata {
|
func (p *PGPUnlocker) GetMetadata() UnlockerMetadata {
|
||||||
return p.Metadata
|
return p.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDirectory implements UnlockKey interface
|
// GetDirectory implements Unlocker interface
|
||||||
func (p *PGPUnlockKey) GetDirectory() string {
|
func (p *PGPUnlocker) GetDirectory() string {
|
||||||
return p.Directory
|
return p.Directory
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetID implements UnlockKey interface
|
// GetID implements Unlocker interface
|
||||||
func (p *PGPUnlockKey) GetID() string {
|
func (p *PGPUnlocker) GetID() string {
|
||||||
return p.Metadata.ID
|
return p.Metadata.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID implements UnlockKey interface - generates ID from GPG key ID
|
// ID implements Unlocker interface - generates ID from GPG key ID
|
||||||
func (p *PGPUnlockKey) ID() string {
|
func (p *PGPUnlocker) ID() string {
|
||||||
// Generate ID using GPG key ID: <keyid>-pgp
|
// Generate ID using GPG key ID: <keyid>-pgp
|
||||||
gpgKeyID, err := p.GetGPGKeyID()
|
gpgKeyID, err := p.GetGPGKeyID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -125,19 +125,19 @@ func (p *PGPUnlockKey) ID() string {
|
|||||||
return fmt.Sprintf("%s-pgp", gpgKeyID)
|
return fmt.Sprintf("%s-pgp", gpgKeyID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove implements UnlockKey interface - removes the PGP unlock key
|
// Remove implements Unlocker interface - removes the PGP unlocker
|
||||||
func (p *PGPUnlockKey) Remove() error {
|
func (p *PGPUnlocker) Remove() error {
|
||||||
// For PGP keys, we just need to remove the directory
|
// For PGP unlockers, we just need to remove the directory
|
||||||
// No external resources (like keychain items) to clean up
|
// No external resources (like keychain items) to clean up
|
||||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||||
return fmt.Errorf("failed to remove PGP unlock key directory: %w", err)
|
return fmt.Errorf("failed to remove PGP unlocker directory: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPGPUnlockKey creates a new PGPUnlockKey instance
|
// NewPGPUnlocker creates a new PGPUnlocker instance
|
||||||
func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PGPUnlockKey {
|
func NewPGPUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *PGPUnlocker {
|
||||||
return &PGPUnlockKey{
|
return &PGPUnlocker{
|
||||||
Directory: directory,
|
Directory: directory,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
@ -145,7 +145,7 @@ func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetGPGKeyID returns the GPG key ID from metadata
|
// GetGPGKeyID returns the GPG key ID from metadata
|
||||||
func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
|
func (p *PGPUnlocker) GetGPGKeyID() (string, error) {
|
||||||
// Load the metadata
|
// Load the metadata
|
||||||
metadataPath := filepath.Join(p.Directory, "unlock-metadata.json")
|
metadataPath := filepath.Join(p.Directory, "unlock-metadata.json")
|
||||||
metadataData, err := afero.ReadFile(p.fs, metadataPath)
|
metadataData, err := afero.ReadFile(p.fs, metadataPath)
|
||||||
@ -153,7 +153,7 @@ func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
|
|||||||
return "", fmt.Errorf("failed to read PGP metadata: %w", err)
|
return "", fmt.Errorf("failed to read PGP metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var pgpMetadata PGPUnlockKeyMetadata
|
var pgpMetadata PGPUnlockerMetadata
|
||||||
if err := json.Unmarshal(metadataData, &pgpMetadata); err != nil {
|
if err := json.Unmarshal(metadataData, &pgpMetadata); err != nil {
|
||||||
return "", fmt.Errorf("failed to parse PGP metadata: %w", err)
|
return "", fmt.Errorf("failed to parse PGP metadata: %w", err)
|
||||||
}
|
}
|
||||||
@ -161,8 +161,8 @@ func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
|
|||||||
return pgpMetadata.GPGKeyID, nil
|
return pgpMetadata.GPGKeyID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generatePGPUnlockKeyName generates a unique name for the PGP unlock key based on hostname and date
|
// generatePGPUnlockerName generates a unique name for the PGP unlocker based on hostname and date
|
||||||
func generatePGPUnlockKeyName() (string, error) {
|
func generatePGPUnlockerName() (string, error) {
|
||||||
hostname, err := os.Hostname()
|
hostname, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get hostname: %w", err)
|
return "", fmt.Errorf("failed to get hostname: %w", err)
|
||||||
@ -173,8 +173,8 @@ func generatePGPUnlockKeyName() (string, error) {
|
|||||||
return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentDate), nil
|
return fmt.Sprintf("%s-pgp-%s", hostname, enrollmentDate), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePGPUnlockKey creates a new PGP unlock key and stores it in the vault
|
// CreatePGPUnlocker creates a new PGP unlocker and stores it in the vault
|
||||||
func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlockKey, error) {
|
func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlocker, error) {
|
||||||
// Check if GPG is available
|
// Check if GPG is available
|
||||||
if err := checkGPGAvailable(); err != nil {
|
if err := checkGPGAvailable(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -186,24 +186,24 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
|||||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the unlock key name based on hostname and date
|
// Generate the unlocker name based on hostname and date
|
||||||
unlockKeyName, err := generatePGPUnlockKeyName()
|
unlockerName, err := generatePGPUnlockerName()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate unlock key name: %w", err)
|
return nil, fmt.Errorf("failed to generate unlocker name: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unlock key directory using the generated name
|
// Create unlocker directory using the generated name
|
||||||
vaultDir, err := vault.GetDirectory()
|
vaultDir, err := vault.GetDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", unlockKeyName)
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerName)
|
||||||
if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
|
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 1: Generate a new age keypair for the PGP unlock key
|
// Step 1: Generate a new age keypair for the PGP unlocker
|
||||||
ageIdentity, err := age.GenerateX25519Identity()
|
ageIdentity, err := age.GenerateX25519Identity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
return nil, fmt.Errorf("failed to generate age keypair: %w", err)
|
||||||
@ -211,7 +211,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
|||||||
|
|
||||||
// Step 2: Store age public key as plaintext
|
// Step 2: Store age public key as plaintext
|
||||||
agePublicKeyString := ageIdentity.Recipient().String()
|
agePublicKeyString := ageIdentity.Recipient().String()
|
||||||
agePubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
agePubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
|
if err := afero.WriteFile(fs, agePubKeyPath, []byte(agePublicKeyString), FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write age public key: %w", err)
|
return nil, fmt.Errorf("failed to write age public key: %w", err)
|
||||||
}
|
}
|
||||||
@ -228,54 +228,54 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
|||||||
}
|
}
|
||||||
ltPrivKeyData = []byte(ltIdentity.String())
|
ltPrivKeyData = []byte(ltIdentity.String())
|
||||||
} else {
|
} else {
|
||||||
// Get the vault to access current unlock key
|
// Get the vault to access current unlocker
|
||||||
currentUnlockKey, err := vault.GetCurrentUnlockKey()
|
currentUnlocker, err := vault.GetCurrentUnlocker()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get current unlock key: %w", err)
|
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the current unlock key identity
|
// Get the current unlocker identity
|
||||||
currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
|
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get current unlock key identity: %w", err)
|
return nil, fmt.Errorf("failed to get current unlocker identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get encrypted long-term key from current unlock key, handling different types
|
// Get encrypted long-term key from current unlocker, handling different types
|
||||||
var encryptedLtPrivKey []byte
|
var encryptedLtPrivKey []byte
|
||||||
switch currentUnlockKey := currentUnlockKey.(type) {
|
switch currentUnlocker := currentUnlocker.(type) {
|
||||||
case *PassphraseUnlockKey:
|
case *PassphraseUnlocker:
|
||||||
// Read the encrypted long-term private key from passphrase unlock key
|
// Read the encrypted long-term private key from passphrase unlocker
|
||||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlock key: %w", err)
|
return nil, fmt.Errorf("failed to read encrypted long-term key from current passphrase unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
case *PGPUnlockKey:
|
case *PGPUnlocker:
|
||||||
// Read the encrypted long-term private key from PGP unlock key
|
// Read the encrypted long-term private key from PGP unlocker
|
||||||
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
|
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlock key: %w", err)
|
return nil, fmt.Errorf("failed to read encrypted long-term key from current PGP unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported current unlock key type for PGP unlock key creation")
|
return nil, fmt.Errorf("unsupported current unlocker type for PGP unlocker creation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Decrypt long-term private key using current unlock key
|
// Step 6: Decrypt long-term private key using current unlocker
|
||||||
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
|
ltPrivKeyData, 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Encrypt long-term private key to the new age unlock key
|
// Step 7: Encrypt long-term private key to the new age unlocker
|
||||||
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
encryptedLtPrivKeyToAge, err := EncryptToRecipient(ltPrivKeyData, ageIdentity.Recipient())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlock key: %w", err)
|
return nil, fmt.Errorf("failed to encrypt long-term private key to age unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write encrypted long-term private key
|
// Write encrypted long-term private key
|
||||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||||
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
|
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
@ -287,7 +287,7 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age.gpg")
|
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
|
||||||
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
|
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
|
return nil, fmt.Errorf("failed to write encrypted age private key: %w", err)
|
||||||
}
|
}
|
||||||
@ -296,8 +296,8 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
|||||||
// Generate the key ID directly using the GPG key ID
|
// Generate the key ID directly using the GPG key ID
|
||||||
keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
|
keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
|
||||||
|
|
||||||
pgpMetadata := PGPUnlockKeyMetadata{
|
pgpMetadata := PGPUnlockerMetadata{
|
||||||
UnlockKeyMetadata: UnlockKeyMetadata{
|
UnlockerMetadata: UnlockerMetadata{
|
||||||
ID: keyID,
|
ID: keyID,
|
||||||
Type: "pgp",
|
Type: "pgp",
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
@ -310,16 +310,16 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
|
|||||||
|
|
||||||
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to marshal unlock key metadata: %w", err)
|
return nil, fmt.Errorf("failed to marshal unlocker metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := afero.WriteFile(fs, filepath.Join(unlockKeyDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
if err := afero.WriteFile(fs, filepath.Join(unlockerDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PGPUnlockKey{
|
return &PGPUnlocker{
|
||||||
Directory: unlockKeyDir,
|
Directory: unlockerDir,
|
||||||
Metadata: pgpMetadata.UnlockKeyMetadata,
|
Metadata: pgpMetadata.UnlockerMetadata,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
@ -20,8 +20,8 @@ type VaultInterface interface {
|
|||||||
AddSecret(name string, value []byte, force bool) error
|
AddSecret(name string, value []byte, force bool) error
|
||||||
GetName() string
|
GetName() string
|
||||||
GetFilesystem() afero.Fs
|
GetFilesystem() afero.Fs
|
||||||
GetCurrentUnlockKey() (UnlockKey, error)
|
GetCurrentUnlocker() (Unlocker, error)
|
||||||
CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error)
|
CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secret represents a secret in a vault
|
// Secret represents a secret in a vault
|
||||||
@ -81,8 +81,8 @@ func (s *Secret) Save(value []byte, force bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetValue retrieves and decrypts the secret value using the provided unlock key
|
// GetValue retrieves and decrypts the secret value using the provided unlocker
|
||||||
func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
|
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, 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()),
|
||||||
@ -118,29 +118,29 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
|
|||||||
return s.decryptWithLongTermKey(ltIdentity)
|
return s.decryptWithLongTermKey(ltIdentity)
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug("Using unlock key for vault access", "secret_name", s.Name)
|
Debug("Using unlocker for vault access", "secret_name", s.Name)
|
||||||
|
|
||||||
// Use the provided unlock key to get the vault's long-term private key
|
// Use the provided unlocker to get the vault's long-term private key
|
||||||
if unlockKey == nil {
|
if unlocker == nil {
|
||||||
Debug("No unlock key provided for secret decryption", "secret_name", s.Name)
|
Debug("No unlocker provided for secret decryption", "secret_name", s.Name)
|
||||||
return nil, fmt.Errorf("unlock key required to decrypt secret")
|
return nil, fmt.Errorf("unlocker required to decrypt secret")
|
||||||
}
|
}
|
||||||
|
|
||||||
DebugWith("Getting vault's long-term key using unlock key",
|
DebugWith("Getting vault's long-term key using unlocker",
|
||||||
slog.String("secret_name", s.Name),
|
slog.String("secret_name", s.Name),
|
||||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
slog.String("unlocker_type", unlocker.GetType()),
|
||||||
slog.String("unlock_key_id", unlockKey.GetID()),
|
slog.String("unlocker_id", unlocker.GetID()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Step 1: Use the unlock key to get the vault's long-term private key
|
// Step 1: Use the unlocker to get the vault's long-term private key
|
||||||
unlockIdentity, err := unlockKey.GetIdentity()
|
unlockIdentity, err := unlocker.GetIdentity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Debug("Failed to get unlock key identity", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
|
Debug("Failed to get unlocker identity", "error", err, "secret_name", s.Name, "unlocker_type", unlocker.GetType())
|
||||||
return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
|
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the encrypted long-term private key from the unlock key directory
|
// Read the encrypted long-term private key from the unlocker directory
|
||||||
encryptedLtPrivKeyPath := filepath.Join(unlockKey.GetDirectory(), "longterm.age")
|
encryptedLtPrivKeyPath := filepath.Join(unlocker.GetDirectory(), "longterm.age")
|
||||||
Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
||||||
|
|
||||||
encryptedLtPrivKey, err := afero.ReadFile(s.vault.GetFilesystem(), encryptedLtPrivKeyPath)
|
encryptedLtPrivKey, err := afero.ReadFile(s.vault.GetFilesystem(), encryptedLtPrivKeyPath)
|
||||||
@ -149,8 +149,8 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
|
return nil, fmt.Errorf("failed to read encrypted long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt the encrypted long-term private key using the unlock key
|
// Decrypt the encrypted long-term private key using the unlocker
|
||||||
Debug("Decrypting long-term private key using unlock key", "secret_name", s.Name)
|
Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
|
||||||
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
|
ltPrivKeyData, 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)
|
||||||
|
@ -39,11 +39,11 @@ func (m *MockVault) GetFilesystem() afero.Fs {
|
|||||||
return m.fs
|
return m.fs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockVault) GetCurrentUnlockKey() (UnlockKey, error) {
|
func (m *MockVault) GetCurrentUnlocker() (Unlocker, error) {
|
||||||
return nil, nil // Not needed for this test
|
return nil, nil // Not needed for this test
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockVault) CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error) {
|
func (m *MockVault) CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error) {
|
||||||
return nil, nil // Not needed for this test
|
return nil, nil // Not needed for this test
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package secret
|
|
||||||
|
|
||||||
import (
|
|
||||||
"filippo.io/age"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UnlockKey interface defines the methods all unlock key types must implement
|
|
||||||
type UnlockKey interface {
|
|
||||||
GetIdentity() (*age.X25519Identity, error)
|
|
||||||
GetType() string
|
|
||||||
GetMetadata() UnlockKeyMetadata
|
|
||||||
GetDirectory() string
|
|
||||||
GetID() string
|
|
||||||
ID() string // Generate ID from the key's public key
|
|
||||||
Remove() error // Remove the unlock key and any associated resources
|
|
||||||
}
|
|
16
internal/secret/unlocker.go
Normal file
16
internal/secret/unlocker.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package secret
|
||||||
|
|
||||||
|
import (
|
||||||
|
"filippo.io/age"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Unlocker interface defines the methods all unlocker types must implement
|
||||||
|
type Unlocker interface {
|
||||||
|
GetIdentity() (*age.X25519Identity, error)
|
||||||
|
GetType() string
|
||||||
|
GetMetadata() UnlockerMetadata
|
||||||
|
GetDirectory() string
|
||||||
|
GetID() string
|
||||||
|
ID() string // Generate ID from the unlocker's public key
|
||||||
|
Remove() error // Remove the unlocker and any associated resources
|
||||||
|
}
|
@ -196,10 +196,10 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
|
|||||||
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
|
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create unlock keys directory
|
// Create unlockers directory
|
||||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||||
if err := fs.MkdirAll(unlockKeysDir, secret.DirPerms); err != nil {
|
if err := fs.MkdirAll(unlockersDir, secret.DirPerms); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create unlock keys directory: %w", err)
|
return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save initial vault metadata (without derivation info until a mnemonic is imported)
|
// Save initial vault metadata (without derivation info until a mnemonic is imported)
|
||||||
|
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
// Alias the metadata types from secret package for convenience
|
// Alias the metadata types from secret package for convenience
|
||||||
type VaultMetadata = secret.VaultMetadata
|
type VaultMetadata = secret.VaultMetadata
|
||||||
type UnlockKeyMetadata = secret.UnlockKeyMetadata
|
type UnlockerMetadata = secret.UnlockerMetadata
|
||||||
type SecretMetadata = secret.SecretMetadata
|
type SecretMetadata = secret.SecretMetadata
|
||||||
type Configuration = secret.Configuration
|
type Configuration = secret.Configuration
|
||||||
|
|
||||||
|
@ -1,376 +0,0 @@
|
|||||||
package vault
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"filippo.io/age"
|
|
||||||
"git.eeqj.de/sneak/secret/internal/secret"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetCurrentUnlockKey returns the current unlock key for this vault
|
|
||||||
func (v *Vault) GetCurrentUnlockKey() (secret.UnlockKey, error) {
|
|
||||||
secret.DebugWith("Getting current unlock key", slog.String("vault_name", v.Name))
|
|
||||||
|
|
||||||
vaultDir, err := v.GetDirectory()
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to get vault directory for unlock key", "error", err, "vault_name", v.Name)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
|
|
||||||
|
|
||||||
// Check if the symlink exists
|
|
||||||
_, err = v.fs.Stat(currentUnlockKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to stat current unlock key symlink", "error", err, "path", currentUnlockKeyPath)
|
|
||||||
return nil, fmt.Errorf("failed to read current unlock key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the symlink to get the target directory
|
|
||||||
var unlockKeyDir string
|
|
||||||
if _, ok := v.fs.(*afero.OsFs); ok {
|
|
||||||
secret.Debug("Resolving unlock key symlink (real filesystem)")
|
|
||||||
// For real filesystems, resolve the symlink properly
|
|
||||||
unlockKeyDir, err = ResolveVaultSymlink(v.fs, currentUnlockKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to resolve unlock key symlink", "error", err, "symlink_path", currentUnlockKeyPath)
|
|
||||||
return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
secret.Debug("Reading unlock key path (mock filesystem)")
|
|
||||||
// Fallback for mock filesystems: read the path from file contents
|
|
||||||
unlockKeyDirBytes, err := afero.ReadFile(v.fs, currentUnlockKeyPath)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to read unlock key path file", "error", err, "path", currentUnlockKeyPath)
|
|
||||||
return nil, fmt.Errorf("failed to read current unlock key: %w", err)
|
|
||||||
}
|
|
||||||
unlockKeyDir = strings.TrimSpace(string(unlockKeyDirBytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
secret.DebugWith("Resolved unlock key directory",
|
|
||||||
slog.String("unlock_key_dir", unlockKeyDir),
|
|
||||||
slog.String("vault_name", v.Name),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Read unlock key metadata
|
|
||||||
metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
|
|
||||||
secret.Debug("Reading unlock key metadata", "path", metadataPath)
|
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to read unlock key metadata", "error", err, "path", metadataPath)
|
|
||||||
return nil, fmt.Errorf("failed to read unlock key metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata UnlockKeyMetadata
|
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
|
||||||
secret.Debug("Failed to parse unlock key metadata", "error", err, "path", metadataPath)
|
|
||||||
return nil, fmt.Errorf("failed to parse unlock key metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret.DebugWith("Parsed unlock key metadata",
|
|
||||||
slog.String("key_id", metadata.ID),
|
|
||||||
slog.String("key_type", metadata.Type),
|
|
||||||
slog.Time("created_at", metadata.CreatedAt),
|
|
||||||
slog.Any("flags", metadata.Flags),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create unlock key instance using direct constructors with filesystem
|
|
||||||
var unlockKey secret.UnlockKey
|
|
||||||
// Convert our metadata to secret.UnlockKeyMetadata
|
|
||||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
|
||||||
switch metadata.Type {
|
|
||||||
case "passphrase":
|
|
||||||
secret.Debug("Creating passphrase unlock key instance", "key_id", metadata.ID)
|
|
||||||
unlockKey = secret.NewPassphraseUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
|
||||||
case "pgp":
|
|
||||||
secret.Debug("Creating PGP unlock key instance", "key_id", metadata.ID)
|
|
||||||
unlockKey = secret.NewPGPUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
|
||||||
case "keychain":
|
|
||||||
secret.Debug("Creating keychain unlock key instance", "key_id", metadata.ID)
|
|
||||||
unlockKey = secret.NewKeychainUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
|
||||||
default:
|
|
||||||
secret.Debug("Unsupported unlock key type", "type", metadata.Type, "key_id", metadata.ID)
|
|
||||||
return nil, fmt.Errorf("unsupported unlock key type: %s", metadata.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
secret.DebugWith("Successfully created unlock key instance",
|
|
||||||
slog.String("key_type", unlockKey.GetType()),
|
|
||||||
slog.String("key_id", unlockKey.GetID()),
|
|
||||||
slog.String("vault_name", v.Name),
|
|
||||||
)
|
|
||||||
|
|
||||||
return unlockKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListUnlockKeys returns a list of available unlock keys for this vault
|
|
||||||
func (v *Vault) ListUnlockKeys() ([]UnlockKeyMetadata, error) {
|
|
||||||
vaultDir, err := v.GetDirectory()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
|
||||||
|
|
||||||
// Check if unlock keys directory exists
|
|
||||||
exists, err := afero.DirExists(v.fs, unlockKeysDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check if unlock keys directory exists: %w", err)
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return []UnlockKeyMetadata{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// List directories in unlock.d
|
|
||||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read unlock keys directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys []UnlockKeyMetadata
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
// Read metadata file
|
|
||||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
|
||||||
exists, err := afero.Exists(v.fs, metadataPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata UnlockKeyMetadata
|
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
keys = append(keys, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveUnlockKey removes an unlock key from this vault
|
|
||||||
func (v *Vault) RemoveUnlockKey(keyID string) error {
|
|
||||||
vaultDir, err := v.GetDirectory()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the key directory and create the unlock key instance
|
|
||||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
|
||||||
|
|
||||||
// List directories in unlock.d
|
|
||||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read unlock keys directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var unlockKey secret.UnlockKey
|
|
||||||
var keyDir string
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
// Read metadata file
|
|
||||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
|
||||||
exists, err := afero.Exists(v.fs, metadataPath)
|
|
||||||
if err != nil || !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata UnlockKeyMetadata
|
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.ID == keyID {
|
|
||||||
keyDir = filepath.Join(unlockKeysDir, file.Name())
|
|
||||||
|
|
||||||
// Convert our metadata to secret.UnlockKeyMetadata
|
|
||||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
|
||||||
|
|
||||||
// Create the appropriate unlock key instance
|
|
||||||
switch metadata.Type {
|
|
||||||
case "passphrase":
|
|
||||||
unlockKey = secret.NewPassphraseUnlockKey(v.fs, keyDir, secretMetadata)
|
|
||||||
case "pgp":
|
|
||||||
unlockKey = secret.NewPGPUnlockKey(v.fs, keyDir, secretMetadata)
|
|
||||||
case "keychain":
|
|
||||||
unlockKey = secret.NewKeychainUnlockKey(v.fs, keyDir, secretMetadata)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported unlock key type: %s", metadata.Type)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if unlockKey == nil {
|
|
||||||
return fmt.Errorf("unlock key with ID %s not found", keyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the unlock key's Remove method
|
|
||||||
return unlockKey.Remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SelectUnlockKey selects an unlock key as current for this vault
|
|
||||||
func (v *Vault) SelectUnlockKey(keyID string) error {
|
|
||||||
vaultDir, err := v.GetDirectory()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the unlock key directory by ID
|
|
||||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
|
||||||
|
|
||||||
// List directories in unlock.d to find the key
|
|
||||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read unlock keys directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetKeyDir string
|
|
||||||
for _, file := range files {
|
|
||||||
if file.IsDir() {
|
|
||||||
// Read metadata file
|
|
||||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
|
||||||
exists, err := afero.Exists(v.fs, metadataPath)
|
|
||||||
if err != nil || !exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata UnlockKeyMetadata
|
|
||||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if metadata.ID == keyID {
|
|
||||||
targetKeyDir = filepath.Join(unlockKeysDir, file.Name())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetKeyDir == "" {
|
|
||||||
return fmt.Errorf("unlock key with ID %s not found", keyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create/update current unlock key symlink
|
|
||||||
currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
|
|
||||||
|
|
||||||
// Remove existing symlink if it exists
|
|
||||||
if exists, _ := afero.Exists(v.fs, currentUnlockKeyPath); exists {
|
|
||||||
if err := v.fs.Remove(currentUnlockKeyPath); err != nil {
|
|
||||||
secret.Debug("Failed to remove existing unlock key symlink", "error", err, "path", currentUnlockKeyPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new symlink
|
|
||||||
return afero.WriteFile(v.fs, currentUnlockKeyPath, []byte(targetKeyDir), secret.FilePerms)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
|
||||||
func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlockKey, error) {
|
|
||||||
vaultDir, err := v.GetDirectory()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create unlock key directory with timestamp
|
|
||||||
timestamp := time.Now().Format("2006-01-02.15.04")
|
|
||||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", "passphrase")
|
|
||||||
if err := v.fs.MkdirAll(unlockKeyDir, secret.DirPerms); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new age keypair for unlock key
|
|
||||||
unlockIdentity, err := age.GenerateX25519Identity()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to generate unlock key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write public key
|
|
||||||
pubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
|
||||||
if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write unlock key public key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt private key with passphrase
|
|
||||||
privKeyData := []byte(unlockIdentity.String())
|
|
||||||
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to encrypt unlock key private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write encrypted private key
|
|
||||||
privKeyPath := filepath.Join(unlockKeyDir, "priv.age")
|
|
||||||
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write encrypted unlock key private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create metadata
|
|
||||||
keyID := fmt.Sprintf("%s-passphrase", timestamp)
|
|
||||||
metadata := UnlockKeyMetadata{
|
|
||||||
ID: keyID,
|
|
||||||
Type: "passphrase",
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
Flags: []string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write metadata
|
|
||||||
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
|
|
||||||
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt long-term private key to this unlock key if vault is unlocked
|
|
||||||
if !v.Locked() {
|
|
||||||
ltPrivKey := []byte(v.GetLongTermKey().String())
|
|
||||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockIdentity.Recipient())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
|
||||||
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select this unlock key as current
|
|
||||||
if err := v.SelectUnlockKey(keyID); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to select new unlock key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert our metadata to secret.UnlockKeyMetadata for the constructor
|
|
||||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
|
||||||
|
|
||||||
return secret.NewPassphraseUnlockKey(v.fs, unlockKeyDir, secretMetadata), nil
|
|
||||||
}
|
|
376
internal/vault/unlockers.go
Normal file
376
internal/vault/unlockers.go
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"git.eeqj.de/sneak/secret/internal/secret"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCurrentUnlocker returns the current unlocker for this vault
|
||||||
|
func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
|
||||||
|
secret.DebugWith("Getting current unlocker", slog.String("vault_name", v.Name))
|
||||||
|
|
||||||
|
vaultDir, err := v.GetDirectory()
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to get vault directory for unlocker", "error", err, "vault_name", v.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||||
|
|
||||||
|
// Check if the symlink exists
|
||||||
|
_, err = v.fs.Stat(currentUnlockerPath)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to stat current unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||||
|
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the symlink to get the target directory
|
||||||
|
var unlockerDir string
|
||||||
|
if _, ok := v.fs.(*afero.OsFs); ok {
|
||||||
|
secret.Debug("Resolving unlocker symlink (real filesystem)")
|
||||||
|
// For real filesystems, resolve the symlink properly
|
||||||
|
unlockerDir, err = ResolveVaultSymlink(v.fs, currentUnlockerPath)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to resolve unlocker symlink", "error", err, "symlink_path", currentUnlockerPath)
|
||||||
|
return nil, fmt.Errorf("failed to resolve current unlocker symlink: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
secret.Debug("Reading unlocker path (mock filesystem)")
|
||||||
|
// Fallback for mock filesystems: read the path from file contents
|
||||||
|
unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to read unlocker path file", "error", err, "path", currentUnlockerPath)
|
||||||
|
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
|
||||||
|
}
|
||||||
|
unlockerDir = strings.TrimSpace(string(unlockerDirBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.DebugWith("Resolved unlocker directory",
|
||||||
|
slog.String("unlocker_dir", unlockerDir),
|
||||||
|
slog.String("vault_name", v.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Read unlocker metadata
|
||||||
|
metadataPath := filepath.Join(unlockerDir, "unlock-metadata.json")
|
||||||
|
secret.Debug("Reading unlocker metadata", "path", metadataPath)
|
||||||
|
|
||||||
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to read unlocker metadata", "error", err, "path", metadataPath)
|
||||||
|
return nil, fmt.Errorf("failed to read unlocker metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata UnlockerMetadata
|
||||||
|
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||||
|
secret.Debug("Failed to parse unlocker metadata", "error", err, "path", metadataPath)
|
||||||
|
return nil, fmt.Errorf("failed to parse unlocker metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.DebugWith("Parsed unlocker metadata",
|
||||||
|
slog.String("unlocker_id", metadata.ID),
|
||||||
|
slog.String("unlocker_type", metadata.Type),
|
||||||
|
slog.Time("created_at", metadata.CreatedAt),
|
||||||
|
slog.Any("flags", metadata.Flags),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create unlocker instance using direct constructors with filesystem
|
||||||
|
var unlocker secret.Unlocker
|
||||||
|
// Convert our metadata to secret.UnlockerMetadata
|
||||||
|
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||||
|
switch metadata.Type {
|
||||||
|
case "passphrase":
|
||||||
|
secret.Debug("Creating passphrase unlocker instance", "unlocker_id", metadata.ID)
|
||||||
|
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||||
|
case "pgp":
|
||||||
|
secret.Debug("Creating PGP unlocker instance", "unlocker_id", metadata.ID)
|
||||||
|
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||||
|
case "keychain":
|
||||||
|
secret.Debug("Creating keychain unlocker instance", "unlocker_id", metadata.ID)
|
||||||
|
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||||
|
default:
|
||||||
|
secret.Debug("Unsupported unlocker type", "type", metadata.Type, "unlocker_id", metadata.ID)
|
||||||
|
return nil, fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret.DebugWith("Successfully created unlocker instance",
|
||||||
|
slog.String("unlocker_type", unlocker.GetType()),
|
||||||
|
slog.String("unlocker_id", unlocker.GetID()),
|
||||||
|
slog.String("vault_name", v.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
return unlocker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUnlockers returns a list of available unlockers for this vault
|
||||||
|
func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||||
|
vaultDir, err := v.GetDirectory()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||||
|
|
||||||
|
// Check if unlockers directory exists
|
||||||
|
exists, err := afero.DirExists(v.fs, unlockersDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check if unlockers directory exists: %w", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return []UnlockerMetadata{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List directories in unlockers.d
|
||||||
|
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unlockers []UnlockerMetadata
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
// Read metadata file
|
||||||
|
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||||
|
exists, err := afero.Exists(v.fs, metadataPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata UnlockerMetadata
|
||||||
|
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
unlockers = append(unlockers, metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlockers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveUnlocker removes an unlocker from this vault
|
||||||
|
func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
||||||
|
vaultDir, err := v.GetDirectory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the unlocker directory and create the unlocker instance
|
||||||
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||||
|
|
||||||
|
// List directories in unlockers.d
|
||||||
|
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var unlocker secret.Unlocker
|
||||||
|
var unlockerDirPath string
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
// Read metadata file
|
||||||
|
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||||
|
exists, err := afero.Exists(v.fs, metadataPath)
|
||||||
|
if err != nil || !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata UnlockerMetadata
|
||||||
|
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.ID == unlockerID {
|
||||||
|
unlockerDirPath = filepath.Join(unlockersDir, file.Name())
|
||||||
|
|
||||||
|
// Convert our metadata to secret.UnlockerMetadata
|
||||||
|
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||||
|
|
||||||
|
// Create the appropriate unlocker instance
|
||||||
|
switch metadata.Type {
|
||||||
|
case "passphrase":
|
||||||
|
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||||
|
case "pgp":
|
||||||
|
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||||
|
case "keychain":
|
||||||
|
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if unlocker == nil {
|
||||||
|
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the unlocker's Remove method
|
||||||
|
return unlocker.Remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectUnlocker selects an unlocker as current for this vault
|
||||||
|
func (v *Vault) SelectUnlocker(unlockerID string) error {
|
||||||
|
vaultDir, err := v.GetDirectory()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the unlocker directory by ID
|
||||||
|
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||||
|
|
||||||
|
// List directories in unlockers.d to find the unlocker
|
||||||
|
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetUnlockerDir string
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
// Read metadata file
|
||||||
|
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||||
|
exists, err := afero.Exists(v.fs, metadataPath)
|
||||||
|
if err != nil || !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata UnlockerMetadata
|
||||||
|
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if metadata.ID == unlockerID {
|
||||||
|
targetUnlockerDir = filepath.Join(unlockersDir, file.Name())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetUnlockerDir == "" {
|
||||||
|
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create/update current unlocker symlink
|
||||||
|
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||||
|
|
||||||
|
// Remove existing symlink if it exists
|
||||||
|
if exists, _ := afero.Exists(v.fs, currentUnlockerPath); exists {
|
||||||
|
if err := v.fs.Remove(currentUnlockerPath); err != nil {
|
||||||
|
secret.Debug("Failed to remove existing unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new symlink
|
||||||
|
return afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||||
|
func (v *Vault) CreatePassphraseUnlocker(passphrase string) (*secret.PassphraseUnlocker, error) {
|
||||||
|
vaultDir, err := v.GetDirectory()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unlocker directory with timestamp
|
||||||
|
timestamp := time.Now().Format("2006-01-02.15.04")
|
||||||
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", "passphrase")
|
||||||
|
if err := v.fs.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new age keypair for unlocker
|
||||||
|
unlockerIdentity, err := age.GenerateX25519Identity()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate unlocker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write public key
|
||||||
|
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||||
|
if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockerIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write unlocker public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt private key with passphrase
|
||||||
|
privKeyData := []byte(unlockerIdentity.String())
|
||||||
|
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write encrypted private key
|
||||||
|
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||||
|
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create metadata
|
||||||
|
unlockerID := fmt.Sprintf("%s-passphrase", timestamp)
|
||||||
|
metadata := UnlockerMetadata{
|
||||||
|
ID: unlockerID,
|
||||||
|
Type: "passphrase",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Flags: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write metadata
|
||||||
|
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataPath := filepath.Join(unlockerDir, "unlock-metadata.json")
|
||||||
|
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt long-term private key to this unlocker if vault is unlocked
|
||||||
|
if !v.Locked() {
|
||||||
|
ltPrivKey := []byte(v.GetLongTermKey().String())
|
||||||
|
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockerIdentity.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||||
|
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select this unlocker as current
|
||||||
|
if err := v.SelectUnlocker(unlockerID); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to select new unlocker: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert our metadata to secret.UnlockerMetadata for the constructor
|
||||||
|
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||||
|
|
||||||
|
return secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata), nil
|
||||||
|
}
|
@ -83,32 +83,32 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
|||||||
return ltIdentity, nil
|
return ltIdentity, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// No mnemonic available, try to use current unlock key
|
// No mnemonic available, try to use current unlocker
|
||||||
secret.Debug("No mnemonic available, using current unlock key to unlock vault", "vault_name", v.Name)
|
secret.Debug("No mnemonic available, using current unlocker to unlock vault", "vault_name", v.Name)
|
||||||
|
|
||||||
// Get current unlock key
|
// Get current unlocker
|
||||||
unlockKey, err := v.GetCurrentUnlockKey()
|
unlocker, err := v.GetCurrentUnlocker()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to get current unlock key", "error", err, "vault_name", v.Name)
|
secret.Debug("Failed to get current unlocker", "error", err, "vault_name", v.Name)
|
||||||
return nil, fmt.Errorf("failed to get current unlock key: %w", err)
|
return nil, fmt.Errorf("failed to get current unlocker: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.DebugWith("Retrieved current unlock key for vault unlock",
|
secret.DebugWith("Retrieved current unlocker for vault unlock",
|
||||||
slog.String("vault_name", v.Name),
|
slog.String("vault_name", v.Name),
|
||||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
slog.String("unlocker_type", unlocker.GetType()),
|
||||||
slog.String("unlock_key_id", unlockKey.GetID()),
|
slog.String("unlocker_id", unlocker.GetID()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get unlock key identity
|
// Get unlocker identity
|
||||||
unlockIdentity, err := unlockKey.GetIdentity()
|
unlockerIdentity, err := unlocker.GetIdentity()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to get unlock key identity", "error", err, "unlock_key_type", unlockKey.GetType())
|
secret.Debug("Failed to get unlocker identity", "error", err, "unlocker_type", unlocker.GetType())
|
||||||
return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
|
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read encrypted long-term private key from unlock key directory
|
// Read encrypted long-term private key from unlocker directory
|
||||||
unlockKeyDir := unlockKey.GetDirectory()
|
unlockerDir := unlocker.GetDirectory()
|
||||||
encryptedLtPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
encryptedLtPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||||
secret.Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
secret.Debug("Reading encrypted long-term private key", "path", encryptedLtPrivKeyPath)
|
||||||
|
|
||||||
encryptedLtPrivKey, err := afero.ReadFile(v.fs, encryptedLtPrivKeyPath)
|
encryptedLtPrivKey, err := afero.ReadFile(v.fs, encryptedLtPrivKeyPath)
|
||||||
@ -119,21 +119,21 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
|||||||
|
|
||||||
secret.DebugWith("Read encrypted long-term private key",
|
secret.DebugWith("Read encrypted long-term private key",
|
||||||
slog.String("vault_name", v.Name),
|
slog.String("vault_name", v.Name),
|
||||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
slog.String("unlocker_type", unlocker.GetType()),
|
||||||
slog.Int("encrypted_length", len(encryptedLtPrivKey)),
|
slog.Int("encrypted_length", len(encryptedLtPrivKey)),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decrypt long-term private key using unlock key
|
// Decrypt long-term private key using unlocker
|
||||||
secret.Debug("Decrypting long-term private key with unlock key", "unlock_key_type", unlockKey.GetType())
|
secret.Debug("Decrypting long-term private key with unlocker", "unlocker_type", unlocker.GetType())
|
||||||
ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
|
ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockerIdentity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
secret.Debug("Failed to decrypt long-term private key", "error", err, "unlock_key_type", unlockKey.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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("unlock_key_type", unlockKey.GetType()),
|
slog.String("unlocker_type", unlocker.GetType()),
|
||||||
slog.Int("decrypted_length", len(ltPrivKeyData)),
|
slog.Int("decrypted_length", len(ltPrivKeyData)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -145,15 +145,15 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
|||||||
return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
|
return nil, fmt.Errorf("failed to parse long-term private key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secret.DebugWith("Successfully obtained long-term identity via unlock key",
|
secret.DebugWith("Successfully obtained long-term identity via unlocker",
|
||||||
slog.String("vault_name", v.Name),
|
slog.String("vault_name", v.Name),
|
||||||
slog.String("unlock_key_type", unlockKey.GetType()),
|
slog.String("unlocker_type", unlocker.GetType()),
|
||||||
slog.String("public_key", ltIdentity.Recipient().String()),
|
slog.String("public_key", ltIdentity.Recipient().String()),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cache the derived key by unlocking the vault
|
// Cache the derived key by unlocking the vault
|
||||||
v.Unlock(ltIdentity)
|
v.Unlock(ltIdentity)
|
||||||
secret.Debug("Vault is unlocked (lt key in memory) via unlock key", "vault_name", v.Name, "unlock_key_type", unlockKey.GetType())
|
secret.Debug("Vault is unlocked (lt key in memory) via unlocker", "vault_name", v.Name, "unlocker_type", unlocker.GetType())
|
||||||
|
|
||||||
return ltIdentity, nil
|
return ltIdentity, nil
|
||||||
}
|
}
|
||||||
|
@ -174,8 +174,8 @@ func TestVaultOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test unlock key operations
|
// Test unlocker operations
|
||||||
t.Run("UnlockKeyOperations", func(t *testing.T) {
|
t.Run("UnlockerOperations", func(t *testing.T) {
|
||||||
vlt, err := GetCurrentVault(fs, stateDir)
|
vlt, err := GetCurrentVault(fs, stateDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get current vault: %v", err)
|
t.Fatalf("Failed to get current vault: %v", err)
|
||||||
@ -189,25 +189,25 @@ func TestVaultOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a passphrase unlock key
|
// Create a passphrase unlocker
|
||||||
passphraseKey, err := vlt.CreatePassphraseKey("test-passphrase")
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker("test-passphrase")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create passphrase key: %v", err)
|
t.Fatalf("Failed to create passphrase unlocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List unlock keys
|
// List unlockers
|
||||||
keys, err := vlt.ListUnlockKeys()
|
unlockers, err := vlt.ListUnlockers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to list unlock keys: %v", err)
|
t.Fatalf("Failed to list unlockers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(keys) == 0 {
|
if len(unlockers) == 0 {
|
||||||
t.Errorf("Expected at least one unlock key")
|
t.Errorf("Expected at least one unlocker")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check key type
|
// Check key type
|
||||||
keyFound := false
|
keyFound := false
|
||||||
for _, key := range keys {
|
for _, key := range unlockers {
|
||||||
if key.Type == "passphrase" {
|
if key.Type == "passphrase" {
|
||||||
keyFound = true
|
keyFound = true
|
||||||
break
|
break
|
||||||
@ -215,23 +215,23 @@ func TestVaultOperations(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !keyFound {
|
if !keyFound {
|
||||||
t.Errorf("Expected to find passphrase unlock key")
|
t.Errorf("Expected to find passphrase unlocker")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test selecting unlock key
|
// Test selecting unlocker
|
||||||
err = vlt.SelectUnlockKey(passphraseKey.GetID())
|
err = vlt.SelectUnlocker(passphraseUnlocker.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to select unlock key: %v", err)
|
t.Fatalf("Failed to select unlocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test getting current unlock key
|
// Test getting current unlocker
|
||||||
currentKey, err := vlt.GetCurrentUnlockKey()
|
currentUnlocker, err := vlt.GetCurrentUnlocker()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to get current unlock key: %v", err)
|
t.Fatalf("Failed to get current unlocker: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentKey.GetID() != passphraseKey.GetID() {
|
if currentUnlocker.GetID() != passphraseUnlocker.GetID() {
|
||||||
t.Errorf("Expected current unlock key ID '%s', got '%s'", passphraseKey.GetID(), currentKey.GetID())
|
t.Errorf("Expected current unlocker ID '%s', got '%s'", passphraseUnlocker.GetID(), currentUnlocker.GetID())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -104,8 +104,19 @@ echo " SB_SECRET_MNEMONIC=$TEST_MNEMONIC"
|
|||||||
# Test 2: Initialize the secret manager (should create default vault)
|
# Test 2: Initialize the secret manager (should create default vault)
|
||||||
print_step "2" "Initializing secret manager (creates default vault)"
|
print_step "2" "Initializing secret manager (creates default vault)"
|
||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
echo " SB_UNLOCK_PASSPHRASE=$SB_UNLOCK_PASSPHRASE"
|
||||||
|
|
||||||
|
# Verify environment variables are exported and visible to subprocesses
|
||||||
|
echo "Verifying environment variables are exported:"
|
||||||
|
env | grep -E "^SB_" || true
|
||||||
|
|
||||||
echo "Running: $SECRET_BINARY init"
|
echo "Running: $SECRET_BINARY init"
|
||||||
if $SECRET_BINARY init; then
|
# Run with explicit environment to ensure variables are passed
|
||||||
|
if SB_SECRET_STATE_DIR="$SB_SECRET_STATE_DIR" \
|
||||||
|
SB_SECRET_MNEMONIC="$SB_SECRET_MNEMONIC" \
|
||||||
|
SB_UNLOCK_PASSPHRASE="$SB_UNLOCK_PASSPHRASE" \
|
||||||
|
GODEBUG="$GODEBUG" \
|
||||||
|
$SECRET_BINARY init </dev/null; then
|
||||||
print_success "Secret manager initialized with default vault"
|
print_success "Secret manager initialized with default vault"
|
||||||
else
|
else
|
||||||
print_error "Failed to initialize secret manager"
|
print_error "Failed to initialize secret manager"
|
||||||
@ -229,42 +240,45 @@ fi
|
|||||||
reset_state
|
reset_state
|
||||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
# Test 5: Unlock key management
|
# Test 5: Unlocker management
|
||||||
print_step "5" "Testing unlock key management"
|
print_step "5" "Testing unlocker management"
|
||||||
|
|
||||||
# Initialize to create default vault
|
# Initialize with mnemonic and passphrase
|
||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
echo "Running: $SECRET_BINARY init (with SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set)"
|
||||||
if $SECRET_BINARY init; then
|
if $SECRET_BINARY init; then
|
||||||
print_success "Initialized for unlock key testing"
|
print_success "Initialized for unlocker testing"
|
||||||
else
|
else
|
||||||
print_error "Failed to initialize for unlock key testing"
|
print_error "Failed to initialize for unlocker testing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create passphrase-protected unlock key
|
# Create passphrase-protected unlocker
|
||||||
echo "Creating passphrase-protected unlock key..."
|
echo "Creating passphrase-protected unlocker..."
|
||||||
echo "Running: $SECRET_BINARY keys add passphrase (with SB_UNLOCK_PASSPHRASE set)"
|
echo "Running: $SECRET_BINARY unlockers add passphrase (with SB_UNLOCK_PASSPHRASE set)"
|
||||||
if $SECRET_BINARY keys add passphrase; then
|
if $SECRET_BINARY unlockers add passphrase; then
|
||||||
print_success "Created passphrase-protected unlock key"
|
print_success "Created passphrase-protected unlocker"
|
||||||
else
|
else
|
||||||
print_error "Failed to create passphrase-protected unlock key"
|
print_error "Failed to create passphrase-protected unlocker"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
unset SB_UNLOCK_PASSPHRASE
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
# List unlock keys
|
# List unlockers
|
||||||
echo "Listing unlock keys..."
|
echo "Listing unlockers..."
|
||||||
echo "Running: $SECRET_BINARY keys list"
|
echo "Running: $SECRET_BINARY unlockers list"
|
||||||
if $SECRET_BINARY keys list; then
|
if $SECRET_BINARY unlockers list; then
|
||||||
KEYS=$($SECRET_BINARY keys list)
|
UNLOCKERS=$($SECRET_BINARY unlockers list)
|
||||||
echo "Available unlock keys: $KEYS"
|
echo "Available unlockers: $UNLOCKERS"
|
||||||
print_success "Listed unlock keys"
|
print_success "Listed unlockers"
|
||||||
else
|
else
|
||||||
print_error "Failed to list unlock keys"
|
print_error "Failed to list unlockers"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 6: Secret management with mnemonic (keyless operation)
|
# Test 6: Secret management with mnemonic (keyless operation)
|
||||||
print_step "6" "Testing mnemonic-based secret operations (keyless)"
|
print_step "6" "Testing mnemonic-based secret operations (keyless)"
|
||||||
|
|
||||||
# Add secrets using mnemonic (no unlock key required)
|
# Add secrets using mnemonic (no unlocker required)
|
||||||
echo "Adding secrets using mnemonic-based long-term key..."
|
echo "Adding secrets using mnemonic-based long-term key..."
|
||||||
|
|
||||||
# Test secret 1
|
# Test secret 1
|
||||||
@ -340,66 +354,59 @@ else
|
|||||||
print_error "Failed to list secrets"
|
print_error "Failed to list secrets"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test 7: Secret management without mnemonic (traditional unlock key approach)
|
# Test 7: Secret management without mnemonic (traditional unlocker approach)
|
||||||
print_step "7" "Testing traditional unlock key approach"
|
print_step "7" "Testing traditional unlocker approach"
|
||||||
|
|
||||||
# Temporarily unset mnemonic to test traditional approach
|
# Create a new vault without mnemonic
|
||||||
unset SB_SECRET_MNEMONIC
|
echo "Running: $SECRET_BINARY vault create traditional"
|
||||||
|
$SECRET_BINARY vault create traditional
|
||||||
|
|
||||||
# Add a secret using traditional unlock key approach
|
# Add a secret using traditional unlocker approach
|
||||||
echo "Adding secret using traditional unlock key..."
|
echo "Adding secret using traditional unlocker..."
|
||||||
echo "Running: echo \"traditional-secret-value\" | $SECRET_BINARY add \"traditional/secret\""
|
echo "Running: echo 'traditional-secret' | $SECRET_BINARY add traditional/secret"
|
||||||
if echo "traditional-secret-value" | $SECRET_BINARY add "traditional/secret"; then
|
if echo "traditional-secret" | $SECRET_BINARY add traditional/secret; then
|
||||||
print_success "Added secret using traditional approach: traditional/secret"
|
print_success "Added secret with traditional approach"
|
||||||
else
|
else
|
||||||
print_error "Failed to add secret using traditional approach"
|
print_error "Failed to add secret with traditional approach"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Retrieve secret using traditional unlock key approach
|
# Retrieve secret using traditional unlocker approach
|
||||||
echo "Retrieving secret using traditional unlock key approach..."
|
echo "Retrieving secret using traditional unlocker approach..."
|
||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
echo "Running: $SECRET_BINARY get traditional/secret"
|
||||||
RETRIEVED_TRADITIONAL=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
|
if RETRIEVED=$($SECRET_BINARY get traditional/secret 2>&1); then
|
||||||
unset SB_UNLOCK_PASSPHRASE
|
print_success "Retrieved: $RETRIEVED"
|
||||||
if [ "$RETRIEVED_TRADITIONAL" = "traditional-secret-value" ]; then
|
|
||||||
print_success "Retrieved and verified traditional secret: traditional/secret"
|
|
||||||
else
|
else
|
||||||
print_error "Failed to retrieve or verify traditional secret"
|
print_error "Failed to retrieve secret with traditional approach"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Re-enable mnemonic for remaining tests
|
# Test 8: Advanced unlocker management
|
||||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
print_step "8" "Testing advanced unlocker management"
|
||||||
|
|
||||||
# Test 8: Advanced unlock key management
|
if [ "$PLATFORM" = "darwin" ]; then
|
||||||
print_step "8" "Testing advanced unlock key management"
|
# macOS only: Test Secure Enclave
|
||||||
|
echo "Testing Secure Enclave unlocker creation..."
|
||||||
# Test Secure Enclave (macOS only)
|
if $SECRET_BINARY unlockers add sep; then
|
||||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
print_success "Created Secure Enclave unlocker"
|
||||||
echo "Testing Secure Enclave unlock key creation..."
|
|
||||||
echo "Running: $SECRET_BINARY enroll sep"
|
|
||||||
if $SECRET_BINARY enroll sep; then
|
|
||||||
print_success "Created Secure Enclave unlock key"
|
|
||||||
else
|
else
|
||||||
print_warning "Secure Enclave unlock key creation not yet implemented"
|
print_warning "Secure Enclave unlocker creation not yet implemented"
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
print_warning "Secure Enclave only available on macOS"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Get current unlock key ID for testing
|
# Get current unlocker ID for testing
|
||||||
echo "Getting current unlock key for testing..."
|
echo "Getting current unlocker for testing..."
|
||||||
echo "Running: $SECRET_BINARY keys list"
|
echo "Running: $SECRET_BINARY unlockers list"
|
||||||
if $SECRET_BINARY keys list; then
|
if $SECRET_BINARY unlockers list; then
|
||||||
CURRENT_KEY_ID=$($SECRET_BINARY keys list | head -n1 | awk '{print $1}')
|
CURRENT_UNLOCKER_ID=$($SECRET_BINARY unlockers list | head -n1 | awk '{print $1}')
|
||||||
if [ -n "$CURRENT_KEY_ID" ]; then
|
if [ -n "$CURRENT_UNLOCKER_ID" ]; then
|
||||||
print_success "Found unlock key ID: $CURRENT_KEY_ID"
|
print_success "Found unlocker ID: $CURRENT_UNLOCKER_ID"
|
||||||
|
|
||||||
# Test key selection
|
# Test unlocker selection
|
||||||
echo "Testing unlock key selection..."
|
echo "Testing unlocker selection..."
|
||||||
echo "Running: $SECRET_BINARY key select $CURRENT_KEY_ID"
|
echo "Running: $SECRET_BINARY unlocker select $CURRENT_UNLOCKER_ID"
|
||||||
if $SECRET_BINARY key select "$CURRENT_KEY_ID"; then
|
if $SECRET_BINARY unlocker select "$CURRENT_UNLOCKER_ID"; then
|
||||||
print_success "Selected unlock key: $CURRENT_KEY_ID"
|
print_success "Selected unlocker: $CURRENT_UNLOCKER_ID"
|
||||||
else
|
else
|
||||||
print_warning "Unlock key selection not yet implemented"
|
print_warning "Unlocker selection not yet implemented"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@ -609,17 +616,15 @@ else
|
|||||||
print_error "Mnemonic cannot access traditional secrets"
|
print_error "Mnemonic cannot access traditional secrets"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Test without mnemonic but with unlock key
|
# Test without mnemonic but with unlocker
|
||||||
unset SB_SECRET_MNEMONIC
|
echo "Testing mnemonic-created vault access..."
|
||||||
echo "Testing traditional unlock key access to mnemonic-created secrets..."
|
echo "Testing traditional unlocker access to mnemonic-created secrets..."
|
||||||
echo "Running: $SECRET_BINARY get \"database/password\" (with SB_UNLOCK_PASSPHRASE set)"
|
echo "Running: $SECRET_BINARY get test/seed (with mnemonic set)"
|
||||||
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
if RETRIEVED=$($SECRET_BINARY get test/seed 2>&1); then
|
||||||
if $SECRET_BINARY get "database/password"; then
|
print_success "Traditional unlocker can access mnemonic-created secrets"
|
||||||
print_success "Traditional unlock key can access mnemonic-created secrets"
|
|
||||||
else
|
else
|
||||||
print_warning "Traditional unlock key cannot access mnemonic-created secrets (may need implementation)"
|
print_warning "Traditional unlocker cannot access mnemonic-created secrets (may need implementation)"
|
||||||
fi
|
fi
|
||||||
unset SB_UNLOCK_PASSPHRASE
|
|
||||||
|
|
||||||
# Re-enable mnemonic for final tests
|
# Re-enable mnemonic for final tests
|
||||||
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
@ -631,9 +636,9 @@ echo -e "${GREEN}✓ Secret manager initialization${NC}"
|
|||||||
echo -e "${GREEN}✓ Vault management (create, list, select)${NC}"
|
echo -e "${GREEN}✓ Vault management (create, list, select)${NC}"
|
||||||
echo -e "${GREEN}✓ Import functionality with environment variable combinations${NC}"
|
echo -e "${GREEN}✓ Import functionality with environment variable combinations${NC}"
|
||||||
echo -e "${GREEN}✓ Import error handling (non-existent vault, invalid mnemonic)${NC}"
|
echo -e "${GREEN}✓ Import error handling (non-existent vault, invalid mnemonic)${NC}"
|
||||||
echo -e "${GREEN}✓ Unlock key management (passphrase, PGP, SEP)${NC}"
|
echo -e "${GREEN}✓ Unlocker management (passphrase, PGP, SEP)${NC}"
|
||||||
echo -e "${GREEN}✓ Mnemonic-based secret operations (keyless)${NC}"
|
echo -e "${GREEN}✓ Secret generation and storage${NC}"
|
||||||
echo -e "${GREEN}✓ Traditional unlock key operations${NC}"
|
echo -e "${GREEN}✓ Traditional unlocker operations${NC}"
|
||||||
echo -e "${GREEN}✓ Secret name validation${NC}"
|
echo -e "${GREEN}✓ Secret name validation${NC}"
|
||||||
echo -e "${GREEN}✓ Overwrite protection and force flag${NC}"
|
echo -e "${GREEN}✓ Overwrite protection and force flag${NC}"
|
||||||
echo -e "${GREEN}✓ Cross-vault operations${NC}"
|
echo -e "${GREEN}✓ Cross-vault operations${NC}"
|
||||||
@ -662,14 +667,14 @@ echo "export SB_SECRET_MNEMONIC=\"abandon abandon...\""
|
|||||||
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
||||||
echo "secret vault import work"
|
echo "secret vault import work"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}# Unlock key management:${NC}"
|
echo -e "${YELLOW}# Unlocker management:${NC}"
|
||||||
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
echo "$SECRET_BINARY unlockers add <type> # Add unlocker (passphrase, pgp, keychain)"
|
||||||
echo "secret keys add passphrase"
|
echo "$SECRET_BINARY unlockers add passphrase"
|
||||||
echo "secret keys add pgp <gpg-key-id>"
|
echo "$SECRET_BINARY unlockers add pgp <gpg-key-id>"
|
||||||
echo "secret enroll sep # macOS only"
|
echo "$SECRET_BINARY unlockers add keychain # macOS only"
|
||||||
echo "secret keys list"
|
echo "$SECRET_BINARY unlockers list # List all unlockers"
|
||||||
echo "secret key select <key-id>"
|
echo "$SECRET_BINARY unlocker select <unlocker-id> # Select current unlocker"
|
||||||
echo "secret keys rm <key-id>"
|
echo "$SECRET_BINARY unlockers rm <unlocker-id> # Remove unlocker"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}# Secret management:${NC}"
|
echo -e "${YELLOW}# Secret management:${NC}"
|
||||||
echo "echo \"my-secret\" | secret add \"app/password\""
|
echo "echo \"my-secret\" | secret add \"app/password\""
|
||||||
|
Loading…
Reference in New Issue
Block a user