'unlock keys' renamed to 'unlockers'
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// PassphraseUnlockKey represents a passphrase-protected unlock key
|
||||
type PassphraseUnlockKey struct {
|
||||
Directory string
|
||||
Metadata UnlockKeyMetadata
|
||||
fs afero.Fs
|
||||
Passphrase string
|
||||
}
|
||||
|
||||
// GetIdentity implements UnlockKey interface for passphrase-based unlock keys
|
||||
func (p *PassphraseUnlockKey) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting passphrase unlock key identity",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("key_type", p.GetType()),
|
||||
)
|
||||
|
||||
// First check if we already have the passphrase
|
||||
passphraseStr := p.Passphrase
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in memory, checking environment")
|
||||
// Check environment variable for passphrase
|
||||
passphraseStr = os.Getenv(EnvUnlockPassphrase)
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in environment, prompting user")
|
||||
// Prompt for passphrase
|
||||
var err error
|
||||
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase", "error", err, "key_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
} else {
|
||||
Debug("Using passphrase from environment", "key_id", p.GetID())
|
||||
}
|
||||
} else {
|
||||
Debug("Using in-memory passphrase", "key_id", p.GetID())
|
||||
}
|
||||
|
||||
// Read encrypted private key of unlock key
|
||||
unlockKeyPrivPath := filepath.Join(p.Directory, "priv.age")
|
||||
Debug("Reading encrypted passphrase unlock key", "path", unlockKeyPrivPath)
|
||||
|
||||
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockKeyPrivPath)
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase unlock key private key", "error", err, "path", unlockKeyPrivPath)
|
||||
return nil, fmt.Errorf("failed to read unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Read encrypted passphrase unlock key",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
||||
)
|
||||
|
||||
Debug("Decrypting unlock key private key with passphrase", "key_id", p.GetID())
|
||||
|
||||
// Decrypt the unlock key private key with passphrase
|
||||
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt unlock key private key", "error", err, "key_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to decrypt unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully decrypted unlock key private key",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.Int("decrypted_length", len(privKeyData)),
|
||||
)
|
||||
|
||||
// Parse the decrypted private key
|
||||
Debug("Parsing decrypted unlock key identity", "key_id", p.GetID())
|
||||
identity, err := age.ParseX25519Identity(string(privKeyData))
|
||||
if err != nil {
|
||||
Debug("Failed to parse unlock key private key", "error", err, "key_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to parse unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully parsed passphrase unlock key identity",
|
||||
slog.String("key_id", p.GetID()),
|
||||
slog.String("public_key", identity.Recipient().String()),
|
||||
)
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
// GetType implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetType() string {
|
||||
return "passphrase"
|
||||
}
|
||||
|
||||
// GetMetadata implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetMetadata() UnlockKeyMetadata {
|
||||
return p.Metadata
|
||||
}
|
||||
|
||||
// GetDirectory implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetDirectory() string {
|
||||
return p.Directory
|
||||
}
|
||||
|
||||
// GetID implements UnlockKey interface
|
||||
func (p *PassphraseUnlockKey) GetID() string {
|
||||
return p.Metadata.ID
|
||||
}
|
||||
|
||||
// ID implements UnlockKey interface - generates ID from creation timestamp
|
||||
func (p *PassphraseUnlockKey) ID() string {
|
||||
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
||||
createdAt := p.Metadata.CreatedAt
|
||||
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
||||
}
|
||||
|
||||
// Remove implements UnlockKey interface - removes the passphrase unlock key
|
||||
func (p *PassphraseUnlockKey) Remove() error {
|
||||
// For passphrase keys, we just need to remove the directory
|
||||
// No external resources (like keychain items) to clean up
|
||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||
return fmt.Errorf("failed to remove passphrase unlock key directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPassphraseUnlockKey creates a new PassphraseUnlockKey instance
|
||||
func NewPassphraseUnlockKey(fs afero.Fs, directory string, metadata UnlockKeyMetadata) *PassphraseUnlockKey {
|
||||
return &PassphraseUnlockKey{
|
||||
Directory: directory,
|
||||
Metadata: metadata,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
||||
func CreatePassphraseKey(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlockKey, error) {
|
||||
// Get current vault
|
||||
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
return currentVault.CreatePassphraseKey(passphrase)
|
||||
}
|
||||
150
internal/secret/passphraseunlocker.go
Normal file
150
internal/secret/passphraseunlocker.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// PassphraseUnlocker represents a passphrase-protected unlocker
|
||||
type PassphraseUnlocker struct {
|
||||
Directory string
|
||||
Metadata UnlockerMetadata
|
||||
fs afero.Fs
|
||||
Passphrase string
|
||||
}
|
||||
|
||||
// GetIdentity implements Unlocker interface for passphrase-based unlockers
|
||||
func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
||||
DebugWith("Getting passphrase unlocker identity",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.String("unlocker_type", p.GetType()),
|
||||
)
|
||||
|
||||
// First check if we already have the passphrase
|
||||
passphraseStr := p.Passphrase
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in memory, checking environment")
|
||||
// Check environment variable for passphrase
|
||||
passphraseStr = os.Getenv(EnvUnlockPassphrase)
|
||||
if passphraseStr == "" {
|
||||
Debug("No passphrase in environment, prompting user")
|
||||
// Prompt for passphrase
|
||||
var err error
|
||||
passphraseStr, err = ReadPassphrase("Enter unlock passphrase: ")
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to read passphrase: %w", err)
|
||||
}
|
||||
} else {
|
||||
Debug("Using passphrase from environment", "unlocker_id", p.GetID())
|
||||
}
|
||||
} else {
|
||||
Debug("Using in-memory passphrase", "unlocker_id", p.GetID())
|
||||
}
|
||||
|
||||
// Read encrypted private key of unlocker
|
||||
unlockerPrivPath := filepath.Join(p.Directory, "priv.age")
|
||||
Debug("Reading encrypted passphrase unlocker", "path", unlockerPrivPath)
|
||||
|
||||
encryptedPrivKeyData, err := afero.ReadFile(p.fs, unlockerPrivPath)
|
||||
if err != nil {
|
||||
Debug("Failed to read passphrase unlocker private key", "error", err, "path", unlockerPrivPath)
|
||||
return nil, fmt.Errorf("failed to read unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Read encrypted passphrase unlocker",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.Int("encrypted_length", len(encryptedPrivKeyData)),
|
||||
)
|
||||
|
||||
Debug("Decrypting unlocker private key with passphrase", "unlocker_id", p.GetID())
|
||||
|
||||
// Decrypt the unlocker private key with passphrase
|
||||
privKeyData, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseStr)
|
||||
if err != nil {
|
||||
Debug("Failed to decrypt unlocker private key", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully decrypted unlocker private key",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.Int("decrypted_length", len(privKeyData)),
|
||||
)
|
||||
|
||||
// Parse the decrypted private key
|
||||
Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID())
|
||||
identity, err := age.ParseX25519Identity(string(privKeyData))
|
||||
if err != nil {
|
||||
Debug("Failed to parse unlocker private key", "error", err, "unlocker_id", p.GetID())
|
||||
return nil, fmt.Errorf("failed to parse unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
DebugWith("Successfully parsed passphrase unlocker identity",
|
||||
slog.String("unlocker_id", p.GetID()),
|
||||
slog.String("public_key", identity.Recipient().String()),
|
||||
)
|
||||
|
||||
return identity, nil
|
||||
}
|
||||
|
||||
// GetType implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetType() string {
|
||||
return "passphrase"
|
||||
}
|
||||
|
||||
// GetMetadata implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetMetadata() UnlockerMetadata {
|
||||
return p.Metadata
|
||||
}
|
||||
|
||||
// GetDirectory implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetDirectory() string {
|
||||
return p.Directory
|
||||
}
|
||||
|
||||
// GetID implements Unlocker interface
|
||||
func (p *PassphraseUnlocker) GetID() string {
|
||||
return p.Metadata.ID
|
||||
}
|
||||
|
||||
// ID implements Unlocker interface - generates ID from creation timestamp
|
||||
func (p *PassphraseUnlocker) ID() string {
|
||||
// Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase
|
||||
createdAt := p.Metadata.CreatedAt
|
||||
return fmt.Sprintf("%s-passphrase", createdAt.Format("2006-01-02.15.04"))
|
||||
}
|
||||
|
||||
// Remove implements Unlocker interface - removes the passphrase unlocker
|
||||
func (p *PassphraseUnlocker) Remove() error {
|
||||
// For passphrase unlockers, we just need to remove the directory
|
||||
// No external resources (like keychain items) to clean up
|
||||
if err := p.fs.RemoveAll(p.Directory); err != nil {
|
||||
return fmt.Errorf("failed to remove passphrase unlocker directory: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPassphraseUnlocker creates a new PassphraseUnlocker instance
|
||||
func NewPassphraseUnlocker(fs afero.Fs, directory string, metadata UnlockerMetadata) *PassphraseUnlocker {
|
||||
return &PassphraseUnlocker{
|
||||
Directory: directory,
|
||||
Metadata: metadata,
|
||||
fs: fs,
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||
func CreatePassphraseUnlocker(fs afero.Fs, stateDir string, passphrase string) (*PassphraseUnlocker, error) {
|
||||
// Get current vault
|
||||
currentVault, err := GetCurrentVault(fs, stateDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current vault: %w", err)
|
||||
}
|
||||
|
||||
return currentVault.CreatePassphraseUnlocker(passphrase)
|
||||
}
|
||||
@@ -123,7 +123,7 @@ func runGPGWithPassphrase(gnupgHome, passphrase string, args []string, input io.
|
||||
return stdout.Bytes(), nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
// UnlockKey interface defines the methods all unlock key types must implement
|
||||
type UnlockKey interface {
|
||||
GetIdentity() (*age.X25519Identity, error)
|
||||
GetType() string
|
||||
GetMetadata() UnlockKeyMetadata
|
||||
GetDirectory() string
|
||||
GetID() string
|
||||
ID() string // Generate ID from the key's public key
|
||||
Remove() error // Remove the unlock key and any associated resources
|
||||
}
|
||||
16
internal/secret/unlocker.go
Normal file
16
internal/secret/unlocker.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
// Unlocker interface defines the methods all unlocker types must implement
|
||||
type Unlocker interface {
|
||||
GetIdentity() (*age.X25519Identity, error)
|
||||
GetType() string
|
||||
GetMetadata() UnlockerMetadata
|
||||
GetDirectory() string
|
||||
GetID() string
|
||||
ID() string // Generate ID from the unlocker's public key
|
||||
Remove() error // Remove the unlocker and any associated resources
|
||||
}
|
||||
@@ -196,10 +196,10 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
|
||||
return nil, fmt.Errorf("failed to create secrets directory: %w", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// GetCurrentUnlockKey returns the current unlock key for this vault
|
||||
func (v *Vault) GetCurrentUnlockKey() (secret.UnlockKey, error) {
|
||||
secret.DebugWith("Getting current unlock key", slog.String("vault_name", v.Name))
|
||||
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory for unlock key", "error", err, "vault_name", v.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
|
||||
|
||||
// Check if the symlink exists
|
||||
_, err = v.fs.Stat(currentUnlockKeyPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to stat current unlock key symlink", "error", err, "path", currentUnlockKeyPath)
|
||||
return nil, fmt.Errorf("failed to read current unlock key: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the symlink to get the target directory
|
||||
var unlockKeyDir string
|
||||
if _, ok := v.fs.(*afero.OsFs); ok {
|
||||
secret.Debug("Resolving unlock key symlink (real filesystem)")
|
||||
// For real filesystems, resolve the symlink properly
|
||||
unlockKeyDir, err = ResolveVaultSymlink(v.fs, currentUnlockKeyPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to resolve unlock key symlink", "error", err, "symlink_path", currentUnlockKeyPath)
|
||||
return nil, fmt.Errorf("failed to resolve current unlock key symlink: %w", err)
|
||||
}
|
||||
} else {
|
||||
secret.Debug("Reading unlock key path (mock filesystem)")
|
||||
// Fallback for mock filesystems: read the path from file contents
|
||||
unlockKeyDirBytes, err := afero.ReadFile(v.fs, currentUnlockKeyPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlock key path file", "error", err, "path", currentUnlockKeyPath)
|
||||
return nil, fmt.Errorf("failed to read current unlock key: %w", err)
|
||||
}
|
||||
unlockKeyDir = strings.TrimSpace(string(unlockKeyDirBytes))
|
||||
}
|
||||
|
||||
secret.DebugWith("Resolved unlock key directory",
|
||||
slog.String("unlock_key_dir", unlockKeyDir),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
// Read unlock key metadata
|
||||
metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
|
||||
secret.Debug("Reading unlock key metadata", "path", metadataPath)
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlock key metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to read unlock key metadata: %w", err)
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
secret.Debug("Failed to parse unlock key metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to parse unlock key metadata: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Parsed unlock key metadata",
|
||||
slog.String("key_id", metadata.ID),
|
||||
slog.String("key_type", metadata.Type),
|
||||
slog.Time("created_at", metadata.CreatedAt),
|
||||
slog.Any("flags", metadata.Flags),
|
||||
)
|
||||
|
||||
// Create unlock key instance using direct constructors with filesystem
|
||||
var unlockKey secret.UnlockKey
|
||||
// Convert our metadata to secret.UnlockKeyMetadata
|
||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
secret.Debug("Creating passphrase unlock key instance", "key_id", metadata.ID)
|
||||
unlockKey = secret.NewPassphraseUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
||||
case "pgp":
|
||||
secret.Debug("Creating PGP unlock key instance", "key_id", metadata.ID)
|
||||
unlockKey = secret.NewPGPUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
||||
case "keychain":
|
||||
secret.Debug("Creating keychain unlock key instance", "key_id", metadata.ID)
|
||||
unlockKey = secret.NewKeychainUnlockKey(v.fs, unlockKeyDir, secretMetadata)
|
||||
default:
|
||||
secret.Debug("Unsupported unlock key type", "type", metadata.Type, "key_id", metadata.ID)
|
||||
return nil, fmt.Errorf("unsupported unlock key type: %s", metadata.Type)
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully created unlock key instance",
|
||||
slog.String("key_type", unlockKey.GetType()),
|
||||
slog.String("key_id", unlockKey.GetID()),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
return unlockKey, nil
|
||||
}
|
||||
|
||||
// ListUnlockKeys returns a list of available unlock keys for this vault
|
||||
func (v *Vault) ListUnlockKeys() ([]UnlockKeyMetadata, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
|
||||
// Check if unlock keys directory exists
|
||||
exists, err := afero.DirExists(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if unlock keys directory exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return []UnlockKeyMetadata{}, nil
|
||||
}
|
||||
|
||||
// List directories in unlock.d
|
||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read unlock keys directory: %w", err)
|
||||
}
|
||||
|
||||
var keys []UnlockKeyMetadata
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
keys = append(keys, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
// RemoveUnlockKey removes an unlock key from this vault
|
||||
func (v *Vault) RemoveUnlockKey(keyID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the key directory and create the unlock key instance
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
|
||||
// List directories in unlock.d
|
||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlock keys directory: %w", err)
|
||||
}
|
||||
|
||||
var unlockKey secret.UnlockKey
|
||||
var keyDir string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == keyID {
|
||||
keyDir = filepath.Join(unlockKeysDir, file.Name())
|
||||
|
||||
// Convert our metadata to secret.UnlockKeyMetadata
|
||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
||||
|
||||
// Create the appropriate unlock key instance
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
unlockKey = secret.NewPassphraseUnlockKey(v.fs, keyDir, secretMetadata)
|
||||
case "pgp":
|
||||
unlockKey = secret.NewPGPUnlockKey(v.fs, keyDir, secretMetadata)
|
||||
case "keychain":
|
||||
unlockKey = secret.NewKeychainUnlockKey(v.fs, keyDir, secretMetadata)
|
||||
default:
|
||||
return fmt.Errorf("unsupported unlock key type: %s", metadata.Type)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unlockKey == nil {
|
||||
return fmt.Errorf("unlock key with ID %s not found", keyID)
|
||||
}
|
||||
|
||||
// Use the unlock key's Remove method
|
||||
return unlockKey.Remove()
|
||||
}
|
||||
|
||||
// SelectUnlockKey selects an unlock key as current for this vault
|
||||
func (v *Vault) SelectUnlockKey(keyID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the unlock key directory by ID
|
||||
unlockKeysDir := filepath.Join(vaultDir, "unlock.d")
|
||||
|
||||
// List directories in unlock.d to find the key
|
||||
files, err := afero.ReadDir(v.fs, unlockKeysDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlock keys directory: %w", err)
|
||||
}
|
||||
|
||||
var targetKeyDir string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockKeysDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockKeyMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == keyID {
|
||||
targetKeyDir = filepath.Join(unlockKeysDir, file.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if targetKeyDir == "" {
|
||||
return fmt.Errorf("unlock key with ID %s not found", keyID)
|
||||
}
|
||||
|
||||
// Create/update current unlock key symlink
|
||||
currentUnlockKeyPath := filepath.Join(vaultDir, "current-unlock-key")
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if exists, _ := afero.Exists(v.fs, currentUnlockKeyPath); exists {
|
||||
if err := v.fs.Remove(currentUnlockKeyPath); err != nil {
|
||||
secret.Debug("Failed to remove existing unlock key symlink", "error", err, "path", currentUnlockKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new symlink
|
||||
return afero.WriteFile(v.fs, currentUnlockKeyPath, []byte(targetKeyDir), secret.FilePerms)
|
||||
}
|
||||
|
||||
// CreatePassphraseKey creates a new passphrase-protected unlock key
|
||||
func (v *Vault) CreatePassphraseKey(passphrase string) (*secret.PassphraseUnlockKey, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Create unlock key directory with timestamp
|
||||
timestamp := time.Now().Format("2006-01-02.15.04")
|
||||
unlockKeyDir := filepath.Join(vaultDir, "unlock.d", "passphrase")
|
||||
if err := v.fs.MkdirAll(unlockKeyDir, secret.DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlock key directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate new age keypair for unlock key
|
||||
unlockIdentity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate unlock key: %w", err)
|
||||
}
|
||||
|
||||
// Write public key
|
||||
pubKeyPath := filepath.Join(unlockKeyDir, "pub.age")
|
||||
if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlock key public key: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt private key with passphrase
|
||||
privKeyData := []byte(unlockIdentity.String())
|
||||
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted private key
|
||||
privKeyPath := filepath.Join(unlockKeyDir, "priv.age")
|
||||
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted unlock key private key: %w", err)
|
||||
}
|
||||
|
||||
// Create metadata
|
||||
keyID := fmt.Sprintf("%s-passphrase", timestamp)
|
||||
metadata := UnlockKeyMetadata{
|
||||
ID: keyID,
|
||||
Type: "passphrase",
|
||||
CreatedAt: time.Now(),
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
// Write metadata
|
||||
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(unlockKeyDir, "unlock-metadata.json")
|
||||
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlock key metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt long-term private key to this unlock key if vault is unlocked
|
||||
if !v.Locked() {
|
||||
ltPrivKey := []byte(v.GetLongTermKey().String())
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||
}
|
||||
|
||||
ltPrivKeyPath := filepath.Join(unlockKeyDir, "longterm.age")
|
||||
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Select this unlock key as current
|
||||
if err := v.SelectUnlockKey(keyID); err != nil {
|
||||
return nil, fmt.Errorf("failed to select new unlock key: %w", err)
|
||||
}
|
||||
|
||||
// Convert our metadata to secret.UnlockKeyMetadata for the constructor
|
||||
secretMetadata := secret.UnlockKeyMetadata(metadata)
|
||||
|
||||
return secret.NewPassphraseUnlockKey(v.fs, unlockKeyDir, secretMetadata), nil
|
||||
}
|
||||
376
internal/vault/unlockers.go
Normal file
376
internal/vault/unlockers.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package vault
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/internal/secret"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// GetCurrentUnlocker returns the current unlocker for this vault
|
||||
func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
|
||||
secret.DebugWith("Getting current unlocker", slog.String("vault_name", v.Name))
|
||||
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
secret.Debug("Failed to get vault directory for unlocker", "error", err, "vault_name", v.Name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||
|
||||
// Check if the symlink exists
|
||||
_, err = v.fs.Stat(currentUnlockerPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to stat current unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Resolve the symlink to get the target directory
|
||||
var unlockerDir string
|
||||
if _, ok := v.fs.(*afero.OsFs); ok {
|
||||
secret.Debug("Resolving unlocker symlink (real filesystem)")
|
||||
// For real filesystems, resolve the symlink properly
|
||||
unlockerDir, err = ResolveVaultSymlink(v.fs, currentUnlockerPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to resolve unlocker symlink", "error", err, "symlink_path", currentUnlockerPath)
|
||||
return nil, fmt.Errorf("failed to resolve current unlocker symlink: %w", err)
|
||||
}
|
||||
} else {
|
||||
secret.Debug("Reading unlocker path (mock filesystem)")
|
||||
// Fallback for mock filesystems: read the path from file contents
|
||||
unlockerDirBytes, err := afero.ReadFile(v.fs, currentUnlockerPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlocker path file", "error", err, "path", currentUnlockerPath)
|
||||
return nil, fmt.Errorf("failed to read current unlocker: %w", err)
|
||||
}
|
||||
unlockerDir = strings.TrimSpace(string(unlockerDirBytes))
|
||||
}
|
||||
|
||||
secret.DebugWith("Resolved unlocker directory",
|
||||
slog.String("unlocker_dir", unlockerDir),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
// Read unlocker metadata
|
||||
metadataPath := filepath.Join(unlockerDir, "unlock-metadata.json")
|
||||
secret.Debug("Reading unlocker metadata", "path", metadataPath)
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
secret.Debug("Failed to read unlocker metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to read unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
secret.Debug("Failed to parse unlocker metadata", "error", err, "path", metadataPath)
|
||||
return nil, fmt.Errorf("failed to parse unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
secret.DebugWith("Parsed unlocker metadata",
|
||||
slog.String("unlocker_id", metadata.ID),
|
||||
slog.String("unlocker_type", metadata.Type),
|
||||
slog.Time("created_at", metadata.CreatedAt),
|
||||
slog.Any("flags", metadata.Flags),
|
||||
)
|
||||
|
||||
// Create unlocker instance using direct constructors with filesystem
|
||||
var unlocker secret.Unlocker
|
||||
// Convert our metadata to secret.UnlockerMetadata
|
||||
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
secret.Debug("Creating passphrase unlocker instance", "unlocker_id", metadata.ID)
|
||||
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||
case "pgp":
|
||||
secret.Debug("Creating PGP unlocker instance", "unlocker_id", metadata.ID)
|
||||
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||
case "keychain":
|
||||
secret.Debug("Creating keychain unlocker instance", "unlocker_id", metadata.ID)
|
||||
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDir, secretMetadata)
|
||||
default:
|
||||
secret.Debug("Unsupported unlocker type", "type", metadata.Type, "unlocker_id", metadata.ID)
|
||||
return nil, fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
|
||||
}
|
||||
|
||||
secret.DebugWith("Successfully created unlocker instance",
|
||||
slog.String("unlocker_type", unlocker.GetType()),
|
||||
slog.String("unlocker_id", unlocker.GetID()),
|
||||
slog.String("vault_name", v.Name),
|
||||
)
|
||||
|
||||
return unlocker, nil
|
||||
}
|
||||
|
||||
// ListUnlockers returns a list of available unlockers for this vault
|
||||
func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// Check if unlockers directory exists
|
||||
exists, err := afero.DirExists(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if unlockers directory exists: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
return []UnlockerMetadata{}, nil
|
||||
}
|
||||
|
||||
// List directories in unlockers.d
|
||||
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
var unlockers []UnlockerMetadata
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
unlockers = append(unlockers, metadata)
|
||||
}
|
||||
}
|
||||
|
||||
return unlockers, nil
|
||||
}
|
||||
|
||||
// RemoveUnlocker removes an unlocker from this vault
|
||||
func (v *Vault) RemoveUnlocker(unlockerID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the unlocker directory and create the unlocker instance
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// List directories in unlockers.d
|
||||
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
var unlocker secret.Unlocker
|
||||
var unlockerDirPath string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == unlockerID {
|
||||
unlockerDirPath = filepath.Join(unlockersDir, file.Name())
|
||||
|
||||
// Convert our metadata to secret.UnlockerMetadata
|
||||
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||
|
||||
// Create the appropriate unlocker instance
|
||||
switch metadata.Type {
|
||||
case "passphrase":
|
||||
unlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||
case "pgp":
|
||||
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||
case "keychain":
|
||||
unlocker = secret.NewKeychainUnlocker(v.fs, unlockerDirPath, secretMetadata)
|
||||
default:
|
||||
return fmt.Errorf("unsupported unlocker type: %s", metadata.Type)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unlocker == nil {
|
||||
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||
}
|
||||
|
||||
// Use the unlocker's Remove method
|
||||
return unlocker.Remove()
|
||||
}
|
||||
|
||||
// SelectUnlocker selects an unlocker as current for this vault
|
||||
func (v *Vault) SelectUnlocker(unlockerID string) error {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the unlocker directory by ID
|
||||
unlockersDir := filepath.Join(vaultDir, "unlockers.d")
|
||||
|
||||
// List directories in unlockers.d to find the unlocker
|
||||
files, err := afero.ReadDir(v.fs, unlockersDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read unlockers directory: %w", err)
|
||||
}
|
||||
|
||||
var targetUnlockerDir string
|
||||
for _, file := range files {
|
||||
if file.IsDir() {
|
||||
// Read metadata file
|
||||
metadataPath := filepath.Join(unlockersDir, file.Name(), "unlock-metadata.json")
|
||||
exists, err := afero.Exists(v.fs, metadataPath)
|
||||
if err != nil || !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
metadataBytes, err := afero.ReadFile(v.fs, metadataPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var metadata UnlockerMetadata
|
||||
if err := json.Unmarshal(metadataBytes, &metadata); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.ID == unlockerID {
|
||||
targetUnlockerDir = filepath.Join(unlockersDir, file.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if targetUnlockerDir == "" {
|
||||
return fmt.Errorf("unlocker with ID %s not found", unlockerID)
|
||||
}
|
||||
|
||||
// Create/update current unlocker symlink
|
||||
currentUnlockerPath := filepath.Join(vaultDir, "current-unlocker")
|
||||
|
||||
// Remove existing symlink if it exists
|
||||
if exists, _ := afero.Exists(v.fs, currentUnlockerPath); exists {
|
||||
if err := v.fs.Remove(currentUnlockerPath); err != nil {
|
||||
secret.Debug("Failed to remove existing unlocker symlink", "error", err, "path", currentUnlockerPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Create new symlink
|
||||
return afero.WriteFile(v.fs, currentUnlockerPath, []byte(targetUnlockerDir), secret.FilePerms)
|
||||
}
|
||||
|
||||
// CreatePassphraseUnlocker creates a new passphrase-protected unlocker
|
||||
func (v *Vault) CreatePassphraseUnlocker(passphrase string) (*secret.PassphraseUnlocker, error) {
|
||||
vaultDir, err := v.GetDirectory()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vault directory: %w", err)
|
||||
}
|
||||
|
||||
// Create unlocker directory with timestamp
|
||||
timestamp := time.Now().Format("2006-01-02.15.04")
|
||||
unlockerDir := filepath.Join(vaultDir, "unlockers.d", "passphrase")
|
||||
if err := v.fs.MkdirAll(unlockerDir, secret.DirPerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to create unlocker directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate new age keypair for unlocker
|
||||
unlockerIdentity, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Write public key
|
||||
pubKeyPath := filepath.Join(unlockerDir, "pub.age")
|
||||
if err := afero.WriteFile(v.fs, pubKeyPath, []byte(unlockerIdentity.Recipient().String()), secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlocker public key: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt private key with passphrase
|
||||
privKeyData := []byte(unlockerIdentity.String())
|
||||
encryptedPrivKey, err := secret.EncryptWithPassphrase(privKeyData, passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
// Write encrypted private key
|
||||
privKeyPath := filepath.Join(unlockerDir, "priv.age")
|
||||
if err := afero.WriteFile(v.fs, privKeyPath, encryptedPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted unlocker private key: %w", err)
|
||||
}
|
||||
|
||||
// Create metadata
|
||||
unlockerID := fmt.Sprintf("%s-passphrase", timestamp)
|
||||
metadata := UnlockerMetadata{
|
||||
ID: unlockerID,
|
||||
Type: "passphrase",
|
||||
CreatedAt: time.Now(),
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
// Write metadata
|
||||
metadataBytes, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
metadataPath := filepath.Join(unlockerDir, "unlock-metadata.json")
|
||||
if err := afero.WriteFile(v.fs, metadataPath, metadataBytes, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write unlocker metadata: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt long-term private key to this unlocker if vault is unlocked
|
||||
if !v.Locked() {
|
||||
ltPrivKey := []byte(v.GetLongTermKey().String())
|
||||
encryptedLtPrivKey, err := secret.EncryptToRecipient(ltPrivKey, unlockerIdentity.Recipient())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt long-term private key: %w", err)
|
||||
}
|
||||
|
||||
ltPrivKeyPath := filepath.Join(unlockerDir, "longterm.age")
|
||||
if err := afero.WriteFile(v.fs, ltPrivKeyPath, encryptedLtPrivKey, secret.FilePerms); err != nil {
|
||||
return nil, fmt.Errorf("failed to write encrypted long-term private key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Select this unlocker as current
|
||||
if err := v.SelectUnlocker(unlockerID); err != nil {
|
||||
return nil, fmt.Errorf("failed to select new unlocker: %w", err)
|
||||
}
|
||||
|
||||
// Convert our metadata to secret.UnlockerMetadata for the constructor
|
||||
secretMetadata := secret.UnlockerMetadata(metadata)
|
||||
|
||||
return secret.NewPassphraseUnlocker(v.fs, unlockerDir, secretMetadata), nil
|
||||
}
|
||||
@@ -83,32 +83,32 @@ func (v *Vault) GetOrDeriveLongTermKey() (*age.X25519Identity, error) {
|
||||
return ltIdentity, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user