// 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//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)) }