check / check (push) Failing after 1s
init and vault create put the mnemonic into the process environment for vault.CreateVault to read back, so every program they ran, gpg included, inherited it, and SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE were read at 13 places and never unset. Each command that may need them now reads both once, in its RunE, into locked buffers on the CLI Instance, and unsets them at once. The buffers are passed down: vault.CreateVault takes the mnemonic, a Vault carries Mnemonic and UnlockPassphrase, and the PGP, keychain and Secure Enclave unlocker constructors take both; CreatePGPUnlocker sets them on the vault it loads through SetMnemonic and SetUnlockPassphrase, new in VaultInterface. README warns against both variables. Model: opus-5-5
176 lines
5.2 KiB
Go
176 lines
5.2 KiB
Go
package secret
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"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 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, 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)
|
|
}
|