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