check / check (push) Waiting to run
init, vault create, unlocker add, and add, import and generate secret with --force asked for the mnemonic or passphrase, or unlocked the vault, while holding the state directory lock, so every other command that changes anything waited at the prompt. They now ask and unlock first and lock after. Under the lock, unlocker add fails if the vault was removed, or created again with another key, and refuses a GPG key added meanwhile; a forced add of a secret created meanwhile lets go of the lock, gets the key and locks again. secret.CreatePGPUnlocker takes the unlocked vault; SetMnemonic and SetUnlockPassphrase are gone. Not done: secret move between vaults still unlocks both under the lock (#189). Model: opus-5-5
128 lines
4.1 KiB
Go
128 lines
4.1 KiB
Go
package secret_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/secret/internal/secret"
|
|
"sneak.berlin/go/secret/internal/vault"
|
|
)
|
|
|
|
// The GPG key ID and fingerprint passed to CreatePGPUnlocker.
|
|
const (
|
|
testGPGKeyID = "0123456789ABCDEF"
|
|
testGPGFingerprint = "0123456789ABCDEF0123456789ABCDEF01234567"
|
|
)
|
|
|
|
// fakeGPGScript is a gpg for which `gpg --version` succeeds and anything
|
|
// else fails.
|
|
const fakeGPGScript = `#!/bin/sh
|
|
[ "$*" = --version ]
|
|
`
|
|
|
|
// installFakeGPG makes fakeGPGScript the only gpg on PATH for the test.
|
|
func installFakeGPG(t *testing.T) {
|
|
t.Helper()
|
|
|
|
dir := t.TempDir()
|
|
|
|
//nolint:gosec // G306: the script must be executable
|
|
err := os.WriteFile(filepath.Join(dir, "gpg"), []byte(fakeGPGScript), 0o700)
|
|
require.NoError(t, err)
|
|
|
|
t.Setenv("PATH", dir)
|
|
}
|
|
|
|
// TestCreatePGPUnlockerFailureWritesNothing makes CreatePGPUnlocker fail at
|
|
// getting the vault's long-term key, which used to come after part of the
|
|
// unlocker was written, and asserts that nothing is written. Getting the key
|
|
// fails because there is no mnemonic and no current unlocker.
|
|
//
|
|
//nolint:paralleltest // installFakeGPG uses t.Setenv
|
|
func TestCreatePGPUnlockerFailureWritesNothing(t *testing.T) {
|
|
installFakeGPG(t)
|
|
|
|
base := afero.NewMemMapFs()
|
|
vlt, err := vault.CreateVault(base, testVaultStateDir, testVaultName, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
fs := hookFs{Fs: base, before: func(_, path string) error {
|
|
t.Errorf("changed %s", path)
|
|
|
|
return nil
|
|
}}
|
|
|
|
_, err = secret.CreatePGPUnlocker(
|
|
vault.NewVault(fs, testVaultStateDir, testVaultName),
|
|
testGPGKeyID, testGPGFingerprint)
|
|
require.Error(t, err)
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
require.NoError(t, err)
|
|
assert.Empty(t, dirNames(t, base, filepath.Join(vaultDir, "unlockers.d")))
|
|
}
|
|
|
|
// TestPGPUnlockerAddedTwiceKeepsFirst adds two PGP unlockers one right after
|
|
// the other, so on the same host and day, and checks that the second gets a
|
|
// directory of its own and leaves the first one's files as they were.
|
|
// CreatePGPUnlocker does not check whether the GPG key already has an
|
|
// unlocker, so the test key serves for both.
|
|
//
|
|
//nolint:paralleltest // installFakeGPG uses t.Setenv
|
|
func TestPGPUnlockerAddedTwiceKeepsFirst(t *testing.T) {
|
|
installFakeGPG(t)
|
|
|
|
original := secret.GPGEncryptFunc
|
|
|
|
t.Cleanup(func() { secret.GPGEncryptFunc = original })
|
|
|
|
// Stands in for gpg, which the test does not have: "encrypts" by copying
|
|
secret.GPGEncryptFunc = func(data *memguard.LockedBuffer, _ string) ([]byte, error) {
|
|
return []byte(data.String()), nil
|
|
}
|
|
|
|
fs := afero.NewMemMapFs()
|
|
mnemonic := testMnemonicBuffer(t)
|
|
vlt, err := vault.CreateVault(fs, testVaultStateDir, testVaultName, mnemonic, nil)
|
|
require.NoError(t, err)
|
|
|
|
first, err := secret.CreatePGPUnlocker(vlt, testGPGKeyID, testGPGFingerprint)
|
|
require.NoError(t, err)
|
|
|
|
firstFiles := dirFiles(t, fs, first.GetDirectory())
|
|
|
|
second, err := secret.CreatePGPUnlocker(vlt, testGPGKeyID, testGPGFingerprint)
|
|
require.NoError(t, err)
|
|
assert.NotEqual(t, first.GetDirectory(), second.GetDirectory())
|
|
assert.Equal(t, firstFiles, dirFiles(t, fs, first.GetDirectory()))
|
|
}
|
|
|
|
// TestGPGDecryptErrorHasGPGReason runs the real gpg on data that is not an
|
|
// OpenPGP message and checks that the error carries the reason gpg writes
|
|
// to stderr.
|
|
func TestGPGDecryptErrorHasGPGReason(t *testing.T) {
|
|
// The empty GNUPGHOME keeps gpg away from the user's own keyring;
|
|
// LC_ALL=C keeps gpg's message in English.
|
|
t.Setenv("GNUPGHOME", t.TempDir())
|
|
t.Setenv("LC_ALL", "C")
|
|
|
|
_, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message"))
|
|
require.ErrorContains(t, err, "gpg: no valid OpenPGP data found")
|
|
}
|
|
|
|
// TestGPGDecryptErrorWithoutGPGStderr checks that when gpg fails without
|
|
// writing to stderr, the error ends with the exit status and nothing after it.
|
|
//
|
|
//nolint:paralleltest // installFakeGPG uses t.Setenv
|
|
func TestGPGDecryptErrorWithoutGPGStderr(t *testing.T) {
|
|
installFakeGPG(t)
|
|
|
|
_, err := secret.GPGDecryptFunc([]byte("not an OpenPGP message"))
|
|
require.EqualError(t, err, "GPG decryption failed: exit status 1")
|
|
}
|