check / check (push) Waiting to run
When gpg failed while decrypting, encrypting or looking up a key fingerprint, the error gave only gpg's exit status, so the user could not tell why. cmd.Output keeps gpg's stderr in the *exec.ExitError it returns; the three errors now end with that stderr, trimmed. When gpg wrote nothing to stderr, the error is left as it was. gpg's stderr can run to more than one line, and the error then does too. The regression test gives gpg data that is not an OpenPGP message instead of one encrypted to a missing key: gpg reports both the same way, and this case starts no gpg-agent the test would have to stop. Model: opus-5-5
129 lines
4.2 KiB
Go
129 lines
4.2 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(
|
|
fs, testVaultStateDir, testGPGKeyID, testGPGFingerprint, nil, nil)
|
|
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)
|
|
_, err := vault.CreateVault(fs, testVaultStateDir, testVaultName, mnemonic, nil)
|
|
require.NoError(t, err)
|
|
|
|
first, err := secret.CreatePGPUnlocker(
|
|
fs, testVaultStateDir, testGPGKeyID, testGPGFingerprint, mnemonic, nil)
|
|
require.NoError(t, err)
|
|
|
|
firstFiles := dirFiles(t, fs, first.GetDirectory())
|
|
|
|
second, err := secret.CreatePGPUnlocker(
|
|
fs, testVaultStateDir, testGPGKeyID, testGPGFingerprint, mnemonic, nil)
|
|
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")
|
|
}
|