Read secret environment variables once per command, then unset them (closes #60)
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
This commit is contained in:
2026-10-04 13:59:35 +00:00
committed by sneak
parent 5ca615a7a6
commit bf01308933
42 changed files with 727 additions and 428 deletions
+19 -18
View File
@@ -39,16 +39,20 @@ func RunInit(cmd *cobra.Command, _ []string) error {
log.Fatalf("failed to initialize CLI: %v", err)
}
destroySecrets := cli.readSecretEnv()
defer destroySecrets()
return cli.Init(cmd)
}
// promptMnemonic reads the mnemonic from the environment or interactively.
// The returned cleanup function must be deferred by the caller.
func promptMnemonic() (string, func(), error) {
if envMnemonic := os.Getenv(secret.EnvMnemonic); envMnemonic != "" {
// promptMnemonic returns the mnemonic from the environment, cli.Mnemonic,
// or reads it interactively. The returned cleanup function must be deferred
// by the caller.
func (cli *Instance) promptMnemonic() (*memguard.LockedBuffer, func(), error) {
if cli.Mnemonic != nil {
secret.Debug("Using mnemonic from environment variable")
return envMnemonic, func() {}, nil
return cli.Mnemonic, func() {}, nil
}
secret.Debug("Prompting user for mnemonic phrase")
@@ -58,23 +62,23 @@ func promptMnemonic() (string, func(), error) {
if err != nil {
secret.Debug("Failed to read mnemonic from stdin", "error", err)
return "", nil, fmt.Errorf("failed to read mnemonic: %w", err)
return nil, nil, fmt.Errorf("failed to read mnemonic: %w", err)
}
fmt.Fprintln(os.Stderr) // Add newline after hidden input
return mnemonicBuffer.String(), mnemonicBuffer.Destroy, nil
return mnemonicBuffer, mnemonicBuffer.Destroy, nil
}
// setupDefaultVault creates the default vault and derives its long-term
// identity from the mnemonic
func (cli *Instance) setupDefaultVault(
stateDir, mnemonicStr string,
stateDir string, mnemonic *memguard.LockedBuffer,
) (*vault.Vault, *age.X25519Identity, error) {
// Create the default vault - it will handle key derivation internally
secret.Debug("Creating default vault")
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default")
vlt, err := vault.CreateVault(cli.fs, cli.stateDir, "default", mnemonic)
if err != nil {
secret.Debug("Failed to create default vault", "error", err)
@@ -92,7 +96,7 @@ func (cli *Instance) setupDefaultVault(
}
// Derive the long-term key using the same index that CreateVault used
ltIdentity, err := agehd.DeriveIdentity(mnemonicStr, metadata.DerivationIndex)
ltIdentity, err := agehd.DeriveIdentity(mnemonic.String(), metadata.DerivationIndex)
if err != nil {
secret.Debug("Failed to derive long-term key", "error", err)
@@ -136,12 +140,13 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
}
// Prompt for mnemonic
mnemonicStr, cleanupMnemonic, err := promptMnemonic()
mnemonic, cleanupMnemonic, err := cli.promptMnemonic()
if err != nil {
return err
}
defer cleanupMnemonic()
mnemonicStr := mnemonic.String()
if mnemonicStr == "" {
secret.Debug("Empty mnemonic provided")
@@ -162,18 +167,14 @@ func (cli *Instance) initialize(cmd *cobra.Command) error {
// Ask for the unlocker passphrase before creating the vault, so that
// stopping at the prompt leaves no vault without an unlocker behind
passphraseBuffer, err := resolvePassphrase()
passphraseBuffer, cleanupPassphrase, err := cli.resolvePassphrase()
if err != nil {
return err
}
defer passphraseBuffer.Destroy()
// Set mnemonic in environment for CreateVault to use
restoreMnemonicEnv := setMnemonicEnv(mnemonicStr)
defer restoreMnemonicEnv()
defer cleanupPassphrase()
// Create the default vault and derive its long-term key
vlt, ltIdentity, err := cli.setupDefaultVault(stateDir, mnemonicStr)
vlt, ltIdentity, err := cli.setupDefaultVault(stateDir, mnemonic)
if err != nil {
return err
}