Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9c552c3cc |
No files matched your search
@@ -241,14 +241,12 @@ that the vault then opens only with its mnemonic (see
|
|||||||
[Confirmation Before Removal](#confirmation-before-removal)). An unlocker
|
[Confirmation Before Removal](#confirmation-before-removal)). An unlocker
|
||||||
directory that `secret unlocker list` skips with a warning, because its metadata
|
directory that `secret unlocker list` skips with a warning, because its metadata
|
||||||
cannot be read or parsed, is removed by the directory name the warning gives.
|
cannot be read or parsed, is removed by the directory name the warning gives.
|
||||||
Removing the current unlocker makes the first of the vault's other unlockers
|
Removing the current unlocker makes the first of the vault's other unlockers, in
|
||||||
that `secret unlocker select` accepts, in the order `secret unlocker list` shows
|
the order `secret unlocker list` shows them, the current one, and says which;
|
||||||
them, the current one, and says which; when there is none, the vault is left
|
removing the last unlocker leaves the vault without a current unlocker. When
|
||||||
without a current unlocker. `secret unlocker select` refuses an unlocker whose
|
`current-unlocker` names an unlocker that does not exist and the vault has
|
||||||
type is not `passphrase` or `pgp`. When `current-unlocker` names an unlocker
|
others, opening the vault fails with an error that says to select one of them
|
||||||
that does not exist and the vault has another that `secret unlocker select`
|
with `secret unlocker select`.
|
||||||
accepts, opening the vault fails with an error that says to select one with
|
|
||||||
`secret unlocker select`.
|
|
||||||
|
|
||||||
- `--force, -f`: Remove without asking, even the last unlocker
|
- `--force, -f`: Remove without asking, even the last unlocker
|
||||||
- **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase,
|
- **CRITICAL WARNING**: Without unlockers and without your mnemonic phrase,
|
||||||
|
|||||||
@@ -23,22 +23,13 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
(https://git.eeqj.de/sneak/secret/issues/137). `current-unlocker` went on
|
(https://git.eeqj.de/sneak/secret/issues/137). `current-unlocker` went on
|
||||||
naming the removed unlocker, so every command that opened the vault failed,
|
naming the removed unlocker, so every command that opened the vault failed,
|
||||||
with advice to use the mnemonic. `secret unlocker remove` of the current
|
with advice to use the mnemonic. `secret unlocker remove` of the current
|
||||||
unlocker now makes the first of the vault's other unlockers that
|
unlocker now makes the first of the vault's other unlockers, in the order
|
||||||
`secret unlocker select` accepts, in the order `secret unlocker list` shows,
|
`secret unlocker list` shows, current before removing it, and says so;
|
||||||
current before removing it, and says so; when there is none, it also removes
|
removing the last unlocker also removes `current-unlocker`.
|
||||||
`current-unlocker`. `vault.RemoveUnlocker` returns the ID of the unlocker it
|
`vault.RemoveUnlocker` returns the ID of the unlocker it made current. When
|
||||||
made current. When `current-unlocker` names an unlocker that does not exist
|
`current-unlocker` names an unlocker that does not exist and the vault has
|
||||||
and the vault has another that `secret unlocker select` accepts, the error
|
others, the error wraps `vault.ErrUnlockerNotFound` and says to run
|
||||||
wraps `vault.ErrUnlockerNotFound` and says to run `secret unlocker select`,
|
`secret unlocker select`, without the advice to use the mnemonic.
|
||||||
without the advice to use the mnemonic.
|
|
||||||
- 2026-10-14: A `secret add --force` that cannot unlock the vault, given a wrong
|
|
||||||
passphrase or mnemonic, leaves the secret as it was
|
|
||||||
(https://git.eeqj.de/sneak/secret/issues/136). The new version used to be
|
|
||||||
written before the vault was unlocked to set `notAfter` on the version it
|
|
||||||
replaces, so each failed add left one more version that `secret version list`
|
|
||||||
showed. The vault is now unlocked, and that version's metadata read, before
|
|
||||||
anything is written. `generate secret --force` and `import --force` take the
|
|
||||||
same path.
|
|
||||||
- 2026-10-12: A mnemonic typed with extra spaces, tabs or line breaks gives the
|
- 2026-10-12: A mnemonic typed with extra spaces, tabs or line breaks gives the
|
||||||
same long-term key as its words separated by single spaces
|
same long-term key as its words separated by single spaces
|
||||||
(https://git.eeqj.de/sneak/secret/issues/135). The key was derived from the
|
(https://git.eeqj.de/sneak/secret/issues/135). The key was derived from the
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
// keeps its cause and ends by saying that the mnemonic still opens that
|
// keeps its cause and ends by saying that the mnemonic still opens that
|
||||||
// vault, but only for a vault that the mnemonic does open, and not when the
|
// vault, but only for a vault that the mnemonic does open, and not when the
|
||||||
// passphrase could not be read at all. When the current unlocker does not
|
// passphrase could not be read at all. When the current unlocker does not
|
||||||
// exist and the vault has another that `secret unlocker select` accepts, the
|
// exist and the vault has another, the error says to select one instead.
|
||||||
// error says to select one instead.
|
|
||||||
// When a secret's current file is missing, the error says how to make a
|
// When a secret's current file is missing, the error says how to make a
|
||||||
// version current again. Each test that pins such advice also follows it.
|
// version current again. Each test that pins such advice also follows it.
|
||||||
|
|
||||||
@@ -252,41 +251,6 @@ func TestMissingCurrentUnlockerNamesSelect(t *testing.T) {
|
|||||||
assert.Equal(t, "value", getSecret(t, c, "x"))
|
assert.Equal(t, "value", getSecret(t, c, "x"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMissingCurrentUnlockerNamesMnemonic checks the error of `secret get`
|
|
||||||
// when the unlocker that current-unlocker names does not exist and the
|
|
||||||
// vault's only other unlocker is the directory "odd", whose metadata names a
|
|
||||||
// type that `secret unlocker select` refuses: the error ends with the advice
|
|
||||||
// that the mnemonic still opens the vault, not with advice to select another
|
|
||||||
// unlocker. The test then follows that advice.
|
|
||||||
func TestMissingCurrentUnlockerNamesMnemonic(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := newTwoVaultFs(t)
|
|
||||||
unlockerDir, c := newUnlockTestCLI(t, fs)
|
|
||||||
|
|
||||||
require.NoError(t, fs.RemoveAll(unlockerDir))
|
|
||||||
require.NoError(t, afero.WriteFile(fs,
|
|
||||||
filepath.Join(unlockTestVaultDir, "unlockers.d", "odd",
|
|
||||||
"unlocker-metadata.json"),
|
|
||||||
[]byte(`{"type": "odd"}`), secret.FilePerms))
|
|
||||||
|
|
||||||
err := c.GetSecret(discardCmd(), "x")
|
|
||||||
|
|
||||||
var cause *os.PathError
|
|
||||||
|
|
||||||
require.ErrorAs(t, err, &cause)
|
|
||||||
require.NotErrorIs(t, err, vault.ErrUnlockerNotFound)
|
|
||||||
require.EqualError(t, err, "failed to unlock vault: "+
|
|
||||||
"failed to get long-term key: failed to get current unlocker: "+
|
|
||||||
"failed to read unlocker metadata: "+cause.Error()+mnemonicAdvice)
|
|
||||||
|
|
||||||
c.Mnemonic = testMnemonicBuffer(t)
|
|
||||||
require.NoError(t, c.UnlockersAdd("passphrase", discardCmd()))
|
|
||||||
|
|
||||||
c.Mnemonic = nil
|
|
||||||
assert.Equal(t, "value", getSecret(t, c, "x"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPassphraseNotReadNamesNoMnemonic runs `secret get x` on the built
|
// TestPassphraseNotReadNamesNoMnemonic runs `secret get x` on the built
|
||||||
// binary without SB_UNLOCK_PASSPHRASE and with a stdin that is not a
|
// binary without SB_UNLOCK_PASSPHRASE and with a stdin that is not a
|
||||||
// terminal, so the passphrase cannot be read. The unlocker was not tried,
|
// terminal, so the passphrase cannot be read. The unlocker was not tried,
|
||||||
|
|||||||
@@ -223,8 +223,7 @@ func newUnlockerRemoveCmd() *cobra.Command {
|
|||||||
`confirmation first, saying whether it is the vault's last ` +
|
`confirmation first, saying whether it is the vault's last ` +
|
||||||
`unlocker; when stdin is not a terminal, fails unless --force ` +
|
`unlocker; when stdin is not a terminal, fails unless --force ` +
|
||||||
`is given. Removing the current unlocker makes another of the ` +
|
`is given. Removing the current unlocker makes another of the ` +
|
||||||
`vault's unlockers current, if it has one that ` +
|
`vault's unlockers current, if it has one. Warning: Without ` +
|
||||||
`'secret unlocker select' accepts. Warning: Without ` +
|
|
||||||
`unlockers and without your mnemonic, vault data will be ` +
|
`unlockers and without your mnemonic, vault data will be ` +
|
||||||
`permanently inaccessible.`,
|
`permanently inaccessible.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/secret/internal/secret"
|
|
||||||
"sneak.berlin/go/secret/internal/vault"
|
"sneak.berlin/go/secret/internal/vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,77 +17,52 @@ import (
|
|||||||
// https://git.eeqj.de/sneak/secret/issues/137. A vault has a passphrase
|
// https://git.eeqj.de/sneak/secret/issues/137. A vault has a passphrase
|
||||||
// unlocker and a PGP unlocker, the current one. Removing the PGP unlocker
|
// unlocker and a PGP unlocker, the current one. Removing the PGP unlocker
|
||||||
// makes the passphrase unlocker current and says so, and the vault then
|
// makes the passphrase unlocker current and says so, and the vault then
|
||||||
// opens through it. Removing the passphrase unlocker too removes
|
// opens through it. Removing the passphrase unlocker too, the vault's last,
|
||||||
// current-unlocker.
|
// removes current-unlocker.
|
||||||
//
|
|
||||||
// The second case also gives the vault the unlocker directory "odd", whose
|
|
||||||
// metadata names a type that `secret unlocker select` refuses, and which
|
|
||||||
// sorts before the passphrase unlocker: neither removal makes it current.
|
|
||||||
//
|
//
|
||||||
//nolint:paralleltest // t.Setenv (GNUPGHOME) forbids parallel tests
|
//nolint:paralleltest // t.Setenv (GNUPGHOME) forbids parallel tests
|
||||||
func TestUnlockerRemoveCurrent(t *testing.T) {
|
func TestUnlockerRemoveCurrent(t *testing.T) {
|
||||||
tests := []struct {
|
newTestGPGKey(t)
|
||||||
name string
|
|
||||||
withOdd bool
|
|
||||||
}{
|
|
||||||
{name: "two unlockers", withOdd: false},
|
|
||||||
{name: "and the directory odd", withOdd: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Cleanup(passphrase.Destroy)
|
||||||
newTestGPGKey(t)
|
|
||||||
|
|
||||||
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
fs := afero.NewMemMapFs()
|
||||||
t.Cleanup(passphrase.Destroy)
|
vlt, err := vault.CreateVault(fs, listTestStateDir, listTestVaultName,
|
||||||
|
testMnemonicBuffer(t), nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphrase)
|
||||||
vlt, err := vault.CreateVault(fs, listTestStateDir,
|
require.NoError(t, err)
|
||||||
listTestVaultName, testMnemonicBuffer(t), nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
passphraseUnlocker, err := vlt.CreatePassphraseUnlocker(passphrase)
|
instance, cmd := newTestInstance(fs)
|
||||||
require.NoError(t, err)
|
instance.UnlockPassphrase = passphrase
|
||||||
|
|
||||||
if tt.withOdd {
|
cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
||||||
require.NoError(t, afero.WriteFile(fs,
|
require.NoError(t, instance.UnlockersAdd(unlockerTypePGP, cmd))
|
||||||
filepath.Join(testVaultDir(listTestVaultName), "unlockers.d",
|
|
||||||
"odd", "unlocker-metadata.json"),
|
|
||||||
[]byte(`{"type": "odd"}`), secret.FilePerms))
|
|
||||||
}
|
|
||||||
|
|
||||||
instance, cmd := newTestInstance(fs)
|
pgpUnlocker, err := vlt.GetCurrentUnlocker()
|
||||||
instance.UnlockPassphrase = passphrase
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, unlockerTypePGP, pgpUnlocker.GetType())
|
||||||
|
|
||||||
cmd.Flags().String("keyid", unreadableTestGPGUserID, "")
|
var output bytes.Buffer
|
||||||
require.NoError(t, instance.UnlockersAdd(unlockerTypePGP, cmd))
|
|
||||||
|
|
||||||
pgpUnlocker, err := vlt.GetCurrentUnlocker()
|
cmd.SetOut(&output)
|
||||||
require.NoError(t, err)
|
require.NoError(t, instance.UnlockersRemove(pgpUnlocker.GetID(), true, cmd))
|
||||||
require.Equal(t, unlockerTypePGP, pgpUnlocker.GetType())
|
assert.Contains(t, output.String(),
|
||||||
|
"Switched current unlocker to '"+passphraseUnlocker.GetID()+"'")
|
||||||
|
|
||||||
var output bytes.Buffer
|
reopened := vault.NewVault(fs, listTestStateDir, listTestVaultName)
|
||||||
|
reopened.UnlockPassphrase = passphrase
|
||||||
|
|
||||||
cmd.SetOut(&output)
|
_, err = reopened.GetOrDeriveLongTermKey()
|
||||||
require.NoError(t,
|
require.NoError(t, err)
|
||||||
instance.UnlockersRemove(pgpUnlocker.GetID(), true, cmd))
|
|
||||||
assert.Contains(t, output.String(),
|
|
||||||
"Switched current unlocker to '"+passphraseUnlocker.GetID()+"'")
|
|
||||||
|
|
||||||
reopened := vault.NewVault(fs, listTestStateDir, listTestVaultName)
|
require.NoError(t,
|
||||||
reopened.UnlockPassphrase = passphrase
|
instance.UnlockersRemove(passphraseUnlocker.GetID(), true, cmd))
|
||||||
|
|
||||||
_, err = reopened.GetOrDeriveLongTermKey()
|
exists, err := afero.Exists(fs,
|
||||||
require.NoError(t, err)
|
filepath.Join(testVaultDir(listTestVaultName), "current-unlocker"))
|
||||||
|
require.NoError(t, err)
|
||||||
require.NoError(t,
|
assert.False(t, exists, "current-unlocker is left after the last unlocker")
|
||||||
instance.UnlockersRemove(passphraseUnlocker.GetID(), true, cmd))
|
|
||||||
|
|
||||||
exists, err := afero.Exists(fs, filepath.Join(
|
|
||||||
testVaultDir(listTestVaultName), "current-unlocker"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.False(t, exists,
|
|
||||||
"current-unlocker is left after the passphrase unlocker")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+12
-39
@@ -219,15 +219,6 @@ func (v *Vault) addVersion(
|
|||||||
name, secretDir string, value *memguard.LockedBuffer,
|
name, secretDir string, value *memguard.LockedBuffer,
|
||||||
previousVersion *secret.Version,
|
previousVersion *secret.Version,
|
||||||
) error {
|
) error {
|
||||||
// Unlock the vault before writing anything, so that a wrong passphrase
|
|
||||||
// or mnemonic leaves the secret as it was
|
|
||||||
err := v.loadPreviousVersionMetadata(previousVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Taken after the unlock, which can wait for the user to type the
|
|
||||||
// passphrase, so that the new version starts when it is created
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
// Create the new version and save the encrypted value
|
// Create the new version and save the encrypted value
|
||||||
@@ -641,35 +632,8 @@ func (v *Vault) checkExistingSecret(
|
|||||||
return true, previousVersion, nil
|
return true, previousVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadPreviousVersionMetadata unlocks the vault and loads the metadata of
|
|
||||||
// the version being superseded. It is a no-op when previousVersion is nil.
|
|
||||||
func (v *Vault) loadPreviousVersionMetadata(
|
|
||||||
previousVersion *secret.Version,
|
|
||||||
) error {
|
|
||||||
if previousVersion == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to get long-term key for previous version", "error", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to get long-term key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = previousVersion.LoadMetadata(ltIdentity)
|
|
||||||
if err != nil {
|
|
||||||
secret.Debug("Failed to load previous version metadata", "error", err)
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to load previous version metadata: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updatePreviousVersion sets the notAfter timestamp on the version being
|
// updatePreviousVersion sets the notAfter timestamp on the version being
|
||||||
// superseded, whose metadata loadPreviousVersionMetadata has loaded. It is
|
// superseded. It is a no-op when previousVersion is nil.
|
||||||
// a no-op when previousVersion is nil.
|
|
||||||
func (v *Vault) updatePreviousVersion(
|
func (v *Vault) updatePreviousVersion(
|
||||||
previousVersion *secret.Version, now *time.Time,
|
previousVersion *secret.Version, now *time.Time,
|
||||||
) error {
|
) error {
|
||||||
@@ -677,13 +641,22 @@ func (v *Vault) updatePreviousVersion(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadPreviousVersionMetadata unlocked the vault, so this returns the
|
// Get long-term key to decrypt/encrypt metadata
|
||||||
// key it keeps in memory
|
|
||||||
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
ltIdentity, err := v.GetOrDeriveLongTermKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
secret.Debug("Failed to get long-term key for metadata update", "error", err)
|
||||||
|
|
||||||
return fmt.Errorf("failed to get long-term key: %w", err)
|
return fmt.Errorf("failed to get long-term key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load previous version metadata
|
||||||
|
err = previousVersion.LoadMetadata(ltIdentity)
|
||||||
|
if err != nil {
|
||||||
|
secret.Debug("Failed to load previous version metadata", "error", err)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to load previous version metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Update notAfter timestamp
|
// Update notAfter timestamp
|
||||||
previousVersion.Metadata.NotAfter = now
|
previousVersion.Metadata.NotAfter = now
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@
|
|||||||
// version
|
// version
|
||||||
// - TestVaultVersionTimestamps: Tests timestamp logic (notBefore/notAfter)
|
// - TestVaultVersionTimestamps: Tests timestamp logic (notBefore/notAfter)
|
||||||
// across versions
|
// across versions
|
||||||
// - TestVaultVersionTimestampsAfterUnlock: Tests that those timestamps are
|
|
||||||
// taken after AddSecret unlocks the vault
|
|
||||||
// - TestVaultGetNonExistentVersion: Tests error handling for invalid versions
|
// - TestVaultGetNonExistentVersion: Tests error handling for invalid versions
|
||||||
// - TestUpdateVersionMetadata: Tests metadata update functionality
|
// - TestUpdateVersionMetadata: Tests metadata update functionality
|
||||||
//
|
//
|
||||||
@@ -297,70 +295,6 @@ func TestVaultVersionTimestamps(t *testing.T) {
|
|||||||
assert.Nil(t, secondVersion.Metadata.NotAfter) // Current version
|
assert.Nil(t, secondVersion.Metadata.NotAfter) // Current version
|
||||||
}
|
}
|
||||||
|
|
||||||
// longTermKeyReadFs records when longterm.age, the last file an unlock
|
|
||||||
// reads, was last opened.
|
|
||||||
type longTermKeyReadFs struct {
|
|
||||||
afero.Fs
|
|
||||||
|
|
||||||
readAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:ireturn // afero.File is the interface required by afero.Fs
|
|
||||||
func (f *longTermKeyReadFs) Open(name string) (afero.File, error) {
|
|
||||||
if filepath.Base(name) == "longterm.age" {
|
|
||||||
f.readAt = time.Now()
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Fs.Open(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVaultVersionTimestampsAfterUnlock checks that when AddSecret has to
|
|
||||||
// unlock the vault to replace a version, the new version's notBefore and the
|
|
||||||
// replaced version's notAfter are taken after the unlock, which can wait for
|
|
||||||
// the user to type the passphrase.
|
|
||||||
func TestVaultVersionTimestampsAfterUnlock(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := &longTermKeyReadFs{Fs: afero.NewMemMapFs()}
|
|
||||||
|
|
||||||
mnemonic := memguard.NewBufferFromBytes([]byte(testMnemonic))
|
|
||||||
defer mnemonic.Destroy()
|
|
||||||
|
|
||||||
passphrase := memguard.NewBufferFromBytes([]byte("test-passphrase"))
|
|
||||||
defer passphrase.Destroy()
|
|
||||||
|
|
||||||
created, err := CreateVault(fs, testStateDir, "test", mnemonic, passphrase)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
addTestSecretToVault(t, created, testSecretPath, []byte("version-1"), false)
|
|
||||||
|
|
||||||
// A new instance is locked; AddSecret unlocks it with the passphrase
|
|
||||||
vault := NewVault(fs, testStateDir, "test")
|
|
||||||
vault.SetUnlockPassphrase(passphrase)
|
|
||||||
|
|
||||||
addTestSecretToVault(t, vault, testSecretPath, []byte("version-2"), true)
|
|
||||||
require.False(t, fs.readAt.IsZero(), "AddSecret did not unlock the vault")
|
|
||||||
|
|
||||||
ltIdentity, err := vault.GetOrDeriveLongTermKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
vaultDir, _ := vault.GetDirectory()
|
|
||||||
secretDir := filepath.Join(vaultDir, "secrets.d", "test%secret")
|
|
||||||
|
|
||||||
versions, err := secret.ListVersions(fs, secretDir)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, versions, 2)
|
|
||||||
|
|
||||||
newVersion := secret.NewVersion(vault, testSecretPath, versions[0])
|
|
||||||
require.NoError(t, newVersion.LoadMetadata(ltIdentity))
|
|
||||||
|
|
||||||
replacedVersion := secret.NewVersion(vault, testSecretPath, versions[1])
|
|
||||||
require.NoError(t, replacedVersion.LoadMetadata(ltIdentity))
|
|
||||||
|
|
||||||
assert.False(t, newVersion.Metadata.NotBefore.Before(fs.readAt))
|
|
||||||
assert.False(t, replacedVersion.Metadata.NotAfter.Before(fs.readAt))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVaultGetNonExistentVersion(t *testing.T) {
|
func TestVaultGetNonExistentVersion(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+23
-43
@@ -21,7 +21,6 @@ import (
|
|||||||
// Unlocker metadata type strings.
|
// Unlocker metadata type strings.
|
||||||
const (
|
const (
|
||||||
unlockerTypePassphrase = "passphrase"
|
unlockerTypePassphrase = "passphrase"
|
||||||
unlockerTypePGP = "pgp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCurrentUnlocker returns the current unlocker for this vault
|
// GetCurrentUnlocker returns the current unlocker for this vault
|
||||||
@@ -82,7 +81,7 @@ func (v *Vault) GetCurrentUnlocker() (secret.Unlocker, error) {
|
|||||||
passphraseUnlocker := secret.NewPassphraseUnlocker(v.fs, unlockerDir, metadata)
|
passphraseUnlocker := secret.NewPassphraseUnlocker(v.fs, unlockerDir, metadata)
|
||||||
passphraseUnlocker.Passphrase = v.UnlockPassphrase
|
passphraseUnlocker.Passphrase = v.UnlockPassphrase
|
||||||
unlocker = passphraseUnlocker
|
unlocker = passphraseUnlocker
|
||||||
case unlockerTypePGP:
|
case "pgp":
|
||||||
secret.Debug("Creating PGP unlocker instance", "unlocker_type", metadata.Type)
|
secret.Debug("Creating PGP unlocker instance", "unlocker_type", metadata.Type)
|
||||||
|
|
||||||
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, metadata)
|
unlocker = secret.NewPGPUnlocker(v.fs, unlockerDir, metadata)
|
||||||
@@ -129,10 +128,9 @@ func (v *Vault) resolveUnlockerDirectory(currentUnlockerPath string) (string, er
|
|||||||
|
|
||||||
// checkCurrentUnlockerExists fails with ErrUnlockerNotFound, saying to
|
// checkCurrentUnlockerExists fails with ErrUnlockerNotFound, saying to
|
||||||
// select another unlocker, when unlockerDir, the unlocker directory that
|
// select another unlocker, when unlockerDir, the unlocker directory that
|
||||||
// current-unlocker names, does not exist and the vault has another unlocker
|
// current-unlocker names, does not exist and the vault has other unlockers.
|
||||||
// that SelectUnlocker accepts. When it has none, it returns nil: reading the
|
// When the vault has none, it returns nil: reading the missing unlocker then
|
||||||
// missing unlocker then fails, with the advice that the mnemonic still opens
|
// fails, with the advice that the mnemonic still opens the vault.
|
||||||
// the vault.
|
|
||||||
func (v *Vault) checkCurrentUnlockerExists(unlockerDir string) error {
|
func (v *Vault) checkCurrentUnlockerExists(unlockerDir string) error {
|
||||||
exists, err := afero.DirExists(v.fs, unlockerDir)
|
exists, err := afero.DirExists(v.fs, unlockerDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -143,12 +141,12 @@ func (v *Vault) checkCurrentUnlockerExists(unlockerDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
otherID, err := v.firstOtherUnlockerID(filepath.Base(unlockerDir))
|
unlockers, err := v.ListUnlockers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if otherID == "" {
|
if len(unlockers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +196,7 @@ func (v *Vault) findUnlockerByID(
|
|||||||
switch metadata.Type {
|
switch metadata.Type {
|
||||||
case unlockerTypePassphrase:
|
case unlockerTypePassphrase:
|
||||||
tempUnlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, metadata)
|
tempUnlocker = secret.NewPassphraseUnlocker(v.fs, unlockerDirPath, metadata)
|
||||||
case unlockerTypePGP:
|
case "pgp":
|
||||||
tempUnlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, metadata)
|
tempUnlocker = secret.NewPGPUnlocker(v.fs, unlockerDirPath, metadata)
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
@@ -323,9 +321,10 @@ func (v *Vault) HasUnlocker(unlockerID string) (bool, error) {
|
|||||||
// type is unknown, so only the directory is removed.
|
// type is unknown, so only the directory is removed.
|
||||||
//
|
//
|
||||||
// It returns the ID of the unlocker it made current, or "" when it made none
|
// It returns the ID of the unlocker it made current, or "" when it made none
|
||||||
// current. If the removed unlocker is the current one, the unlocker
|
// current. If the removed unlocker is the current one, the vault's first
|
||||||
// firstOtherUnlockerID returns is made current before the removal; with
|
// other unlocker in ID order, the order `secret unlocker list` shows, is
|
||||||
// none, current-unlocker is removed after it.
|
// made current before the removal; with no other, current-unlocker is
|
||||||
|
// removed after it.
|
||||||
func (v *Vault) RemoveUnlocker(unlockerID string) (string, error) {
|
func (v *Vault) RemoveUnlocker(unlockerID string) (string, error) {
|
||||||
vaultDir, err := v.GetDirectory()
|
vaultDir, err := v.GetDirectory()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -382,45 +381,26 @@ func (v *Vault) RemoveUnlocker(unlockerID string) (string, error) {
|
|||||||
return newCurrentID, nil
|
return newCurrentID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// selectOtherUnlocker makes the unlocker firstOtherUnlockerID returns for
|
// selectOtherUnlocker makes the vault's first unlocker in ID order other
|
||||||
// unlockerID the current unlocker and returns its ID. It returns "" when
|
// than unlockerID the current unlocker and returns its ID. It returns ""
|
||||||
// there is no such unlocker.
|
// when the vault has no other unlocker.
|
||||||
func (v *Vault) selectOtherUnlocker(unlockerID string) (string, error) {
|
func (v *Vault) selectOtherUnlocker(unlockerID string) (string, error) {
|
||||||
otherID, err := v.firstOtherUnlockerID(unlockerID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if otherID == "" {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = v.SelectUnlocker(otherID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return otherID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// firstOtherUnlockerID returns the ID of the vault's first unlocker other
|
|
||||||
// than unlockerID, in ID order, the order `secret unlocker list` shows, that
|
|
||||||
// SelectUnlocker accepts, or "" when there is none. SelectUnlocker refuses
|
|
||||||
// an unlocker whose metadata names a type other than passphrase or pgp,
|
|
||||||
// which ListUnlockers still returns.
|
|
||||||
func (v *Vault) firstOtherUnlockerID(unlockerID string) (string, error) {
|
|
||||||
unlockers, err := v.ListUnlockers()
|
unlockers, err := v.ListUnlockers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range slices.Sorted(maps.Keys(unlockers)) {
|
for _, id := range slices.Sorted(maps.Keys(unlockers)) {
|
||||||
unlockerType := unlockers[id].Type
|
if id == unlockerID {
|
||||||
|
continue
|
||||||
if id != unlockerID && (unlockerType == unlockerTypePassphrase ||
|
|
||||||
unlockerType == unlockerTypePGP) {
|
|
||||||
return id, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = v.SelectUnlocker(id)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"filippo.io/age"
|
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -104,78 +103,3 @@ func TestAddSecretCleansUpOnFailure(t *testing.T) {
|
|||||||
require.Len(t, entries, 1)
|
require.Len(t, entries, 1)
|
||||||
assert.Equal(t, "pub.age", entries[0].Name())
|
assert.Equal(t, "pub.age", entries[0].Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAddSecretForceWithWrongPassphraseChangesNothing checks that adding a
|
|
||||||
// version to an existing secret, with a passphrase that does not open the
|
|
||||||
// vault, fails and leaves the secret's directory as it was.
|
|
||||||
func TestAddSecretForceWithWrongPassphraseChangesNothing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
|
||||||
|
|
||||||
passphrase := memguard.NewBufferFromBytes([]byte(testPassphrase))
|
|
||||||
t.Cleanup(passphrase.Destroy)
|
|
||||||
|
|
||||||
created, err := vault.CreateVault(fs, testStateDir, testVaultName,
|
|
||||||
testMnemonicBuffer(t), passphrase)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
first := memguard.NewBufferFromBytes([]byte("first"))
|
|
||||||
t.Cleanup(first.Destroy)
|
|
||||||
|
|
||||||
require.NoError(t, created.AddSecret(testSecretName, first, false))
|
|
||||||
|
|
||||||
wrong := memguard.NewBufferFromBytes([]byte("wrong passphrase"))
|
|
||||||
t.Cleanup(wrong.Destroy)
|
|
||||||
|
|
||||||
vlt := vault.NewVault(fs, testStateDir, testVaultName)
|
|
||||||
vlt.SetUnlockPassphrase(wrong)
|
|
||||||
|
|
||||||
secretDir := filepath.Join(testStateDir, "vaults.d", testVaultName,
|
|
||||||
"secrets.d", testSecretName)
|
|
||||||
before := dirContents(t, fs, secretDir)
|
|
||||||
|
|
||||||
second := memguard.NewBufferFromBytes([]byte("second"))
|
|
||||||
t.Cleanup(second.Destroy)
|
|
||||||
|
|
||||||
err = vlt.AddSecret(testSecretName, second, true)
|
|
||||||
|
|
||||||
var noMatch *age.NoIdentityMatchError
|
|
||||||
|
|
||||||
require.ErrorAs(t, err, &noMatch)
|
|
||||||
assert.Equal(t, before, dirContents(t, fs, secretDir))
|
|
||||||
}
|
|
||||||
|
|
||||||
// dirContents maps every file under dir to its contents, and every
|
|
||||||
// directory, written with a trailing "/", to "".
|
|
||||||
func dirContents(t *testing.T, fs afero.Fs, dir string) map[string]string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
contents := map[string]string{}
|
|
||||||
|
|
||||||
err := afero.Walk(fs, dir, func(
|
|
||||||
path string, info os.FileInfo, err error,
|
|
||||||
) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() {
|
|
||||||
contents[path+"/"] = ""
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := afero.ReadFile(fs, path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
contents[path] = string(content)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return contents
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user