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
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
// Package cli implements the command-line interface for the secret application.
|
|
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Instance encapsulates all CLI functionality and state
|
|
type Instance struct {
|
|
fs afero.Fs
|
|
stateDir string
|
|
cmd *cobra.Command
|
|
// Mnemonic and UnlockPassphrase hold the values of SB_SECRET_MNEMONIC
|
|
// and SB_UNLOCK_PASSPHRASE that readSecretEnv read, or nil when it found
|
|
// none.
|
|
Mnemonic *memguard.LockedBuffer
|
|
UnlockPassphrase *memguard.LockedBuffer
|
|
}
|
|
|
|
// NewCLIInstance creates a new CLI instance with the real filesystem
|
|
func NewCLIInstance() (*Instance, error) {
|
|
fs := afero.NewOsFs()
|
|
|
|
stateDir, err := secret.DetermineStateDir("")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot determine state directory: %w", err)
|
|
}
|
|
|
|
return &Instance{
|
|
fs: fs,
|
|
stateDir: stateDir,
|
|
}, nil
|
|
}
|
|
|
|
// NewCLIInstanceWithFs creates a new CLI instance with the given
|
|
// filesystem (for testing)
|
|
func NewCLIInstanceWithFs(fs afero.Fs) (*Instance, error) {
|
|
stateDir, err := secret.DetermineStateDir("")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot determine state directory: %w", err)
|
|
}
|
|
|
|
return &Instance{
|
|
fs: fs,
|
|
stateDir: stateDir,
|
|
}, nil
|
|
}
|
|
|
|
// NewCLIInstanceWithStateDir creates a new CLI instance with custom state
|
|
// directory (for testing)
|
|
func NewCLIInstanceWithStateDir(fs afero.Fs, stateDir string) *Instance {
|
|
return &Instance{
|
|
fs: fs,
|
|
stateDir: stateDir,
|
|
}
|
|
}
|
|
|
|
// SetFilesystem sets the filesystem for this CLI instance (for testing)
|
|
func (cli *Instance) SetFilesystem(fs afero.Fs) {
|
|
cli.fs = fs
|
|
}
|
|
|
|
// SetStateDir sets the state directory for this CLI instance (for testing)
|
|
func (cli *Instance) SetStateDir(stateDir string) {
|
|
cli.stateDir = stateDir
|
|
}
|
|
|
|
// GetStateDir returns the state directory for this CLI instance
|
|
func (cli *Instance) GetStateDir() string {
|
|
return cli.stateDir
|
|
}
|
|
|
|
// readSecretEnv reads SB_SECRET_MNEMONIC into cli.Mnemonic and
|
|
// SB_UNLOCK_PASSPHRASE into cli.UnlockPassphrase. A command that may need
|
|
// either calls it once, before anything else, and passes the buffers on
|
|
// from there: each variable is unset as soon as it is read, so that the
|
|
// processes this one starts, gpg among them, do not inherit it, and a
|
|
// second read would find nothing. The returned function destroys both
|
|
// buffers.
|
|
func (cli *Instance) readSecretEnv() func() {
|
|
cli.Mnemonic = readAndUnsetEnv(secret.EnvMnemonic)
|
|
cli.UnlockPassphrase = readAndUnsetEnv(secret.EnvUnlockPassphrase)
|
|
|
|
mnemonic, passphrase := cli.Mnemonic, cli.UnlockPassphrase
|
|
|
|
return func() {
|
|
if mnemonic != nil {
|
|
mnemonic.Destroy()
|
|
}
|
|
|
|
if passphrase != nil {
|
|
passphrase.Destroy()
|
|
}
|
|
}
|
|
}
|
|
|
|
// readAndUnsetEnv returns the value of the environment variable name in a
|
|
// locked buffer, or nil when it is unset or empty, and unsets the variable.
|
|
// Unsetting does not erase the value: it stays in this process's memory,
|
|
// and in /proc/<pid>/environ, which shows the environment the process
|
|
// started with. The caller must destroy the returned buffer.
|
|
func readAndUnsetEnv(name string) *memguard.LockedBuffer {
|
|
value := os.Getenv(name)
|
|
_ = os.Unsetenv(name)
|
|
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
|
|
return memguard.NewBufferFromBytes([]byte(value))
|
|
}
|