check / check (push) Failing after 3s
Vault.GetSecret and Vault.GetSecretVersion return the decrypted value as a *memguard.LockedBuffer instead of copying it into an ordinary []byte that nothing wiped. Every caller destroys the buffer, and `secret get` writes its bytes straight to stdout, still with no trailing newline. Instance.Print, which formatted through fmt and had no other callers, is removed, and so is a debug log line in `get --version` that held the plaintext value. Model: opus-5-5
679 lines
19 KiB
Go
679 lines
19 KiB
Go
package secret_test
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
"git.eeqj.de/sneak/secret/internal/secret"
|
|
"git.eeqj.de/sneak/secret/internal/vault"
|
|
"github.com/awnumar/memguard"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var errInjected = errors.New("injected failure")
|
|
|
|
// The kinds of change hookFs passes to before.
|
|
const (
|
|
opCreate = "create"
|
|
opOpen = "open"
|
|
opSync = "sync"
|
|
opMkdir = "mkdir"
|
|
opRemove = "remove"
|
|
opRename = "rename"
|
|
)
|
|
|
|
// currentFile is the file in a secret's directory that names its current
|
|
// version.
|
|
const currentFile = "current"
|
|
|
|
// unlockerMetadataFile is the file a new unlocker writes last.
|
|
const unlockerMetadataFile = "unlocker-metadata.json"
|
|
|
|
// unlockerPassphrase protects the passphrase unlockers the tests create.
|
|
//
|
|
//nolint:gosec // G101: test data, not a real credential
|
|
const unlockerPassphrase = "unlocker passphrase"
|
|
|
|
// hookFs passes every call through to Fs, but first calls before for each
|
|
// call that changes the filesystem, and for each Sync of a file opened
|
|
// through it, with the path it changes (the new path, for Rename). A test
|
|
// uses before to inspect the tree at every point where a crash could stop
|
|
// the code under test, or returns an error from it to make that call fail.
|
|
// If opened is set, OpenFile also tells it the mode it opens each file with.
|
|
type hookFs struct {
|
|
afero.Fs
|
|
|
|
before func(op, path string) error
|
|
opened func(path string, perm os.FileMode)
|
|
}
|
|
|
|
// hookFile is a file opened through hookFs.
|
|
type hookFile struct {
|
|
afero.File
|
|
|
|
before func(op, path string) error
|
|
}
|
|
|
|
func (f hookFile) Sync() error {
|
|
err := f.before(opSync, f.Name())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return f.File.Sync()
|
|
}
|
|
|
|
//nolint:ireturn // implements afero.Fs
|
|
func (h hookFs) Create(name string) (afero.File, error) {
|
|
err := h.before(opCreate, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
file, err := h.Fs.Create(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return hookFile{File: file, before: h.before}, nil
|
|
}
|
|
|
|
//nolint:ireturn // implements afero.Fs
|
|
func (h hookFs) OpenFile(
|
|
name string, flag int, perm os.FileMode,
|
|
) (afero.File, error) {
|
|
err := h.before(opOpen, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if h.opened != nil {
|
|
h.opened(name, perm)
|
|
}
|
|
|
|
file, err := h.Fs.OpenFile(name, flag, perm)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return hookFile{File: file, before: h.before}, nil
|
|
}
|
|
|
|
func (h hookFs) Mkdir(name string, perm os.FileMode) error {
|
|
err := h.before(opMkdir, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Fs.Mkdir(name, perm)
|
|
}
|
|
|
|
func (h hookFs) MkdirAll(path string, perm os.FileMode) error {
|
|
err := h.before(opMkdir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Fs.MkdirAll(path, perm)
|
|
}
|
|
|
|
func (h hookFs) Remove(name string) error {
|
|
err := h.before(opRemove, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Fs.Remove(name)
|
|
}
|
|
|
|
func (h hookFs) RemoveAll(path string) error {
|
|
err := h.before(opRemove, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Fs.RemoveAll(path)
|
|
}
|
|
|
|
func (h hookFs) Rename(oldname, newname string) error {
|
|
err := h.before(opRename, newname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Fs.Rename(oldname, newname)
|
|
}
|
|
|
|
// testFilesystem is a filesystem to run a test on, with a directory in it
|
|
// to work in.
|
|
type testFilesystem struct {
|
|
name string
|
|
open func(t *testing.T) (afero.Fs, string)
|
|
}
|
|
|
|
// testFilesystems are the in-memory filesystem that most tests use and the
|
|
// real one: every rename-based guarantee is checked on both.
|
|
//
|
|
//nolint:gochecknoglobals // read-only table shared by the tests below
|
|
var testFilesystems = []testFilesystem{
|
|
{"memory", func(*testing.T) (afero.Fs, string) {
|
|
return afero.NewMemMapFs(), "/test"
|
|
}},
|
|
{"real", func(t *testing.T) (afero.Fs, string) {
|
|
t.Helper()
|
|
|
|
return afero.NewOsFs(), t.TempDir()
|
|
}},
|
|
}
|
|
|
|
// dirNames lists the names in dir.
|
|
func dirNames(t *testing.T, fs afero.Fs, dir string) []string {
|
|
t.Helper()
|
|
|
|
entries, err := afero.ReadDir(fs, dir)
|
|
require.NoError(t, err)
|
|
|
|
names := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
names = append(names, entry.Name())
|
|
}
|
|
|
|
return names
|
|
}
|
|
|
|
// writeLongTermKey gives the test vault under stateDir a new long-term key
|
|
// and returns it.
|
|
func writeLongTermKey(
|
|
t *testing.T, fs afero.Fs, stateDir string,
|
|
) *age.X25519Identity {
|
|
t.Helper()
|
|
|
|
vault := &MockVersionVault{Name: testVaultName, fs: fs, stateDir: stateDir}
|
|
|
|
vaultDir, err := vault.GetDirectory()
|
|
require.NoError(t, err)
|
|
require.NoError(t, fs.MkdirAll(vaultDir, 0o700))
|
|
|
|
ltIdentity, err := age.GenerateX25519Identity()
|
|
require.NoError(t, err)
|
|
require.NoError(t, afero.WriteFile(fs, filepath.Join(vaultDir, "pub.age"),
|
|
[]byte(ltIdentity.Recipient().String()), 0o600))
|
|
|
|
return ltIdentity
|
|
}
|
|
|
|
// newVaultWithSecret creates the vault name under stateDir from the test
|
|
// mnemonic, with a secret "shared" in it that holds value.
|
|
func newVaultWithSecret(
|
|
t *testing.T, fs afero.Fs, stateDir, name, value string,
|
|
) *vault.Vault {
|
|
t.Helper()
|
|
|
|
vlt, err := vault.CreateVault(fs, stateDir, name)
|
|
require.NoError(t, err)
|
|
|
|
buffer := memguard.NewBufferFromBytes([]byte(value))
|
|
defer buffer.Destroy()
|
|
|
|
require.NoError(t, vlt.AddSecret("shared", buffer, false))
|
|
|
|
return vlt
|
|
}
|
|
|
|
func TestWriteFileAtomicReplacesFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fs, dir := tfs.open(t)
|
|
path := filepath.Join(dir, currentFile)
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs, path, []byte("old")))
|
|
require.NoError(t, secret.WriteFileAtomic(fs, path, []byte("new")))
|
|
|
|
data, err := afero.ReadFile(fs, path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "new", string(data))
|
|
|
|
info, err := fs.Stat(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, secret.FilePerms, info.Mode().Perm())
|
|
|
|
// No temporary file is left next to it
|
|
assert.Equal(t, []string{currentFile}, dirNames(t, fs, dir))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteFileAtomicFailureKeepsOldFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
base, dir := tfs.open(t)
|
|
path := filepath.Join(dir, currentFile)
|
|
require.NoError(t, secret.WriteFileAtomic(base, path, []byte("old")))
|
|
|
|
fs := hookFs{Fs: base, before: func(op, _ string) error {
|
|
if op == opRename {
|
|
return errInjected
|
|
}
|
|
|
|
return nil
|
|
}}
|
|
|
|
err := secret.WriteFileAtomic(fs, path, []byte("new"))
|
|
require.ErrorIs(t, err, errInjected)
|
|
|
|
data, err := afero.ReadFile(base, path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "old", string(data))
|
|
|
|
// The temporary file is removed again
|
|
assert.Equal(t, []string{currentFile}, dirNames(t, base, dir))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestRemoveDirAtomic checks that RemoveDirAtomic deletes nothing where the
|
|
// directory stands, which a crash could stop half-way, and that it leaves
|
|
// nothing behind.
|
|
func TestRemoveDirAtomic(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
base, dir := tfs.open(t)
|
|
listed := filepath.Join(dir, "secrets.d")
|
|
target := filepath.Join(listed, "doomed")
|
|
|
|
require.NoError(t, base.MkdirAll(filepath.Join(target, "versions"), 0o700))
|
|
require.NoError(t, secret.WriteFileAtomic(base,
|
|
filepath.Join(target, currentFile), []byte("20231216.001")))
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
if op == opRemove && strings.HasPrefix(path, target) {
|
|
t.Errorf("deleted %s where it stands", path)
|
|
}
|
|
|
|
return nil
|
|
}}
|
|
|
|
require.NoError(t, secret.RemoveDirAtomic(fs, target))
|
|
|
|
// Gone, and no temporary directory is left in the directory
|
|
// that is listed or in the one above it
|
|
assert.Empty(t, dirNames(t, base, listed))
|
|
assert.Equal(t, []string{"secrets.d"}, dirNames(t, base, dir))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLongestNames adds a secret to a vault and removes the vault, both
|
|
// named with 255 bytes, the most a file name may have, on the real
|
|
// filesystem: the temporary directories they use must fit that limit too.
|
|
func TestLongestNames(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
const longestName = 255
|
|
|
|
fs := afero.NewOsFs()
|
|
name := strings.Repeat("a", longestName)
|
|
|
|
vlt, err := vault.CreateVault(fs, t.TempDir(), name)
|
|
require.NoError(t, err)
|
|
|
|
value := memguard.NewBufferFromBytes([]byte("long"))
|
|
defer value.Destroy()
|
|
|
|
require.NoError(t, vlt.AddSecret(name, value, false))
|
|
|
|
got, err := vlt.GetSecret(name)
|
|
require.NoError(t, err)
|
|
|
|
defer got.Destroy()
|
|
|
|
assert.Equal(t, []byte("long"), got.Bytes())
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
require.NoError(t, err)
|
|
require.NoError(t, secret.RemoveDirAtomic(fs, vaultDir))
|
|
assert.NoDirExists(t, vaultDir)
|
|
}
|
|
|
|
// TestForcedCopyKeepsDestinationUntilReplaced copies a secret over one in
|
|
// another vault, as a forced move between vaults does, and makes the last
|
|
// step that completes the copy fail. The secret it was to replace must
|
|
// still be there unchanged: it may go only once its replacement is whole.
|
|
//
|
|
//nolint:paralleltest // t.Setenv forbids t.Parallel
|
|
func TestForcedCopyKeepsDestinationUntilReplaced(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
base, stateDir := tfs.open(t)
|
|
src := newVaultWithSecret(t, base, stateDir, "source", "new")
|
|
dest := newVaultWithSecret(t, base, stateDir, "dest", "old")
|
|
|
|
// The copy is complete once its current file is written
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
if op == opRename && filepath.Base(path) == currentFile {
|
|
return errInjected
|
|
}
|
|
|
|
return nil
|
|
}}
|
|
|
|
err := vault.NewVault(fs, stateDir, "dest").
|
|
CopySecretAllVersions(src, "shared", "shared", true)
|
|
require.ErrorIs(t, err, errInjected)
|
|
|
|
value, err := dest.GetSecret("shared")
|
|
require.NoError(t, err)
|
|
|
|
defer value.Destroy()
|
|
|
|
assert.Equal(t, []byte("old"), value.Bytes())
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestTempDirsStayOutOfListings adds a version, adds a secret, copies a
|
|
// secret over another and removes one, and checks that none of them makes a
|
|
// directory directly in secrets.d or in a versions directory. Those are
|
|
// listed to find secrets and versions, so a temporary directory made there
|
|
// would be listed while half-built, and one left by a crash would stay.
|
|
//
|
|
//nolint:paralleltest // t.Setenv forbids t.Parallel
|
|
func TestTempDirsStayOutOfListings(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
base, stateDir := tfs.open(t)
|
|
newVaultWithSecret(t, base, stateDir, "default", "first")
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
parent := filepath.Base(filepath.Dir(path))
|
|
if op == opMkdir && (parent == "secrets.d" || parent == "versions") {
|
|
t.Errorf("made %s where it is listed", path)
|
|
}
|
|
|
|
return nil
|
|
}}
|
|
vlt := vault.NewVault(fs, stateDir, "default")
|
|
|
|
value := memguard.NewBufferFromBytes([]byte("second"))
|
|
defer value.Destroy()
|
|
|
|
require.NoError(t, vlt.AddSecret("shared", value, true))
|
|
require.NoError(t, vlt.AddSecret("other", value, false))
|
|
require.NoError(t, vlt.CopySecretAllVersions(vlt, "shared", "other", true))
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
require.NoError(t, err)
|
|
require.NoError(t, secret.RemoveDirAtomic(fs,
|
|
filepath.Join(vaultDir, "secrets.d", "shared")))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVersionSaveIsWholeOrAbsent checks, before every change Save makes and
|
|
// once after it returns, that the version directory either does not exist
|
|
// or holds all of its files: a crash at any point leaves no version that
|
|
// cannot be decrypted.
|
|
func TestVersionSaveIsWholeOrAbsent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
base, stateDir := tfs.open(t)
|
|
ltIdentity := writeLongTermKey(t, base, stateDir)
|
|
|
|
var versionDir string
|
|
|
|
checkVersionDir := func(string, string) error {
|
|
exists, err := afero.DirExists(base, versionDir)
|
|
require.NoError(t, err)
|
|
|
|
if exists {
|
|
assert.ElementsMatch(t,
|
|
[]string{"pub.age", "value.age", "priv.age", "metadata.age"},
|
|
dirNames(t, base, versionDir),
|
|
"version directory visible before it was complete")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
fs := hookFs{Fs: base, before: checkVersionDir}
|
|
vault := &MockVersionVault{Name: testVaultName, fs: fs, stateDir: stateDir}
|
|
sv := secret.NewVersion(vault, "test/secret", "20231215.001")
|
|
versionDir = sv.Directory
|
|
|
|
value := memguard.NewBufferFromBytes([]byte("whole or nothing"))
|
|
defer value.Destroy()
|
|
|
|
require.NoError(t, sv.Save(value))
|
|
require.NoError(t, checkVersionDir("", ""))
|
|
|
|
got, err := sv.GetValue(ltIdentity)
|
|
require.NoError(t, err)
|
|
|
|
defer got.Destroy()
|
|
|
|
assert.Equal(t, "whole or nothing", got.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestVersionSaveFailureLeavesNothing makes the write of the encrypted
|
|
// private key fail, after the value has been written, and checks that
|
|
// neither the version nor its temporary directory is left behind.
|
|
func TestVersionSaveFailureLeavesNothing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
base, stateDir := tfs.open(t)
|
|
writeLongTermKey(t, base, stateDir)
|
|
|
|
fs := hookFs{Fs: base, before: func(op, path string) error {
|
|
if op == opRename && filepath.Base(path) == "priv.age" {
|
|
return errInjected
|
|
}
|
|
|
|
return nil
|
|
}}
|
|
vault := &MockVersionVault{Name: testVaultName, fs: fs, stateDir: stateDir}
|
|
sv := secret.NewVersion(vault, "test/secret", "20231215.001")
|
|
|
|
value := memguard.NewBufferFromBytes([]byte("never stored"))
|
|
defer value.Destroy()
|
|
|
|
require.ErrorIs(t, sv.Save(value), errInjected)
|
|
|
|
// The secret directory holds only the empty versions directory
|
|
versionsDir := filepath.Dir(sv.Directory)
|
|
assert.Equal(t, []string{"versions"},
|
|
dirNames(t, base, filepath.Dir(versionsDir)))
|
|
assert.Empty(t, dirNames(t, base, versionsDir))
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCurrentFilesNeverMissing selects the current version, vault and
|
|
// unlocker again and checks, before each change this makes, that the file
|
|
// naming the current one exists: a reader or a crash never finds it
|
|
// missing.
|
|
//
|
|
//nolint:paralleltest // t.Setenv forbids t.Parallel
|
|
func TestCurrentFilesNeverMissing(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
base, stateDir := tfs.open(t)
|
|
vlt := newVaultWithSecret(t, base, stateDir, testVaultName, "value")
|
|
|
|
passphrase := memguard.NewBufferFromBytes([]byte(unlockerPassphrase))
|
|
defer passphrase.Destroy()
|
|
|
|
// Created as the current unlocker
|
|
unlocker, err := vlt.CreatePassphraseUnlocker(passphrase)
|
|
require.NoError(t, err)
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
require.NoError(t, err)
|
|
|
|
secretDir := filepath.Join(vaultDir, "secrets.d", "shared")
|
|
version, err := secret.GetCurrentVersion(base, secretDir)
|
|
require.NoError(t, err)
|
|
|
|
for _, tc := range []struct {
|
|
path string
|
|
reselect func(fs afero.Fs) error
|
|
}{
|
|
{filepath.Join(secretDir, currentFile), func(fs afero.Fs) error {
|
|
return secret.SetCurrentVersion(fs, secretDir, version)
|
|
}},
|
|
{filepath.Join(stateDir, "currentvault"), func(fs afero.Fs) error {
|
|
return vault.SelectVault(fs, stateDir, testVaultName)
|
|
}},
|
|
{filepath.Join(vaultDir, "current-unlocker"), func(fs afero.Fs) error {
|
|
return vault.NewVault(fs, stateDir, testVaultName).
|
|
SelectUnlocker(unlocker.GetID())
|
|
}},
|
|
} {
|
|
fs := hookFs{Fs: base, before: func(string, string) error {
|
|
exists, err := afero.Exists(base, tc.path)
|
|
require.NoError(t, err)
|
|
assert.True(t, exists, "%s is missing", filepath.Base(tc.path))
|
|
|
|
return nil
|
|
}}
|
|
|
|
require.NoError(t, tc.reselect(fs))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWriteFileAtomicTempFile checks that WriteFileAtomic creates its
|
|
// temporary file with mode 0600, rather than wider and narrowed later, so
|
|
// that no other user can ever read it, and syncs it before renaming it into
|
|
// place, so that a crash cannot leave the file named but its data lost.
|
|
func TestWriteFileAtomicTempFile(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, tfs := range testFilesystems {
|
|
t.Run(tfs.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
base, dir := tfs.open(t)
|
|
|
|
var modes []os.FileMode
|
|
|
|
synced := false
|
|
fs := hookFs{
|
|
Fs: base,
|
|
before: func(op, _ string) error {
|
|
switch op {
|
|
case opSync:
|
|
synced = true
|
|
case opRename:
|
|
assert.True(t, synced, "renamed before syncing")
|
|
}
|
|
|
|
return nil
|
|
},
|
|
opened: func(_ string, perm os.FileMode) {
|
|
modes = append(modes, perm)
|
|
},
|
|
}
|
|
|
|
require.NoError(t, secret.WriteFileAtomic(fs,
|
|
filepath.Join(dir, currentFile), []byte("new")))
|
|
assert.Equal(t, []os.FileMode{secret.FilePerms}, modes)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPassphraseUnlockerGetsKeyFirst creates a passphrase unlocker in a
|
|
// vault whose long-term key cannot be had: it must fail without writing
|
|
// anything, so that it never leaves a partial unlocker, nor breaks the one
|
|
// it would replace.
|
|
func TestPassphraseUnlockerGetsKeyFirst(t *testing.T) {
|
|
// No mnemonic, and no current unlocker to get the key from
|
|
t.Setenv(secret.EnvMnemonic, "")
|
|
|
|
base := afero.NewMemMapFs()
|
|
_, err := vault.CreateVault(base, testVaultStateDir, testVaultName)
|
|
require.NoError(t, err)
|
|
|
|
fs := hookFs{Fs: base, before: func(_, path string) error {
|
|
t.Errorf("changed %s before getting the long-term key", path)
|
|
|
|
return nil
|
|
}}
|
|
|
|
passphrase := memguard.NewBufferFromBytes([]byte(unlockerPassphrase))
|
|
defer passphrase.Destroy()
|
|
|
|
_, err = vault.NewVault(fs, testVaultStateDir, testVaultName).
|
|
CreatePassphraseUnlocker(passphrase)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// TestPassphraseUnlockerWritesMetadataLast checks that the last file a new
|
|
// passphrase unlocker writes in its directory is its metadata: an unlocker
|
|
// directory without metadata is never used, so one interrupted earlier
|
|
// cannot be.
|
|
func TestPassphraseUnlockerWritesMetadataLast(t *testing.T) {
|
|
t.Setenv(secret.EnvMnemonic, testMnemonic)
|
|
|
|
base := afero.NewMemMapFs()
|
|
vlt, err := vault.CreateVault(base, testVaultStateDir, testVaultName)
|
|
require.NoError(t, err)
|
|
|
|
vaultDir, err := vlt.GetDirectory()
|
|
require.NoError(t, err)
|
|
|
|
unlockerDir := filepath.Join(vaultDir, "unlockers.d", "passphrase")
|
|
|
|
var last string
|
|
|
|
fs := hookFs{Fs: base, before: func(_, path string) error {
|
|
if filepath.Dir(path) == unlockerDir {
|
|
last = filepath.Base(path)
|
|
}
|
|
|
|
return nil
|
|
}}
|
|
|
|
passphrase := memguard.NewBufferFromBytes([]byte(unlockerPassphrase))
|
|
defer passphrase.Destroy()
|
|
|
|
_, err = vault.NewVault(fs, testVaultStateDir, testVaultName).
|
|
CreatePassphraseUnlocker(passphrase)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, unlockerMetadataFile, last)
|
|
}
|