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
+23 -16
View File
@@ -3,7 +3,6 @@ package vault
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
@@ -11,6 +10,7 @@ import (
"git.eeqj.de/sneak/secret/internal/secret"
"git.eeqj.de/sneak/secret/pkg/agehd"
"github.com/awnumar/memguard"
"github.com/spf13/afero"
)
@@ -155,19 +155,18 @@ func ListVaults(fs afero.Fs, stateDir string) ([]string, error) {
// It returns the derivation index, public key hash, and family hash.
func processMnemonicForVault(
fs afero.Fs, stateDir, vaultDir, vaultName string,
mnemonicBuffer *memguard.LockedBuffer,
) (uint32, string, string, error) {
// Check if mnemonic is available in environment
mnemonic := os.Getenv(secret.EnvMnemonic)
if mnemonic == "" {
secret.Debug("No mnemonic in environment, vault created without long-term key",
if mnemonicBuffer == nil {
secret.Debug("No mnemonic given, vault created without long-term key",
"vault", vaultName)
// Use 0 for derivation index when no mnemonic is provided
return 0, "", "", nil
}
secret.Debug("Mnemonic found in environment, deriving long-term key",
"vault", vaultName)
mnemonic := mnemonicBuffer.String()
secret.Debug("Mnemonic given, deriving long-term key", "vault", vaultName)
// Get the next available derivation index for this mnemonic
derivationIndex, err := GetNextDerivationIndex(fs, stateDir, mnemonic)
@@ -208,12 +207,17 @@ func processMnemonicForVault(
return derivationIndex, publicKeyHash, familyHash, nil
}
// CreateVault creates a new vault and selects it as the current vault. It
// refuses a vault that already exists before writing anything: creating it
// again would replace its keys, and its secrets could no longer be
// decrypted. The commands that call it hold the state directory lock, so no
// other command can create the vault between the check and the writes.
func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
// CreateVault creates a new vault and selects it as the current vault. When
// mnemonic is not nil, the vault's long-term key is derived from it, and the
// returned vault has it as its Mnemonic; when it is nil, the vault has no
// long-term key until one is imported. It refuses a vault that already
// exists before writing anything: creating it again would replace its keys,
// and its secrets could no longer be decrypted. The commands that call it
// hold the state directory lock, so no other command can create the vault
// between the check and the writes.
func CreateVault(
fs afero.Fs, stateDir string, name string, mnemonic *memguard.LockedBuffer,
) (*Vault, error) {
secret.Debug("Creating new vault", "name", name, "state_dir", stateDir)
err := ValidateVaultName(name)
@@ -263,7 +267,7 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
// Process mnemonic if available
derivationIndex, publicKeyHash, familyHash, err := processMnemonicForVault(
fs, stateDir, vaultDir, name)
fs, stateDir, vaultDir, name, mnemonic)
if err != nil {
return nil, err
}
@@ -292,7 +296,10 @@ func CreateVault(fs afero.Fs, stateDir string, name string) (*Vault, error) {
// Create and return the vault
secret.Debug("Successfully created vault", "name", name)
return NewVault(fs, stateDir, name), nil
vlt := NewVault(fs, stateDir, name)
vlt.Mnemonic = mnemonic
return vlt, nil
}
// SelectVault selects the given vault as the current vault