check / check (push) Waiting to run
getDefaultGPGKey took the default-key value from `gpgconf --list-options gpg` as printed. gpgconf prints a string value with a leading `"` and percent-escaped, so gpg rejected the key ID. The parsing moved into parseDefaultGPGKey, which drops the `"` and percent-decodes the rest with url.PathUnescape, which leaves `+` alone. A value that does not decode is an error, not a fallback to the first secret key. Rule suppressed: gosec G703 on the test's write of gpg.conf into the GNUPGHOME directory newTestGPGKey made. Model: opus-5-5
218 lines
7.0 KiB
Go
218 lines
7.0 KiB
Go
//nolint:testpackage // white-box test of unexported internals
|
|
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
// unknownTestGPGUserID is a GPG user ID that no key in the test keyring has.
|
|
const unknownTestGPGUserID = "not-in-keyring@example.com"
|
|
|
|
// The secret TestAddPGPUnlocker stores, then reads through the new unlocker.
|
|
const (
|
|
addTestSecretName = "api-key"
|
|
addTestSecretValue = "value"
|
|
)
|
|
|
|
// TestAddPGPUnlocker adds a PGP unlocker for a throwaway GPG key to a vault
|
|
// with a passphrase unlocker, getting the vault's long-term key from the
|
|
// mnemonic or, with no mnemonic given, from the passphrase unlocker. It
|
|
// then reads a secret with neither the mnemonic nor the passphrase given, so
|
|
// through the new unlocker, which the add selects.
|
|
//
|
|
//nolint:paralleltest // t.Setenv (GNUPGHOME) forbids parallel tests
|
|
func TestAddPGPUnlocker(t *testing.T) {
|
|
newTestGPGKey(t)
|
|
|
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
|
t.Cleanup(passphrase.Destroy)
|
|
|
|
tests := []struct {
|
|
name string
|
|
// mnemonic is the mnemonic given while the unlocker is added, or nil.
|
|
mnemonic *memguard.LockedBuffer
|
|
}{
|
|
{"long-term key from the mnemonic", testMnemonicBuffer(t)},
|
|
{"long-term key from the current unlocker", nil},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
fs := afero.NewMemMapFs()
|
|
vlt, err := vault.CreateVault(fs, listTestStateDir, listTestVaultName,
|
|
testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
err = vlt.AddSecret(addTestSecretName,
|
|
memguard.NewBufferFromBytes([]byte(addTestSecretValue)), false)
|
|
require.NoError(t, err)
|
|
|
|
_, err = vlt.CreatePassphraseUnlocker(
|
|
memguard.NewBufferFromBytes([]byte(testPassphrase)))
|
|
require.NoError(t, err)
|
|
|
|
instance, cmd := newTestInstance(fs)
|
|
instance.Mnemonic = test.mnemonic
|
|
instance.UnlockPassphrase = passphrase
|
|
|
|
cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
|
require.NoError(t, instance.UnlockersAdd(unlockerTypePGP, cmd))
|
|
|
|
reopened := vault.NewVault(fs, listTestStateDir, listTestVaultName)
|
|
|
|
current, err := reopened.GetCurrentUnlocker()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unlockerTypePGP, current.GetType())
|
|
|
|
value, err := reopened.GetSecret(addTestSecretName)
|
|
require.NoError(t, err)
|
|
|
|
defer value.Destroy()
|
|
|
|
assert.Equal(t, addTestSecretValue, value.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAddPGPUnlockerDefaultKey is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/141: with default-key set in
|
|
// gpg.conf and no --keyid, `secret unlocker add pgp` uses that key.
|
|
// gpg.conf names the key by its user ID, and the printed key ID must be that
|
|
// user ID. The first secret key, which is used when gpg.conf sets no
|
|
// default-key, prints as a hex key ID.
|
|
func TestAddPGPUnlockerDefaultKey(t *testing.T) {
|
|
newTestGPGKey(t)
|
|
// The key comes from neither --keyid nor the environment.
|
|
t.Setenv(secret.EnvGPGKeyID, "")
|
|
|
|
gpgConf := filepath.Join(os.Getenv("GNUPGHOME"), "gpg.conf")
|
|
//nolint:gosec // G703: GNUPGHOME is the directory newTestGPGKey made
|
|
require.NoError(t, os.WriteFile(gpgConf,
|
|
[]byte("default-key "+unreadableTestGPGUserID+"\n"), 0o600))
|
|
|
|
fs := afero.NewMemMapFs()
|
|
_, err := vault.CreateVault(fs, listTestStateDir, listTestVaultName,
|
|
testMnemonicBuffer(t), nil)
|
|
require.NoError(t, err)
|
|
|
|
instance, cmd := newTestInstance(fs)
|
|
instance.Mnemonic = testMnemonicBuffer(t)
|
|
|
|
var output bytes.Buffer
|
|
cmd.SetOut(&output)
|
|
|
|
require.NoError(t, instance.UnlockersAdd(unlockerTypePGP, cmd))
|
|
assert.Contains(t, output.String(),
|
|
"Using default GPG key: "+unreadableTestGPGUserID+"\n")
|
|
}
|
|
|
|
// TestParseDefaultGPGKey is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/141: gpgconf prints the
|
|
// default-key value with a leading `"` and percent-escaped, and the key ID
|
|
// is that value decoded.
|
|
func TestParseDefaultGPGKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// gpgconfLine is the default-key line of `gpgconf --list-options gpg`
|
|
// up to the value, its last field.
|
|
const gpgconfLine = "default-key:0:0:use NAME as default secret key:" +
|
|
"1:1:NAME:::"
|
|
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
want string
|
|
}{
|
|
{
|
|
"fingerprint", `"56163D7F3FAFF709608855BEC1A360216D007F41`,
|
|
"56163D7F3FAFF709608855BEC1A360216D007F41",
|
|
},
|
|
{
|
|
"user ID with a comma", `"Doe%2c Alice <alice@example.com>`,
|
|
"Doe, Alice <alice@example.com>",
|
|
},
|
|
{"no default-key in gpg.conf", "", ""},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keyID, err := parseDefaultGPGKey(gpgconfLine + test.value + "\n")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, test.want, keyID)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAddPGPUnlockerUnknownKey asserts that adding a PGP unlocker for a key
|
|
// the keyring does not hold fails at looking up the key's fingerprint and
|
|
// leaves no new unlocker directory. The error must come from the lookup: a
|
|
// lookup moved after anything is written would also come after getting the
|
|
// vault's long-term key, which fails first here: this vault's unlockers hold
|
|
// no keys.
|
|
//
|
|
//nolint:paralleltest // t.Setenv (GNUPGHOME) forbids parallel tests
|
|
func TestAddPGPUnlockerUnknownKey(t *testing.T) {
|
|
newTestGPGKey(t)
|
|
|
|
base := newListTestVault(t, 1)
|
|
instance, cmd := newTestInstance(base)
|
|
cmd.Flags().String("keyid", unknownTestGPGUserID, "")
|
|
|
|
err := instance.addPGPUnlocker(cmd)
|
|
|
|
require.ErrorIs(t, err, secret.ErrGPGKeyNotFound)
|
|
assertDirEntries(t, base,
|
|
filepath.Join(testVaultDir(listTestVaultName), listTestUnlockersDirName),
|
|
listTestUnlockerDirOne)
|
|
}
|
|
|
|
// TestUnlockerAddKnowsOnlyPassphraseAndPGP is a regression test for
|
|
// https://git.eeqj.de/sneak/secret/issues/132: `secret unlocker add` takes
|
|
// only passphrase and pgp, on every platform. keychain and secure-enclave
|
|
// fail as unknown types, and neither --help nor shell completion offers
|
|
// them.
|
|
//
|
|
//nolint:paralleltest // executes the CLI in-process and sets the environment
|
|
func TestUnlockerAddKnowsOnlyPassphraseAndPGP(t *testing.T) {
|
|
env := map[string]string{secret.EnvStateDir: t.TempDir()}
|
|
removedTypes := []string{"keychain", "secure-enclave"}
|
|
|
|
for _, unlockerType := range removedTypes {
|
|
_, err := ExecuteCommandInProcess(
|
|
[]string{"unlocker", "add", unlockerType}, "", env)
|
|
require.ErrorIs(t, err, errInvalidUnlockerType)
|
|
|
|
instance, cmd := newTestInstance(newListTestVault(t, 1))
|
|
require.ErrorIs(t, instance.UnlockersAdd(unlockerType, cmd),
|
|
errInvalidUnlockerType)
|
|
}
|
|
|
|
// --help prints the command's Long text, and shell completion offers
|
|
// its ValidArgs.
|
|
addCmd := newUnlockerAddCmd()
|
|
help := addCmd.Long
|
|
completions := strings.Join(addCmd.ValidArgs, " ")
|
|
|
|
for _, offered := range []string{help, completions} {
|
|
assert.Contains(t, offered, unlockerTypePassphrase)
|
|
assert.Contains(t, offered, unlockerTypePGP)
|
|
|
|
for _, unlockerType := range removedTypes {
|
|
assert.NotContains(t, offered, unlockerType)
|
|
}
|
|
}
|
|
}
|