package secret import ( "fmt" "log/slog" "os" "path/filepath" "filippo.io/age" "github.com/awnumar/memguard" "github.com/spf13/afero" ) // PassphraseUnlocker represents a passphrase-protected unlocker type PassphraseUnlocker struct { Directory string Metadata UnlockerMetadata fs afero.Fs Passphrase *memguard.LockedBuffer // Secure buffer for passphrase } // NewPassphraseUnlocker creates a new PassphraseUnlocker instance func NewPassphraseUnlocker( fs afero.Fs, directory string, metadata UnlockerMetadata, ) *PassphraseUnlocker { return &PassphraseUnlocker{ Directory: directory, Metadata: metadata, fs: fs, } } // 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()), ) passphraseBuffer, err := p.getPassphrase() if err != nil { return nil, err } defer passphraseBuffer.Destroy() // 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 privKeyBuffer, err := DecryptWithPassphrase(encryptedPrivKeyData, passphraseBuffer) 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) } defer privKeyBuffer.Destroy() DebugWith("Successfully decrypted unlocker private key", slog.String("unlocker_id", p.GetID()), slog.Int("decrypted_length", privKeyBuffer.Size()), ) // Parse the decrypted private key Debug("Parsing decrypted unlocker identity", "unlocker_id", p.GetID()) identity, err := age.ParseX25519Identity(privKeyBuffer.String()) 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 - generates ID from creation timestamp func (p *PassphraseUnlocker) GetID() string { // Generate ID using creation timestamp: YYYY-MM-DD.HH.mm-passphrase createdAt := p.Metadata.CreatedAt return createdAt.Format("2006-01-02.15.04") + "-passphrase" } // Remove implements Unlocker interface - removes the passphrase unlocker func (p *PassphraseUnlocker) Remove() error { // Clean up the passphrase from memory if it exists if p.Passphrase != nil && p.Passphrase.IsAlive() { p.Passphrase.Destroy() } // For passphrase unlockers, we just need to remove the directory // No external resources (like keychain items) to clean up err := RemoveDirAtomic(p.fs, p.Directory) if err != nil { return fmt.Errorf("failed to remove passphrase unlocker directory: %w", err) } return nil } // getPassphrase retrieves the passphrase from memory, environment, or // user input. Returns a LockedBuffer for secure memory handling func (p *PassphraseUnlocker) getPassphrase() (*memguard.LockedBuffer, error) { // First check if we already have the passphrase if p.Passphrase != nil && p.Passphrase.IsAlive() { Debug("Using in-memory passphrase", "unlocker_id", p.GetID()) // Return a copy of the passphrase buffer return memguard.NewBufferFromBytes(p.Passphrase.Bytes()), nil } Debug("No passphrase in memory, checking environment") // Check environment variable for passphrase passphraseStr := os.Getenv(EnvUnlockPassphrase) if passphraseStr != "" { Debug("Using passphrase from environment", "unlocker_id", p.GetID()) // Convert to secure buffer secureBuffer := memguard.NewBufferFromBytes([]byte(passphraseStr)) return secureBuffer, nil } Debug("No passphrase in environment, prompting user") // Prompt for passphrase secureBuffer, 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) } return secureBuffer, nil } // CreatePassphraseUnlocker creates a new passphrase-protected unlocker // The passphrase must be provided as a LockedBuffer for security func CreatePassphraseUnlocker( fs afero.Fs, stateDir string, passphrase *memguard.LockedBuffer, ) (*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) }