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.
Nothing below the command reads the environment. README warns against
both variables.

Model: opus-5-5
This commit is contained in:
2026-10-04 12:28:18 +00:00
committed by sneak
parent 007254a1f0
commit 63928ea745
40 changed files with 679 additions and 415 deletions
+24
View File
@@ -81,6 +81,9 @@ func newAddCmd() *cobra.Command {
cli.cmd = cmd // Set the command for stdin access
destroySecrets := cli.readSecretEnv()
defer destroySecrets()
secret.Debug("Created CLI instance, calling AddSecret")
return cli.AddSecret(args[0], force)
@@ -111,6 +114,9 @@ func newGetCmd() *cobra.Command {
return fmt.Errorf("failed to initialize CLI: %w", err)
}
destroySecrets := cli.readSecretEnv()
defer destroySecrets()
// Without --version, get the current version. A given
// --version is checked as typed, so an empty one is rejected.
if !cmd.Flags().Changed("version") {
@@ -174,6 +180,9 @@ func newImportCmd() *cobra.Command {
return fmt.Errorf("failed to initialize CLI: %w", err)
}
destroySecrets := cli.readSecretEnv()
defer destroySecrets()
return cli.ImportSecret(cmd, args[0], sourceFile, force)
},
}
@@ -248,6 +257,9 @@ The source secret is deleted after successful copy.`,
return fmt.Errorf("failed to initialize CLI: %w", err)
}
destroySecrets := cli.readSecretEnv()
defer destroySecrets()
return cli.MoveSecret(cmd, args[0], args[1], force)
},
}
@@ -354,6 +366,8 @@ func (cli *Instance) AddSecret(secretName string, force bool) error {
return err
}
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
secret.Debug("Got current vault", "vault_name", vlt.GetName())
// Read secret value directly into protected buffers
@@ -420,6 +434,8 @@ func (cli *Instance) GetSecret(cmd *cobra.Command, secretName string) error {
return err
}
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
value, err := vlt.GetSecret(secretName)
if err != nil {
return err
@@ -448,6 +464,8 @@ func (cli *Instance) GetSecretWithVersion(
return err
}
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
// Get the secret value
value, err := vlt.GetSecretVersion(secretName, version)
if err != nil {
@@ -633,6 +651,8 @@ func (cli *Instance) ImportSecret(
return err
}
vlt.Mnemonic, vlt.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
// Read secret value from the source file into protected buffers
file, err := cli.fs.Open(sourceFile)
if err != nil {
@@ -993,6 +1013,10 @@ func (cli *Instance) moveSecretCrossVault(
destVault.Name, destSecretName)
}
// Copying needs the long-term keys of both vaults
srcVault.Mnemonic, srcVault.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
destVault.Mnemonic, destVault.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
// Unlock destination vault (will fail if neither mnemonic nor unlocker available)
_, err = destVault.GetOrDeriveLongTermKey()
if err != nil {