package cli_test import ( "bytes" "os" "testing" "git.eeqj.de/sneak/secret/internal/cli" "git.eeqj.de/sneak/secret/internal/secret" "git.eeqj.de/sneak/secret/internal/vault" "github.com/awnumar/memguard" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/stretchr/testify/require" ) // TestCreateExistingVaultChangesNothing is a regression test for // https://git.eeqj.de/sneak/secret/issues/74, where running `secret init` // a second time, or `secret vault create` with the name of an existing // vault, replaced that vault's keys, so that none of its secrets could be // decrypted any more. Each must refuse, change nothing, and leave every // vault's secret readable through its passphrase unlocker. // //nolint:paralleltest // the cases share cmd func TestCreateExistingVaultChangesNothing(t *testing.T) { mnemonic := testMnemonicBuffer(t) passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase)) t.Cleanup(passphrase.Destroy) // newCLI returns an instance on fs given the mnemonic and the unlock // passphrase, as from the environment newCLI := func(fs afero.Fs) *cli.Instance { c := cli.NewCLIInstanceWithStateDir(fs, testStateDir) c.Mnemonic = mnemonic c.UnlockPassphrase = passphrase return c } // `secret init`, `secret vault create work`, `secret vault select // default`, and the secret "x" in each vault. "work" is then not the // current vault, which creating it again must not change. fs := afero.NewMemMapFs() c := newCLI(fs) cmd := &cobra.Command{} require.NoError(t, c.Init(cmd)) require.NoError(t, c.CreateVault(cmd, "work")) require.NoError(t, c.SelectVault(cmd, "default")) vaults, err := vault.ListVaults(fs, testStateDir) require.NoError(t, err) require.Len(t, vaults, 2) for _, name := range vaults { value := memguard.NewBufferFromBytes([]byte("value")) err := vault.NewVault(fs, testStateDir, name).AddSecret("x", value, false) require.NoError(t, err) } before := snapshotStateDir(t, fs) tests := []struct { command string want string run func(c *cli.Instance) error }{ { "init", "failed to create default vault: vault default already exists", func(c *cli.Instance) error { return c.Init(cmd) }, }, { "vault create default", "vault default already exists", func(c *cli.Instance) error { return c.CreateVault(cmd, "default") }, }, { "vault create work", "vault work already exists", func(c *cli.Instance) error { return c.CreateVault(cmd, "work") }, }, } for _, tt := range tests { t.Run(tt.command, func(t *testing.T) { fs := newFsFromSnapshot(t, before) err := tt.run(newCLI(fs)) require.EqualError(t, err, tt.want) require.Equal(t, before, snapshotStateDir(t, fs)) }) } // Every case left the state directory exactly as recorded in before, so // reading each vault's secret once from it shows that it still decrypts // after each case. Without the mnemonic, reading a secret goes through // the vault's passphrase unlocker, which is slow. for _, name := range vaults { vlt := vault.NewVault(fs, testStateDir, name) vlt.UnlockPassphrase = passphrase value, err := vlt.GetSecret("x") require.NoError(t, err) unchanged := bytes.Equal([]byte("value"), value.Bytes()) value.Destroy() require.True(t, unchanged, "vault %q kept its secret", name) } } // TestVaultCreationLeavesNoSecretInEnvironment is a regression test for // https://git.eeqj.de/sneak/secret/issues/60, where `secret init` and // `secret vault create` put the mnemonic into the process environment, // which every program they ran inherited, and SB_SECRET_MNEMONIC and // SB_UNLOCK_PASSPHRASE were never unset. Each command, given both, must // leave neither in the environment. func TestVaultCreationLeavesNoSecretInEnvironment(t *testing.T) { t.Setenv(secret.EnvStateDir, t.TempDir()) run := func(args ...string) { t.Setenv(secret.EnvMnemonic, testMnemonic) t.Setenv(secret.EnvUnlockPassphrase, testPassphrase) // With no terminal to prompt on, this succeeds only if the command // read both variables _, err := cli.ExecuteCommandInProcess(args, "", nil) require.NoError(t, err) for _, name := range []string{secret.EnvMnemonic, secret.EnvUnlockPassphrase} { _, set := os.LookupEnv(name) require.False(t, set, "%s is set after %v", name, args) } } run("init") run("vault", "create", "work") } // TestStopAtPassphrasePromptLeavesNothing is a regression test for the // review of https://git.eeqj.de/sneak/secret/pulls/82: `secret init` or // `secret vault create` stopped at the passphrase prompt left a vault with // no unlocker, which neither command would then create again. Each must ask // for the passphrase before writing anything. // //nolint:paralleltest // the cases share cmd func TestStopAtPassphrasePromptLeavesNothing(t *testing.T) { mnemonic := testMnemonicBuffer(t) // An empty state directory for `secret init`, and one holding the vault // "default" for `secret vault create work`. empty := afero.NewMemMapFs() require.NoError(t, empty.MkdirAll(testStateDir, secret.DirPerms)) withDefault := afero.NewMemMapFs() _, err := vault.CreateVault(withDefault, testStateDir, "default", mnemonic) require.NoError(t, err) cmd := &cobra.Command{} tests := []struct { command string fs afero.Fs run func(c *cli.Instance) error }{ { "init", empty, func(c *cli.Instance) error { return c.Init(cmd) }, }, { "vault create work", withDefault, func(c *cli.Instance) error { return c.CreateVault(cmd, "work") }, }, } for _, tt := range tests { t.Run(tt.command, func(t *testing.T) { before := snapshotStateDir(t, tt.fs) // Given no unlock passphrase, both commands prompt for it, which // fails because the tests do not run in a terminal. c := cli.NewCLIInstanceWithStateDir(tt.fs, testStateDir) c.Mnemonic = mnemonic err := tt.run(c) require.ErrorContains(t, err, "failed to read passphrase") require.Equal(t, before, snapshotStateDir(t, tt.fs)) }) } }