package secret import ( "errors" "fmt" "log/slog" "path/filepath" "filippo.io/age" "github.com/awnumar/memguard" "github.com/spf13/afero" ) // ErrPassphraseNotRead is wrapped in every error of ReadPassphrase: there // is no terminal to read the passphrase from, reading it failed, or it was // empty. A passphrase unlocker that fails with it was not tried. var ErrPassphraseNotRead = errors.New("failed to read passphrase") // ErrWrongPassphrase is wrapped in the error of GetIdentity when the // passphrase does not decrypt the unlocker's private key. var ErrWrongPassphrase = errors.New("wrong passphrase") // 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()) // age says that no identity matched when the passphrase is wrong var noMatch *age.NoIdentityMatchError if errors.As(err, &noMatch) { return nil, fmt.Errorf("%w for unlocker %s", ErrWrongPassphrase, 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: the name of the unlocker's directory func (p *PassphraseUnlocker) GetID() string { return filepath.Base(p.Directory) } // 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 err := RemoveDirAtomic(p.fs, p.Directory) if err != nil { return fmt.Errorf("failed to remove passphrase unlocker directory: %w", err) } return nil } // getPassphrase returns a copy of p.Passphrase, or else asks the user for // the passphrase. The caller must destroy the returned buffer. func (p *PassphraseUnlocker) getPassphrase() (*memguard.LockedBuffer, error) { if p.Passphrase != nil && p.Passphrase.IsAlive() { Debug("Using in-memory passphrase", "unlocker_id", p.GetID()) // Not NewBufferFromBytes, which would wipe p.Passphrase passphrase := memguard.NewBuffer(p.Passphrase.Size()) passphrase.Copy(p.Passphrase.Bytes()) return passphrase, nil } Debug("No passphrase in memory, 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, 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) }