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 was merged in pull request #94.
This commit is contained in:
2026-10-04 16:07:57 +02:00
parent 5ca615a7a6
commit db7d2c952e
42 changed files with 727 additions and 428 deletions
+34 -19
View File
@@ -19,6 +19,17 @@ import (
const testMnemonic = "abandon abandon abandon abandon abandon abandon " +
"abandon abandon abandon abandon abandon about"
// testMnemonicBuffer returns testMnemonic in a locked buffer that is
// destroyed when the test ends.
func testMnemonicBuffer(t *testing.T) *memguard.LockedBuffer {
t.Helper()
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
t.Cleanup(mnemonic.Destroy)
return mnemonic
}
// writeTestPublicKey writes the unlocker public key and verifies it exists.
func writeTestPublicKey(
t *testing.T, fs afero.Fs, unlockerDir string, agePublicKey string,
@@ -163,7 +174,7 @@ func newTestPassphraseUnlocker(
return unlocker, ageIdentity, unlockerDir
}
//nolint:paralleltest // subtests share real-FS state and t.Setenv, order matters
//nolint:paralleltest // subtests share real-FS state, order matters
func TestPassphraseUnlockerWithRealFS(t *testing.T) {
// This test uses real filesystem
if os.Getenv("CI") == "true" {
@@ -195,38 +206,42 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) {
writeTestLongTermKey(t, fs, unlockerDir, agePublicKey)
})
// Set test environment variable (cleaned up automatically)
t.Setenv(secret.EnvUnlockPassphrase, testPassphrase)
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
defer passphrase.Destroy()
// Test getting identity from environment variable
t.Run("GetIdentityFromEnv", func(t *testing.T) {
identity, err := unlocker.GetIdentity()
if err != nil {
t.Fatalf("Failed to get identity from env: %v", err)
}
unlocker.Passphrase = passphrase
// Verify the identity matches what we expect
expectedPubKey := ageIdentity.Recipient().String()
// Test getting identity with the passphrase the unlocker was given,
// twice: using it must leave it intact for the next use
t.Run("GetIdentityWithPassphrase", func(t *testing.T) {
for range 2 {
identity, err := unlocker.GetIdentity()
if err != nil {
t.Fatalf("Failed to get identity with passphrase: %v", err)
}
actualPubKey := identity.Recipient().String()
if actualPubKey != expectedPubKey {
t.Errorf("Public key mismatch. Expected %s, got %s",
expectedPubKey, actualPubKey)
// Verify the identity matches what we expect
expectedPubKey := ageIdentity.Recipient().String()
actualPubKey := identity.Recipient().String()
if actualPubKey != expectedPubKey {
t.Errorf("Public key mismatch. Expected %s, got %s",
expectedPubKey, actualPubKey)
}
}
})
// Unset the environment variable to test interactive prompt
_ = os.Unsetenv(secret.EnvUnlockPassphrase)
unlocker.Passphrase = nil
// Test getting identity from prompt (this would require mocking the
// prompt). For real integration tests, we'd need a way to mock the
// passphrase input. Here we just verify the error is what we expect
// when no passphrase is available.
t.Run("GetIdentityWithoutEnv", func(t *testing.T) {
t.Run("GetIdentityWithoutPassphrase", func(t *testing.T) {
// This should fail since we're not in an interactive terminal
_, err := unlocker.GetIdentity()
if err == nil {
t.Errorf("Should have failed to get identity without passphrase env var")
t.Errorf("Should have failed to get identity without a passphrase")
}
})