'unlock keys' renamed to 'unlockers'

This commit is contained in:
2025-05-30 07:29:02 -07:00
parent 0bf8e71b52
commit f59ee4d2d6
25 changed files with 1115 additions and 1103 deletions

View File

@@ -74,11 +74,11 @@ func (cli *CLIInstance) Encrypt(secretName, inputFile, outputFile string) error
if os.Getenv(secret.EnvMnemonic) != "" {
secretValue, err = secretObj.GetValue(nil)
} else {
unlockKey, unlockErr := vlt.GetCurrentUnlockKey()
unlocker, unlockErr := vlt.GetCurrentUnlocker()
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 {
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) != "" {
secretValue, err = secretObj.GetValue(nil)
} else {
unlockKey, unlockErr := vlt.GetCurrentUnlockKey()
unlocker, unlockErr := vlt.GetCurrentUnlocker()
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 {
return fmt.Errorf("failed to get secret value: %w", err)

View File

@@ -140,7 +140,7 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
// Unlock the vault with the derived long-term key
vlt.Unlock(ltIdentity)
// Prompt for passphrase for unlock key
// Prompt for passphrase for unlocker
var passphraseStr string
if envPassphrase := os.Getenv(secret.EnvUnlockPassphrase); envPassphrase != "" {
secret.Debug("Using unlock passphrase from environment variable")
@@ -148,61 +148,61 @@ func (cli *CLIInstance) Init(cmd *cobra.Command) error {
} else {
secret.Debug("Prompting user for unlock passphrase")
// Use secure passphrase input with confirmation
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ")
if err != nil {
secret.Debug("Failed to read unlock passphrase", "error", err)
return fmt.Errorf("failed to read passphrase: %w", err)
}
}
// Create passphrase-protected unlock key
secret.Debug("Creating passphrase-protected unlock key")
passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr)
// Create passphrase-protected unlocker
secret.Debug("Creating passphrase-protected unlocker")
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
if err != nil {
secret.Debug("Failed to create unlock key", "error", err)
return fmt.Errorf("failed to create unlock key: %w", err)
secret.Debug("Failed to create unlocker", "error", err)
return fmt.Errorf("failed to create unlocker: %w", err)
}
// Encrypt long-term private key to the unlock key
unlockKeyDir := passphraseKey.GetDirectory()
// Encrypt long-term private key to the unlocker
unlockerDir := passphraseUnlocker.GetDirectory()
// Read unlock key public key
unlockPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockKeyDir, "pub.age"))
// Read unlocker public key
unlockerPubKeyData, err := afero.ReadFile(cli.fs, filepath.Join(unlockerDir, "pub.age"))
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 {
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())
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, unlockRecipient)
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKeyData, unlockerRecipient)
if err != nil {
return fmt.Errorf("failed to encrypt long-term private key: %w", err)
}
// 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)
}
if cmd != nil {
cmd.Printf("\nDefault vault created and configured\n")
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("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
}
// 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) {
// Get the first passphrase
passphrase1, err := secret.ReadPassphrase(prompt)

View File

@@ -35,8 +35,8 @@ func newRootCmd() *cobra.Command {
cmd.AddCommand(newAddCmd())
cmd.AddCommand(newGetCmd())
cmd.AddCommand(newListCmd())
cmd.AddCommand(newKeysCmd())
cmd.AddCommand(newKeyCmd())
cmd.AddCommand(newUnlockersCmd())
cmd.AddCommand(newUnlockerCmd())
cmd.AddCommand(newImportCmd())
cmd.AddCommand(newEncryptCmd())
cmd.AddCommand(newDecryptCmd())

View File

@@ -18,29 +18,29 @@ import (
// ... existing imports ...
func newKeysCmd() *cobra.Command {
func newUnlockersCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "keys",
Short: "Manage unlock keys",
Long: `Create, list, and remove unlock keys for the current vault.`,
Use: "unlockers",
Short: "Manage unlockers",
Long: `Create, list, and remove unlockers for the current vault.`,
}
cmd.AddCommand(newKeysListCmd())
cmd.AddCommand(newKeysAddCmd())
cmd.AddCommand(newKeysRmCmd())
cmd.AddCommand(newUnlockersListCmd())
cmd.AddCommand(newUnlockersAddCmd())
cmd.AddCommand(newUnlockersRmCmd())
return cmd
}
func newKeysListCmd() *cobra.Command {
func newUnlockersListCmd() *cobra.Command {
cmd := &cobra.Command{
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 {
jsonOutput, _ := cmd.Flags().GetBool("json")
cli := NewCLIInstance()
return cli.KeysList(jsonOutput)
return cli.UnlockersList(jsonOutput)
},
}
@@ -48,60 +48,60 @@ func newKeysListCmd() *cobra.Command {
return cmd
}
func newKeysAddCmd() *cobra.Command {
func newUnlockersAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add <type>",
Short: "Add a new unlock key",
Long: `Add a new unlock key of the specified type (passphrase, keychain, pgp).`,
Short: "Add a new unlocker",
Long: `Add a new unlocker of the specified type (passphrase, keychain, pgp).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
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
}
func newKeysRmCmd() *cobra.Command {
func newUnlockersRmCmd() *cobra.Command {
return &cobra.Command{
Use: "rm <key-id>",
Short: "Remove an unlock key",
Use: "rm <unlocker-id>",
Short: "Remove an unlocker",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cli := NewCLIInstance()
return cli.KeysRemove(args[0])
return cli.UnlockersRemove(args[0])
},
}
}
func newKeyCmd() *cobra.Command {
func newUnlockerCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage current unlock key",
Long: `Select the current unlock key for operations.`,
Use: "unlocker",
Short: "Manage current unlocker",
Long: `Select the current unlocker for operations.`,
}
cmd.AddCommand(newKeySelectSubCmd())
cmd.AddCommand(newUnlockerSelectSubCmd())
return cmd
}
func newKeySelectSubCmd() *cobra.Command {
func newUnlockerSelectSubCmd() *cobra.Command {
return &cobra.Command{
Use: "select <key-id>",
Short: "Select an unlock key as current",
Use: "select <unlocker-id>",
Short: "Select an unlocker as current",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cli := NewCLIInstance()
return cli.KeySelect(args[0])
return cli.UnlockerSelect(args[0])
},
}
}
// KeysList lists unlock keys in the current vault
func (cli *CLIInstance) KeysList(jsonOutput bool) error {
// UnlockersList lists unlockers in the current vault
func (cli *CLIInstance) UnlockersList(jsonOutput bool) error {
// Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil {
@@ -109,90 +109,90 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
}
// Get the metadata first
keyMetadataList, err := vlt.ListUnlockKeys()
unlockerMetadataList, err := vlt.ListUnlockers()
if err != nil {
return err
}
// Load actual unlock key objects to get the proper IDs
type KeyInfo struct {
// Load actual unlocker objects to get the proper IDs
type UnlockerInfo struct {
ID string `json:"id"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
Flags []string `json:"flags,omitempty"`
}
var keys []KeyInfo
for _, metadata := range keyMetadataList {
// Create unlock key instance to get the proper ID
var unlockers []UnlockerInfo
for _, metadata := range unlockerMetadataList {
// Create unlocker instance to get the proper ID
vaultDir, err := vlt.GetDirectory()
if err != nil {
continue
}
// Find the key directory by type and created time
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
files, err := afero.ReadDir(cli.fs, unlockKeysDir)
// Find the unlocker directory by type and created time
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
files, err := afero.ReadDir(cli.fs, unlockersDir)
if err != nil {
continue
}
var unlockKey secret.UnlockKey
var unlocker secret.Unlocker
for _, file := range files {
if !file.IsDir() {
continue
}
keyDir := filepath.Join(unlockKeysDir, file.Name())
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
unlockerDir := filepath.Join(unlockersDir, file.Name())
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)
if err != nil {
continue
}
var diskMetadata secret.UnlockKeyMetadata
var diskMetadata secret.UnlockerMetadata
if err := json.Unmarshal(metadataBytes, &diskMetadata); err != nil {
continue
}
// Match by type and creation time
if diskMetadata.Type == metadata.Type && diskMetadata.CreatedAt.Equal(metadata.CreatedAt) {
// Create the appropriate unlock key instance
// Create the appropriate unlocker instance
switch metadata.Type {
case "passphrase":
unlockKey = secret.NewPassphraseUnlockKey(cli.fs, keyDir, diskMetadata)
unlocker = secret.NewPassphraseUnlocker(cli.fs, unlockerDir, diskMetadata)
case "keychain":
unlockKey = secret.NewKeychainUnlockKey(cli.fs, keyDir, diskMetadata)
unlocker = secret.NewKeychainUnlocker(cli.fs, unlockerDir, diskMetadata)
case "pgp":
unlockKey = secret.NewPGPUnlockKey(cli.fs, keyDir, diskMetadata)
unlocker = secret.NewPGPUnlocker(cli.fs, unlockerDir, diskMetadata)
}
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
if unlockKey != nil {
properID = unlockKey.GetID()
if unlocker != nil {
properID = unlocker.GetID()
} else {
properID = metadata.ID // fallback to metadata ID
}
keyInfo := KeyInfo{
unlockerInfo := UnlockerInfo{
ID: properID,
Type: metadata.Type,
CreatedAt: metadata.CreatedAt,
Flags: metadata.Flags,
}
keys = append(keys, keyInfo)
unlockers = append(unlockers, unlockerInfo)
}
if jsonOutput {
// JSON output
output := map[string]interface{}{
"keys": keys,
"unlockers": unlockers,
}
jsonBytes, err := json.MarshalIndent(output, "", " ")
@@ -203,36 +203,36 @@ func (cli *CLIInstance) KeysList(jsonOutput bool) error {
fmt.Println(string(jsonBytes))
} else {
// Pretty table output
if len(keys) == 0 {
fmt.Println("No unlock keys found in current vault.")
fmt.Println("Run 'secret keys add passphrase' to create one.")
if len(unlockers) == 0 {
fmt.Println("No unlockers found in current vault.")
fmt.Println("Run 'secret unlockers add passphrase' to create one.")
return nil
}
fmt.Printf("%-18s %-12s %-20s %s\n", "KEY ID", "TYPE", "CREATED", "FLAGS")
fmt.Printf("%-18s %-12s %-20s %s\n", "------", "----", "-------", "-----")
fmt.Printf("%-18s %-12s %-20s %s\n", "UNLOCKER ID", "TYPE", "CREATED", "FLAGS")
fmt.Printf("%-18s %-12s %-20s %s\n", "-----------", "----", "-------", "-----")
for _, key := range keys {
for _, unlocker := range unlockers {
flags := ""
if len(key.Flags) > 0 {
flags = strings.Join(key.Flags, ",")
if len(unlocker.Flags) > 0 {
flags = strings.Join(unlocker.Flags, ",")
}
fmt.Printf("%-18s %-12s %-20s %s\n",
key.ID,
key.Type,
key.CreatedAt.Format("2006-01-02 15:04:05"),
unlocker.ID,
unlocker.Type,
unlocker.CreatedAt.Format("2006-01-02 15:04:05"),
flags)
}
fmt.Printf("\nTotal: %d unlock key(s)\n", len(keys))
fmt.Printf("\nTotal: %d unlocker(s)\n", len(unlockers))
}
return nil
}
// KeysAdd adds a new unlock key
func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
switch keyType {
// UnlockersAdd adds a new unlocker
func (cli *CLIInstance) UnlockersAdd(unlockerType string, cmd *cobra.Command) error {
switch unlockerType {
case "passphrase":
// Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
@@ -254,28 +254,28 @@ func (cli *CLIInstance) KeysAdd(keyType string, cmd *cobra.Command) error {
passphraseStr = envPassphrase
} else {
// Use secure passphrase input with confirmation
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlock key: ")
passphraseStr, err = readSecurePassphrase("Enter passphrase for unlocker: ")
if err != nil {
return fmt.Errorf("failed to read passphrase: %w", err)
}
}
passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr)
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
if err != nil {
return err
}
cmd.Printf("Created passphrase unlock key: %s\n", passphraseKey.GetID())
cmd.Printf("Created passphrase unlocker: %s\n", passphraseUnlocker.GetID())
return nil
case "keychain":
keychainKey, err := secret.CreateKeychainUnlockKey(cli.fs, cli.stateDir)
keychainUnlocker, err := secret.CreateKeychainUnlocker(cli.fs, cli.stateDir)
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())
if keyName, err := keychainKey.GetKeychainItemName(); err == nil {
cmd.Printf("Created macOS Keychain unlocker: %s\n", keychainUnlocker.GetID())
if keyName, err := keychainUnlocker.GetKeychainItemName(); err == nil {
cmd.Printf("Keychain Item Name: %s\n", keyName)
}
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")
}
pgpKey, err := secret.CreatePGPUnlockKey(cli.fs, cli.stateDir, gpgKeyID)
pgpUnlocker, err := secret.CreatePGPUnlocker(cli.fs, cli.stateDir, gpgKeyID)
if err != nil {
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)
return nil
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
func (cli *CLIInstance) KeysRemove(keyID string) error {
// UnlockersRemove removes an unlocker
func (cli *CLIInstance) UnlockersRemove(unlockerID string) error {
// Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil {
return err
}
return vlt.RemoveUnlockKey(keyID)
return vlt.RemoveUnlocker(unlockerID)
}
// KeySelect selects an unlock key as current
func (cli *CLIInstance) KeySelect(keyID string) error {
// UnlockerSelect selects an unlocker as current
func (cli *CLIInstance) UnlockerSelect(unlockerID string) error {
// Get current vault
vlt, err := vault.GetCurrentVault(cli.fs, cli.stateDir)
if err != nil {
return err
}
return vlt.SelectUnlockKey(keyID)
return vlt.SelectUnlocker(unlockerID)
}

View File

@@ -251,17 +251,17 @@ func (cli *CLIInstance) VaultImport(vaultName string) error {
// Unlock the vault with the derived long-term key
vlt.Unlock(ltIdentity)
// Create passphrase-protected unlock key
secret.Debug("Creating passphrase-protected unlock key")
passphraseKey, err := vlt.CreatePassphraseKey(passphraseStr)
// Create passphrase-protected unlocker
secret.Debug("Creating passphrase-protected unlocker")
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphraseStr)
if err != nil {
secret.Debug("Failed to create unlock key", "error", err)
return fmt.Errorf("failed to create unlock key: %w", err)
secret.Debug("Failed to create unlocker", "error", err)
return fmt.Errorf("failed to create unlocker: %w", err)
}
fmt.Printf("Successfully imported mnemonic into vault '%s'\n", vaultName)
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
}

View File

@@ -15,19 +15,19 @@ import (
"github.com/spf13/afero"
)
// KeychainUnlockKeyMetadata extends UnlockKeyMetadata with keychain-specific data
type KeychainUnlockKeyMetadata struct {
UnlockKeyMetadata
// KeychainUnlockerMetadata extends UnlockerMetadata with keychain-specific data
type KeychainUnlockerMetadata struct {
UnlockerMetadata
// Age keypair information
AgePublicKey string `json:"age_public_key"`
// Keychain item name
KeychainItemName string `json:"keychain_item_name"`
}
// KeychainUnlockKey represents a macOS Keychain-protected unlock key
type KeychainUnlockKey struct {
// KeychainUnlocker represents a macOS Keychain-protected unlocker
type KeychainUnlocker struct {
Directory string
Metadata UnlockKeyMetadata
Metadata UnlockerMetadata
fs afero.Fs
}
@@ -38,17 +38,17 @@ type KeychainData struct {
EncryptedLongtermKey string `json:"encrypted_longterm_key"`
}
// GetIdentity implements UnlockKey interface for Keychain-based unlock keys
func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
DebugWith("Getting keychain unlock key identity",
slog.String("key_id", k.GetID()),
slog.String("key_type", k.GetType()),
// GetIdentity implements Unlocker interface for Keychain-based unlockers
func (k *KeychainUnlocker) GetIdentity() (*age.X25519Identity, error) {
DebugWith("Getting keychain unlocker identity",
slog.String("unlocker_id", k.GetID()),
slog.String("unlocker_type", k.GetType()),
)
// Step 1: Get keychain item name
keychainItemName, err := k.GetKeychainItemName()
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)
}
@@ -61,18 +61,18 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
}
DebugWith("Retrieved data from keychain",
slog.String("key_id", k.GetID()),
slog.String("unlocker_id", k.GetID()),
slog.Int("data_length", len(keychainDataBytes)),
)
// Step 3: Parse keychain data
var keychainData KeychainData
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)
}
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
agePrivKeyPath := filepath.Join(k.Directory, "priv.age")
@@ -85,61 +85,61 @@ func (k *KeychainUnlockKey) GetIdentity() (*age.X25519Identity, error) {
}
DebugWith("Read encrypted age private key",
slog.String("key_id", k.GetID()),
slog.String("unlocker_id", k.GetID()),
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
)
// 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)
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)
}
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)),
)
// 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))
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)
}
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()),
)
return ageIdentity, nil
}
// GetType implements UnlockKey interface
func (k *KeychainUnlockKey) GetType() string {
// GetType implements Unlocker interface
func (k *KeychainUnlocker) GetType() string {
return "keychain"
}
// GetMetadata implements UnlockKey interface
func (k *KeychainUnlockKey) GetMetadata() UnlockKeyMetadata {
// GetMetadata implements Unlocker interface
func (k *KeychainUnlocker) GetMetadata() UnlockerMetadata {
return k.Metadata
}
// GetDirectory implements UnlockKey interface
func (k *KeychainUnlockKey) GetDirectory() string {
// GetDirectory implements Unlocker interface
func (k *KeychainUnlocker) GetDirectory() string {
return k.Directory
}
// GetID implements UnlockKey interface
func (k *KeychainUnlockKey) GetID() string {
// GetID implements Unlocker interface
func (k *KeychainUnlocker) GetID() string {
return k.Metadata.ID
}
// ID implements UnlockKey interface - generates ID from keychain item name
func (k *KeychainUnlockKey) ID() string {
// ID implements Unlocker interface - generates ID from keychain item name
func (k *KeychainUnlocker) ID() string {
// Generate ID using keychain item name
keychainItemName, err := k.GetKeychainItemName()
if err != nil {
@@ -149,12 +149,12 @@ func (k *KeychainUnlockKey) ID() string {
return fmt.Sprintf("%s-keychain", keychainItemName)
}
// Remove implements UnlockKey interface - removes the keychain unlock key
func (k *KeychainUnlockKey) Remove() error {
// Remove implements Unlocker interface - removes the keychain unlocker
func (k *KeychainUnlocker) Remove() error {
// Step 1: Get keychain item name
keychainItemName, err := k.GetKeychainItemName()
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)
}
@@ -166,19 +166,19 @@ func (k *KeychainUnlockKey) Remove() error {
}
// 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 {
Debug("Failed to remove keychain unlock key directory", "error", err, "directory", k.Directory)
return fmt.Errorf("failed to remove keychain unlock key directory: %w", err)
Debug("Failed to remove keychain unlocker directory", "error", err, "directory", k.Directory)
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
}
// NewKeychainUnlockKey creates a new KeychainUnlockKey instance
func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *KeychainUnlockKey {
return &KeychainUnlockKey{
// NewKeychainUnlocker creates a new KeychainUnlocker instance
func NewKeychainUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *KeychainUnlocker {
return &KeychainUnlocker{
Directory: directory,
Metadata: metadata,
fs: fs,
@@ -186,7 +186,7 @@ func NewKeychainUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetad
}
// GetKeychainItemName returns the keychain item name from metadata
func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
func (k *KeychainUnlocker) GetKeychainItemName() (string, error) {
// Load the metadata
metadataPath := filepath.Join(k.Directory, "unlock-metadata.json")
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)
}
var keychainMetadata KeychainUnlockKeyMetadata
var keychainMetadata KeychainUnlockerMetadata
if err := json.Unmarshal(metadataData, &keychainMetadata); err != nil {
return "", fmt.Errorf("failed to parse keychain metadata: %w", err)
}
@@ -202,8 +202,8 @@ func (k *KeychainUnlockKey) GetKeychainItemName() (string, error) {
return keychainMetadata.KeychainItemName, nil
}
// generateKeychainUnlockKeyName generates a unique name for the keychain unlock key
func generateKeychainUnlockKeyName(vaultName string) (string, error) {
// generateKeychainUnlockerName generates a unique name for the keychain unlocker
func generateKeychainUnlockerName(vaultName string) (string, error) {
hostname, err := os.Hostname()
if err != nil {
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
}
// CreateKeychainUnlockKey creates a new keychain unlock key and stores it in the vault
func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey, error) {
// CreateKeychainUnlocker creates a new keychain unlocker and stores it in the vault
func CreateKeychainUnlocker(fs afero.Fs, stateDir string) (*KeychainUnlocker, error) {
// Check if we're on macOS
if err := checkMacOSAvailable(); err != nil {
return nil, err
@@ -228,23 +228,23 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
}
// Generate the keychain item name
keychainItemName, err := generateKeychainUnlockKeyName(vault.GetName())
keychainItemName, err := generateKeychainUnlockerName(vault.GetName())
if err != nil {
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()
if err != nil {
return nil, fmt.Errorf("failed to get vault directory: %w", err)
}
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", keychainItemName)
if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
unlockerDir := filepath.Join(vaultDir, "unlockers.d", keychainItemName)
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
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()
if err != nil {
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
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 {
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)
}
agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age")
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age")
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
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())
} else {
// Get the vault to access current unlock key
currentUnlockKey, err := vault.GetCurrentUnlockKey()
// Get the vault to access current unlocker
currentUnlocker, err := vault.GetCurrentUnlocker()
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
currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
// Get the current unlocker identity
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
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
switch currentUnlockKey := currentUnlockKey.(type) {
case *PassphraseUnlockKey:
// Read the encrypted long-term private key from passphrase unlock key
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
switch currentUnlocker := currentUnlocker.(type) {
case *PassphraseUnlocker:
// Read the encrypted long-term private key from passphrase unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
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:
// Read the encrypted long-term private key from PGP unlock key
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
case *PGPUnlocker:
// Read the encrypted long-term private key from PGP unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
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:
// Read the encrypted long-term private key from another keychain unlock key
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
case *KeychainUnlocker:
// Read the encrypted long-term private key from another keychain unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
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:
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
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
// Decrypt long-term private key using current unlocker
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
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())
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
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
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
keyID := fmt.Sprintf("%s-keychain", keychainItemName)
keychainMetadata := KeychainUnlockKeyMetadata{
UnlockKeyMetadata: UnlockKeyMetadata{
keychainMetadata := KeychainUnlockerMetadata{
UnlockerMetadata: UnlockerMetadata{
ID: keyID,
Type: "keychain",
CreatedAt: time.Now(),
@@ -380,16 +380,16 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
metadataBytes, err := json.MarshalIndent(keychainMetadata, "", " ")
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 {
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
if err := afero.WriteFile(fs, filepath.Join(unlockerDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
}
return &KeychainUnlockKey{
Directory: unlockKeyDir,
Metadata: keychainMetadata.UnlockKeyMetadata,
return &KeychainUnlocker{
Directory: unlockerDir,
Metadata: keychainMetadata.UnlockerMetadata,
fs: fs,
}, nil
}
@@ -398,7 +398,7 @@ func CreateKeychainUnlockKey(fs afero.Fs, stateDir string) (*KeychainUnlockKey,
func checkMacOSAvailable() error {
cmd := exec.Command("/usr/bin/security", "help")
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
}

View File

@@ -14,8 +14,8 @@ type VaultMetadata struct {
MnemonicHash string `json:"mnemonic_hash"` // Double SHA256 hash of mnemonic for index tracking
}
// UnlockKeyMetadata contains information about an unlock key
type UnlockKeyMetadata struct {
// UnlockerMetadata contains information about an unlocker
type UnlockerMetadata struct {
ID string `json:"id"`
Type string `json:"type"` // passphrase, pgp, keychain
CreatedAt time.Time `json:"createdAt"`

View File

@@ -12,7 +12,7 @@ import (
"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
if os.Getenv("CI") == "true" {
t.Skip("Skipping test with real filesystem in CI environment")
@@ -33,21 +33,21 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
testPassphrase := "test-passphrase-123"
// Create the directory structure
keyDir := filepath.Join(tempDir, "unlock-key")
if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
t.Fatalf("Failed to create key directory: %v", err)
unlockerDir := filepath.Join(tempDir, "unlocker")
if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
t.Fatalf("Failed to create unlocker directory: %v", err)
}
// Set up test metadata
metadata := secret.UnlockKeyMetadata{
metadata := secret.UnlockerMetadata{
ID: "test-passphrase",
Type: "passphrase",
CreatedAt: time.Now(),
Flags: []string{},
}
// Create passphrase unlock key
unlockKey := secret.NewPassphraseUnlockKey(fs, keyDir, metadata)
// Create passphrase unlocker
unlocker := secret.NewPassphraseUnlocker(fs, unlockerDir, metadata)
// Generate a test age identity
ageIdentity, err := age.GenerateX25519Identity()
@@ -59,7 +59,7 @@ func TestPassphraseUnlockKeyWithRealFS(t *testing.T) {
// Test writing public key
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 {
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)
}
privKeyPath := filepath.Join(keyDir, "priv.age")
privKeyPath := filepath.Join(unlockerDir, "priv.age")
if err := afero.WriteFile(fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
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)
}
// 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)
if err != nil {
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)
}
ltPrivKeyPath := filepath.Join(keyDir, "longterm.age")
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
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
t.Run("GetIdentityFromEnv", func(t *testing.T) {
identity, err := unlockKey.GetIdentity()
identity, err := unlocker.GetIdentity()
if err != nil {
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
t.Run("GetIdentityWithoutEnv", func(t *testing.T) {
// This should fail since we're not in an interactive terminal
_, err := unlockKey.GetIdentity()
_, err := unlocker.GetIdentity()
if err == nil {
t.Errorf("Should have failed to get identity without passphrase env var")
}
})
// Test removing the unlock key
t.Run("RemoveUnlockKey", func(t *testing.T) {
err := unlockKey.Remove()
// Test removing the unlocker
t.Run("RemoveUnlocker", func(t *testing.T) {
err := unlocker.Remove()
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
exists, err := afero.DirExists(fs, keyDir)
exists, err := afero.DirExists(fs, unlockerDir)
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 {
t.Errorf("Key directory should not exist after removal")
t.Errorf("Unlocker directory should not exist after removal")
}
})
}

View File

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

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

View File

@@ -123,7 +123,7 @@ func runGPGWithPassphrase(gnupgHome, passphrase string, args []string, input io.
return stdout.Bytes(), nil
}
func TestPGPUnlockKeyWithRealFS(t *testing.T) {
func TestPGPUnlockerWithRealFS(t *testing.T) {
// Skip tests if gpg is not available
if _, err := exec.LookPath("gpg"); err != nil {
t.Skip("GPG not available, skipping PGP unlock key tests")
@@ -258,7 +258,7 @@ Passphrase: ` + testPassphrase + `
vaultName := "test-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
timer := time.AfterFunc(30*time.Second, func() {
t.Fatalf("Test timed out after 30 seconds")
@@ -298,50 +298,50 @@ Passphrase: ` + testPassphrase + `
// Unlock the vault
vlt.Unlock(ltIdentity)
// Create a passphrase unlock key first (to have current unlock key)
passKey, err := vlt.CreatePassphraseKey("test-passphrase")
// Create a passphrase unlocker first (to have current unlocker)
passUnlocker, err := vlt.CreatePassphraseUnlocker("test-passphrase")
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
if passKey == nil {
t.Fatal("Passphrase key is nil")
// Verify passphrase unlocker was created
if passUnlocker == nil {
t.Fatal("Passphrase unlocker is nil")
}
// 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 {
t.Fatalf("Failed to create PGP unlock key: %v", err)
}
// Verify the PGP unlock key was created
if pgpKey == nil {
if pgpUnlocker == nil {
t.Fatal("PGP unlock key is nil")
}
// Check if the key has the correct type
if pgpKey.GetType() != "pgp" {
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpKey.GetType())
if pgpUnlocker.GetType() != "pgp" {
t.Errorf("Expected PGP unlock key type 'pgp', got '%s'", pgpUnlocker.GetType())
}
// Check if the key ID includes the GPG key ID
if !strings.Contains(pgpKey.GetID(), keyID) {
t.Errorf("PGP unlock key ID '%s' does not contain GPG key ID '%s'", pgpKey.GetID(), keyID)
if !strings.Contains(pgpUnlocker.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
keyDir := pgpKey.GetDirectory()
keyExists, err := afero.DirExists(fs, keyDir)
unlockerDir := pgpUnlocker.GetDirectory()
keyExists, err := afero.DirExists(fs, unlockerDir)
if err != nil {
t.Fatalf("Failed to check if PGP key directory exists: %v", err)
}
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
pubKeyPath := filepath.Join(keyDir, "pub.age")
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
pubKeyExists, err := afero.Exists(fs, pubKeyPath)
if err != nil {
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)
}
privKeyPath := filepath.Join(keyDir, "priv.age.gpg")
privKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
privKeyExists, err := afero.Exists(fs, privKeyPath)
if err != nil {
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)
}
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
metadataExists, err := afero.Exists(fs, metadataPath)
if err != nil {
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)
}
longtermPath := filepath.Join(keyDir, "longterm.age")
longtermPath := filepath.Join(unlockerDir, "longterm.age")
longtermExists, err := afero.Exists(fs, longtermPath)
if err != nil {
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
keyDir := filepath.Join(tempDir, "unlock-key")
if err := os.MkdirAll(keyDir, secret.DirPerms); err != nil {
t.Fatalf("Failed to create key directory: %v", err)
unlockerDir := filepath.Join(tempDir, "unlocker")
if err := os.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
t.Fatalf("Failed to create unlocker directory: %v", err)
}
// Set up test metadata
metadata := secret.UnlockKeyMetadata{
metadata := secret.UnlockerMetadata{
ID: fmt.Sprintf("%s-pgp", keyID),
Type: "pgp",
CreatedAt: time.Now(),
Flags: []string{"gpg", "encrypted"},
}
// Create a PGP unlock key for the remaining tests
unlockKey := secret.NewPGPUnlockKey(fs, keyDir, metadata)
// Create a PGP unlocker for the remaining tests
unlocker := secret.NewPGPUnlocker(fs, unlockerDir, metadata)
// Test getting GPG key ID
t.Run("GetGPGKeyID", func(t *testing.T) {
// Create PGP metadata with GPG key ID
type PGPUnlockKeyMetadata struct {
secret.UnlockKeyMetadata
type PGPUnlockerMetadata struct {
secret.UnlockerMetadata
GPGKeyID string `json:"gpg_key_id"`
}
pgpMetadata := PGPUnlockKeyMetadata{
UnlockKeyMetadata: metadata,
GPGKeyID: keyID,
pgpMetadata := PGPUnlockerMetadata{
UnlockerMetadata: metadata,
GPGKeyID: keyID,
}
// Write metadata file
metadataPath := filepath.Join(keyDir, "unlock-metadata.json")
metadataPath := filepath.Join(unlockerDir, "unlocker-metadata.json")
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
if err != nil {
t.Fatalf("Failed to marshal metadata: %v", err)
@@ -445,7 +445,7 @@ Passphrase: ` + testPassphrase + `
}
// Get GPG key ID
retrievedKeyID, err := unlockKey.GetGPGKeyID()
retrievedKeyID, err := unlocker.GetGPGKeyID()
if err != nil {
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) {
// Generate an age identity for testing
ageIdentity, err := age.GenerateX25519Identity()
@@ -465,7 +465,7 @@ Passphrase: ` + testPassphrase + `
}
// 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 {
t.Fatalf("Failed to write public key: %v", err)
}
@@ -478,13 +478,13 @@ Passphrase: ` + testPassphrase + `
}
// 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 {
t.Fatalf("Failed to write encrypted private key: %v", err)
}
// Now try to get the identity - this will use our custom GPGDecryptFunc
identity, err := unlockKey.GetIdentity()
identity, err := unlocker.GetIdentity()
if err != nil {
t.Fatalf("Failed to get identity: %v", err)
}
@@ -497,30 +497,30 @@ Passphrase: ` + testPassphrase + `
}
})
// Test removing the unlock key
t.Run("RemoveUnlockKey", func(t *testing.T) {
// Ensure key directory exists before removal
keyExists, err := afero.DirExists(fs, keyDir)
// Test removing the unlocker
t.Run("RemoveUnlocker", func(t *testing.T) {
// Ensure unlocker directory exists before removal
keyExists, err := afero.DirExists(fs, unlockerDir)
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 {
t.Fatalf("Key directory does not exist: %s", keyDir)
t.Fatalf("Unlocker directory does not exist: %s", unlockerDir)
}
// Remove unlock key
err = unlockKey.Remove()
// Remove unlocker
err = unlocker.Remove()
if err != nil {
t.Fatalf("Failed to remove unlock key: %v", err)
t.Fatalf("Failed to remove unlocker: %v", err)
}
// Verify directory is gone
keyExists, err = afero.DirExists(fs, keyDir)
keyExists, err = afero.DirExists(fs, unlockerDir)
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 {
t.Errorf("Key directory still exists after removal: %s", keyDir)
t.Errorf("Unlocker directory still exists after removal: %s", unlockerDir)
}
})
}

View File

@@ -26,9 +26,9 @@ var (
GPGDecryptFunc = gpgDecryptDefault
)
// PGPUnlockKeyMetadata extends UnlockKeyMetadata with PGP-specific data
type PGPUnlockKeyMetadata struct {
UnlockKeyMetadata
// PGPUnlockerMetadata extends UnlockerMetadata with PGP-specific data
type PGPUnlockerMetadata struct {
UnlockerMetadata
// GPG key ID used for encryption
GPGKeyID string `json:"gpg_key_id"`
// Age keypair information
@@ -36,18 +36,18 @@ type PGPUnlockKeyMetadata struct {
AgeRecipient string `json:"age_recipient"`
}
// PGPUnlockKey represents a PGP-protected unlock key
type PGPUnlockKey struct {
// PGPUnlocker represents a PGP-protected unlocker
type PGPUnlocker struct {
Directory string
Metadata UnlockKeyMetadata
Metadata UnlockerMetadata
fs afero.Fs
}
// GetIdentity implements UnlockKey interface for PGP-based unlock keys
func (p *PGPUnlockKey) GetIdentity() (*age.X25519Identity, error) {
DebugWith("Getting PGP unlock key identity",
slog.String("key_id", p.GetID()),
slog.String("key_type", p.GetType()),
// GetIdentity implements Unlocker interface for PGP-based unlockers
func (p *PGPUnlocker) GetIdentity() (*age.X25519Identity, error) {
DebugWith("Getting PGP unlocker identity",
slog.String("unlocker_id", p.GetID()),
slog.String("unlocker_type", p.GetType()),
)
// 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",
slog.String("key_id", p.GetID()),
slog.String("unlocker_id", p.GetID()),
slog.Int("encrypted_length", len(encryptedAgePrivKeyData)),
)
// 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)
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)
}
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)),
)
// 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))
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)
}
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()),
)
return ageIdentity, nil
}
// GetType implements UnlockKey interface
func (p *PGPUnlockKey) GetType() string {
// GetType implements Unlocker interface
func (p *PGPUnlocker) GetType() string {
return "pgp"
}
// GetMetadata implements UnlockKey interface
func (p *PGPUnlockKey) GetMetadata() UnlockKeyMetadata {
// GetMetadata implements Unlocker interface
func (p *PGPUnlocker) GetMetadata() UnlockerMetadata {
return p.Metadata
}
// GetDirectory implements UnlockKey interface
func (p *PGPUnlockKey) GetDirectory() string {
// GetDirectory implements Unlocker interface
func (p *PGPUnlocker) GetDirectory() string {
return p.Directory
}
// GetID implements UnlockKey interface
func (p *PGPUnlockKey) GetID() string {
// GetID implements Unlocker interface
func (p *PGPUnlocker) GetID() string {
return p.Metadata.ID
}
// ID implements UnlockKey interface - generates ID from GPG key ID
func (p *PGPUnlockKey) ID() string {
// ID implements Unlocker interface - generates ID from GPG key ID
func (p *PGPUnlocker) ID() string {
// Generate ID using GPG key ID: <keyid>-pgp
gpgKeyID, err := p.GetGPGKeyID()
if err != nil {
@@ -125,19 +125,19 @@ func (p *PGPUnlockKey) ID() string {
return fmt.Sprintf("%s-pgp", gpgKeyID)
}
// Remove implements UnlockKey interface - removes the PGP unlock key
func (p *PGPUnlockKey) Remove() error {
// For PGP keys, we just need to remove the directory
// Remove implements Unlocker interface - removes the PGP unlocker
func (p *PGPUnlocker) Remove() error {
// For PGP 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 PGP unlock key directory: %w", err)
return fmt.Errorf("failed to remove PGP unlocker directory: %w", err)
}
return nil
}
// NewPGPUnlockKey creates a new PGPUnlockKey instance
func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PGPUnlockKey {
return &PGPUnlockKey{
// NewPGPUnlocker creates a new PGPUnlocker instance
func NewPGPUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *PGPUnlocker {
return &PGPUnlocker{
Directory: directory,
Metadata: metadata,
fs: fs,
@@ -145,7 +145,7 @@ func NewPGPUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata)
}
// GetGPGKeyID returns the GPG key ID from metadata
func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
func (p *PGPUnlocker) GetGPGKeyID() (string, error) {
// Load the metadata
metadataPath := filepath.Join(p.Directory, "unlock-metadata.json")
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)
}
var pgpMetadata PGPUnlockKeyMetadata
var pgpMetadata PGPUnlockerMetadata
if err := json.Unmarshal(metadataData, &pgpMetadata); err != nil {
return "", fmt.Errorf("failed to parse PGP metadata: %w", err)
}
@@ -161,8 +161,8 @@ func (p *PGPUnlockKey) GetGPGKeyID() (string, error) {
return pgpMetadata.GPGKeyID, nil
}
// generatePGPUnlockKeyName generates a unique name for the PGP unlock key based on hostname and date
func generatePGPUnlockKeyName() (string, error) {
// generatePGPUnlockerName generates a unique name for the PGP unlocker based on hostname and date
func generatePGPUnlockerName() (string, error) {
hostname, err := os.Hostname()
if err != nil {
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
}
// CreatePGPUnlockKey creates a new PGP unlock key and stores it in the vault
func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlockKey, error) {
// CreatePGPUnlocker creates a new PGP unlocker and stores it in the vault
func CreatePGPUnlocker(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlocker, error) {
// Check if GPG is available
if err := checkGPGAvailable(); err != nil {
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)
}
// Generate the unlock key name based on hostname and date
unlockKeyName, err := generatePGPUnlockKeyName()
// Generate the unlocker name based on hostname and date
unlockerName, err := generatePGPUnlockerName()
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()
if err != nil {
return nil, fmt.Errorf("failed to get vault directory: %w", err)
}
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", unlockKeyName)
if err := fs.MkdirAll(unlockKeyDir, DirPerms); err != nil {
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
unlockerDir := filepath.Join(vaultDir, "unlockers.d", unlockerName)
if err := fs.MkdirAll(unlockerDir, DirPerms); err != nil {
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()
if err != nil {
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
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 {
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())
} else {
// Get the vault to access current unlock key
currentUnlockKey, err := vault.GetCurrentUnlockKey()
// Get the vault to access current unlocker
currentUnlocker, err := vault.GetCurrentUnlocker()
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
currentUnlockIdentity, err := currentUnlockKey.GetIdentity()
// Get the current unlocker identity
currentUnlockerIdentity, err := currentUnlocker.GetIdentity()
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
switch currentUnlockKey := currentUnlockKey.(type) {
case *PassphraseUnlockKey:
// Read the encrypted long-term private key from passphrase unlock key
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
switch currentUnlocker := currentUnlocker.(type) {
case *PassphraseUnlocker:
// Read the encrypted long-term private key from passphrase unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
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:
// Read the encrypted long-term private key from PGP unlock key
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlockKey.GetDirectory(), "longterm.age"))
case *PGPUnlocker:
// Read the encrypted long-term private key from PGP unlocker
encryptedLtPrivKey, err = afero.ReadFile(fs, filepath.Join(currentUnlocker.GetDirectory(), "longterm.age"))
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:
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
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockIdentity)
// Step 6: Decrypt long-term private key using current unlocker
ltPrivKeyData, err = DecryptWithIdentity(encryptedLtPrivKey, currentUnlockerIdentity)
if err != nil {
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())
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
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
if err := afero.WriteFile(fs, ltPrivKeyPath, encryptedLtPrivKeyToAge, FilePerms); err != nil {
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)
}
agePrivKeyPath := filepath.Join(unlockKeyDir, "priv.age.gpg")
agePrivKeyPath := filepath.Join(unlockerDir, "priv.age.gpg")
if err := afero.WriteFile(fs, agePrivKeyPath, encryptedAgePrivKey, FilePerms); err != nil {
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
keyID := fmt.Sprintf("%s-pgp", gpgKeyID)
pgpMetadata := PGPUnlockKeyMetadata{
UnlockKeyMetadata: UnlockKeyMetadata{
pgpMetadata := PGPUnlockerMetadata{
UnlockerMetadata: UnlockerMetadata{
ID: keyID,
Type: "pgp",
CreatedAt: time.Now(),
@@ -310,16 +310,16 @@ func CreatePGPUnlockKey(fs afero.Fs, stateDir string, gpgKeyID string) (*PGPUnlo
metadataBytes, err := json.MarshalIndent(pgpMetadata, "", " ")
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 {
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
if err := afero.WriteFile(fs, filepath.Join(unlockerDir, "unlock-metadata.json"), metadataBytes, FilePerms); err != nil {
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
}
return &PGPUnlockKey{
Directory: unlockKeyDir,
Metadata: pgpMetadata.UnlockKeyMetadata,
return &PGPUnlocker{
Directory: unlockerDir,
Metadata: pgpMetadata.UnlockerMetadata,
fs: fs,
}, nil
}

View File

@@ -20,8 +20,8 @@ type VaultInterface interface {
AddSecret(name string, value []byte, force bool) error
GetName() string
GetFilesystem() afero.Fs
GetCurrentUnlockKey() (UnlockKey, error)
CreatePassphraseKey(passphrase string) (*PassphraseUnlockKey, error)
GetCurrentUnlocker() (Unlocker, error)
CreatePassphraseUnlocker(passphrase string) (*PassphraseUnlocker, error)
}
// Secret represents a secret in a vault
@@ -81,8 +81,8 @@ func (s *Secret) Save(value []byte, force bool) error {
return nil
}
// GetValue retrieves and decrypts the secret value using the provided unlock key
func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
// GetValue retrieves and decrypts the secret value using the provided unlocker
func (s *Secret) GetValue(unlocker Unlocker) ([]byte, error) {
DebugWith("Getting secret value",
slog.String("secret_name", s.Name),
slog.String("vault_name", s.vault.GetName()),
@@ -118,29 +118,29 @@ func (s *Secret) GetValue(unlockKey UnlockKey) ([]byte, error) {
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
if unlockKey == nil {
Debug("No unlock key provided for secret decryption", "secret_name", s.Name)
return nil, fmt.Errorf("unlock key required to decrypt secret")
// Use the provided unlocker to get the vault's long-term private key
if unlocker == nil {
Debug("No unlocker provided for secret decryption", "secret_name", s.Name)
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("unlock_key_type", unlockKey.GetType()),
slog.String("unlock_key_id", unlockKey.GetID()),
slog.String("unlocker_type", unlocker.GetType()),
slog.String("unlocker_id", unlocker.GetID()),
)
// Step 1: Use the unlock key to get the vault's long-term private key
unlockIdentity, err := unlockKey.GetIdentity()
// Step 1: Use the unlocker to get the vault's long-term private key
unlockIdentity, err := unlocker.GetIdentity()
if err != nil {
Debug("Failed to get unlock key identity", "error", err, "secret_name", s.Name, "unlock_key_type", unlockKey.GetType())
return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
Debug("Failed to get unlocker identity", "error", err, "secret_name", s.Name, "unlocker_type", unlocker.GetType())
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
}
// Read the encrypted long-term private key from the unlock key directory
encryptedLtPrivKeyPath := filepath.Join(unlockKey.GetDirectory(), "longterm.age")
// Read the encrypted long-term private key from the unlocker directory
encryptedLtPrivKeyPath := filepath.Join(unlocker.GetDirectory(), "longterm.age")
Debug("Reading encrypted long-term private key", "path", 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)
}
// Decrypt the encrypted long-term private key using the unlock key
Debug("Decrypting long-term private key using unlock key", "secret_name", s.Name)
// Decrypt the encrypted long-term private key using the unlocker
Debug("Decrypting long-term private key using unlocker", "secret_name", s.Name)
ltPrivKeyData, err := DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
if err != nil {
Debug("Failed to decrypt long-term private key", "error", err, "secret_name", s.Name)

View File

@@ -39,11 +39,11 @@ func (m *MockVault) GetFilesystem() afero.Fs {
return m.fs
}
func (m *MockVault) GetCurrentUnlockKey() (UnlockKey, error) {
func (m *MockVault) GetCurrentUnlocker() (Unlocker, error) {
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
}

View File

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

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

View File

@@ -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)
}
// Create unlock keys directory
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
if err := fs.MkdirAll(unlockKeysDir, secret.DirPerms); err != nil {
return nil, fmt.Errorf("failed to create unlock keys directory: %w", err)
// Create unlockers directory
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
if err := fs.MkdirAll(unlockersDir, secret.DirPerms); err != nil {
return nil, fmt.Errorf("failed to create unlockers directory: %w", err)
}
// Save initial vault metadata (without derivation info until a mnemonic is imported)

View File

@@ -13,7 +13,7 @@ import (
// Alias the metadata types from secret package for convenience
type VaultMetadata = secret.VaultMetadata
type UnlockKeyMetadata = secret.UnlockKeyMetadata
type UnlockerMetadata = secret.UnlockerMetadata
type SecretMetadata = secret.SecretMetadata
type Configuration = secret.Configuration

View File

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

View File

@@ -83,32 +83,32 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
return ltIdentity, nil
}
// No mnemonic available, try to use current unlock key
secret.Debug("No mnemonic available, using current unlock key to unlock vault", "vault_name", v.Name)
// No mnemonic available, try to use current unlocker
secret.Debug("No mnemonic available, using current unlocker to unlock vault", "vault_name", v.Name)
// Get current unlock key
unlockKey, err := v.GetCurrentUnlockKey()
// Get current unlocker
unlocker, err := v.GetCurrentUnlocker()
if err != nil {
secret.Debug("Failed to get current unlock key", "error", err, "vault_name", v.Name)
return nil, fmt.Errorf("failed to get current unlock key: %w", err)
secret.Debug("Failed to get current unlocker", "error", err, "vault_name", v.Name)
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("unlock_key_type", unlockKey.GetType()),
slog.String("unlock_key_id", unlockKey.GetID()),
slog.String("unlocker_type", unlocker.GetType()),
slog.String("unlocker_id", unlocker.GetID()),
)
// Get unlock key identity
unlockIdentity, err := unlockKey.GetIdentity()
// Get unlocker identity
unlockerIdentity, err := unlocker.GetIdentity()
if err != nil {
secret.Debug("Failed to get unlock key identity", "error", err, "unlock_key_type", unlockKey.GetType())
return nil, fmt.Errorf("failed to get unlock key identity: %w", err)
secret.Debug("Failed to get unlocker identity", "error", err, "unlocker_type", unlocker.GetType())
return nil, fmt.Errorf("failed to get unlocker identity: %w", err)
}
// Read encrypted long-term private key from unlock key directory
unlockKeyDir := unlockKey.GetDirectory()
encryptedLtPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
// Read encrypted long-term private key from unlocker directory
unlockerDir := unlocker.GetDirectory()
encryptedLtPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
secret.Debug("Reading encrypted long-term private key", "path", 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",
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)),
)
// Decrypt long-term private key using unlock key
secret.Debug("Decrypting long-term private key with unlock key", "unlock_key_type", unlockKey.GetType())
ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockIdentity)
// Decrypt long-term private key using unlocker
secret.Debug("Decrypting long-term private key with unlocker", "unlocker_type", unlocker.GetType())
ltPrivKeyData, err := secret.DecryptWithIdentity(encryptedLtPrivKey, unlockerIdentity)
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)
}
secret.DebugWith("Successfully decrypted long-term private key",
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)),
)
@@ -145,15 +145,15 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
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("unlock_key_type", unlockKey.GetType()),
slog.String("unlocker_type", unlocker.GetType()),
slog.String("public_key", ltIdentity.Recipient().String()),
)
// Cache the derived key by unlocking the vault
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
}

View File

@@ -174,8 +174,8 @@ func TestVaultOperations(t *testing.T) {
}
})
// Test unlock key operations
t.Run("UnlockKeyOperations", func(t *testing.T) {
// Test unlocker operations
t.Run("UnlockerOperations", func(t *testing.T) {
vlt, err := GetCurrentVault(fs, stateDir)
if err != nil {
t.Fatalf("Failed to get current vault: %v", err)
@@ -189,25 +189,25 @@ func TestVaultOperations(t *testing.T) {
}
}
// Create a passphrase unlock key
passphraseKey, err := vlt.CreatePassphraseKey("test-passphrase")
// Create a passphrase unlocker
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker("test-passphrase")
if err != nil {
t.Fatalf("Failed to create passphrase key: %v", err)
t.Fatalf("Failed to create passphrase unlocker: %v", err)
}
// List unlock keys
keys, err := vlt.ListUnlockKeys()
// List unlockers
unlockers, err := vlt.ListUnlockers()
if err != nil {
t.Fatalf("Failed to list unlock keys: %v", err)
t.Fatalf("Failed to list unlockers: %v", err)
}
if len(keys) == 0 {
t.Errorf("Expected at least one unlock key")
if len(unlockers) == 0 {
t.Errorf("Expected at least one unlocker")
}
// Check key type
keyFound := false
for _, key := range keys {
for _, key := range unlockers {
if key.Type == "passphrase" {
keyFound = true
break
@@ -215,23 +215,23 @@ func TestVaultOperations(t *testing.T) {
}
if !keyFound {
t.Errorf("Expected to find passphrase unlock key")
t.Errorf("Expected to find passphrase unlocker")
}
// Test selecting unlock key
err = vlt.SelectUnlockKey(passphraseKey.GetID())
// Test selecting unlocker
err = vlt.SelectUnlocker(passphraseUnlocker.GetID())
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
currentKey, err := vlt.GetCurrentUnlockKey()
// Test getting current unlocker
currentUnlocker, err := vlt.GetCurrentUnlocker()
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() {
t.Errorf("Expected current unlock key ID '%s', got '%s'", passphraseKey.GetID(), currentKey.GetID())
if currentUnlocker.GetID() != passphraseUnlocker.GetID() {
t.Errorf("Expected current unlocker ID '%s', got '%s'", passphraseUnlocker.GetID(), currentUnlocker.GetID())
}
})
}