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") }