check / check (push) Failing after 3s
internal/cli's copies of vault.ErrSecretNotFound, ErrVaultNotFound, ErrVersionNotFound and ErrSecretExists are removed; the commands wrap the vault errors. errUnsupportedUnlockerType is removed for errInvalidUnlockerType, which names the same failure. Every error of secret.ReadPassphrase wraps ErrPassphraseNotRead, so its callers no longer add those words. ResolveGPGKeyFingerprint returns ErrGPGKeyNotFound for a key the keyring lacks, recognised by gpg's status line. storeInKeychain returns errNilDataBuffer. bip85's ErrPasswordTooShort and ErrEncodedTooShort go with their unreachable checks. Tests that matched these errors' text use errors.Is. Model: opus-5-5
179 lines
5.3 KiB
Go
179 lines
5.3 KiB
Go
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")
|
|
|
|
// 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: 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
|
|
// 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 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)
|
|
}
|