Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0e857ff9d |
@@ -232,13 +232,18 @@ func TestCrossVaultMoveUnlocksBeforeLocking(t *testing.T) {
|
||||
// TestCrossVaultMoveChecksAgainOnceLocked runs a move between vaults, given
|
||||
// the mnemonic, as TestCommandsCheckAgainOnceLocked runs the commands in its
|
||||
// table: once the move waits for the state directory lock, the test removes
|
||||
// the source secret or the destination vault, and the move must then fail
|
||||
// and change nothing.
|
||||
// the source secret or the destination vault, or creates the destination
|
||||
// vault again from another mnemonic, and the move must then fail and change
|
||||
// nothing.
|
||||
//
|
||||
//nolint:paralleltest // waitingForLock sees any test's command waiting for the lock
|
||||
func TestCrossVaultMoveChecksAgainOnceLocked(t *testing.T) {
|
||||
mnemonic := testMnemonicBuffer(t)
|
||||
|
||||
otherMnemonic := memguard.NewBufferFromBytes([]byte("legal winner thank " +
|
||||
"year wave sausage worth useful legal winner thank yellow"))
|
||||
t.Cleanup(otherMnemonic.Destroy)
|
||||
|
||||
move := func(cli *Instance) error {
|
||||
cli.Mnemonic = mnemonic
|
||||
|
||||
@@ -256,6 +261,19 @@ func TestCrossVaultMoveChecksAgainOnceLocked(t *testing.T) {
|
||||
return fs.RemoveAll(testStateDir + "/vaults.d/other")
|
||||
}, vault.ErrVaultNotFound)
|
||||
})
|
||||
|
||||
t.Run("destination vault created again from another mnemonic", func(t *testing.T) {
|
||||
requireChecksAgainOnceLocked(t, move, func(fs afero.Fs) error {
|
||||
err := fs.RemoveAll(testStateDir + "/vaults.d/other")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = vault.CreateVault(fs, testStateDir, "other", otherMnemonic, nil)
|
||||
|
||||
return err
|
||||
}, errVaultCreatedAgain)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCrossVaultMoveRefusesExistingSecretBeforeUnlocking checks that a move
|
||||
|
||||
+13
-5
@@ -1084,10 +1084,13 @@ func (cli *Instance) sameDirectory(dir, other string) (bool, error) {
|
||||
|
||||
// moveSecretCrossVault handles moving between two different vaults. Its
|
||||
// caller, MoveSecret, has already checked both secret names and that both
|
||||
// vaults exist. The copy needs the source vault's long-term key; the
|
||||
// destination vault is unlocked only to check that it can be. Either can ask
|
||||
// for a passphrase, so both happen before it takes the state directory lock,
|
||||
// and no other command waits while the user answers.
|
||||
// vaults exist. The copy needs the source vault's long-term key and is
|
||||
// encrypted to the destination vault's long-term public key. The
|
||||
// destination vault is unlocked to check that the copy will open with what
|
||||
// the user gave, and, once the state directory lock is held, that it still
|
||||
// has the key unlocked. Either unlock can ask for a passphrase, so both
|
||||
// happen before the lock is taken, and no other command waits while the
|
||||
// user answers.
|
||||
func (cli *Instance) moveSecretCrossVault(
|
||||
cmd *cobra.Command,
|
||||
srcVault *vault.Vault, srcSecretName string,
|
||||
@@ -1108,7 +1111,7 @@ func (cli *Instance) moveSecretCrossVault(
|
||||
destVault.Mnemonic, destVault.UnlockPassphrase = cli.Mnemonic, cli.UnlockPassphrase
|
||||
|
||||
// Unlock destination vault (will fail if neither mnemonic nor unlocker available)
|
||||
_, err = destVault.GetOrDeriveLongTermKey()
|
||||
destIdentity, err := destVault.GetOrDeriveLongTermKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unlock destination vault '%s': %w", destVault.Name, err)
|
||||
}
|
||||
@@ -1125,6 +1128,11 @@ func (cli *Instance) moveSecretCrossVault(
|
||||
}
|
||||
defer release()
|
||||
|
||||
err = cli.checkLongTermKey(destVault, destIdentity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cli.copySecretAndRemoveSource(
|
||||
cmd, srcVault, srcSecretName, destVault, destSecretName, force)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
"sneak.berlin/go/secret/internal/secret"
|
||||
@@ -442,29 +443,41 @@ func (cli *Instance) unlockThenLock(vlt *vault.Vault) (func(), error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = cli.existingVault(vlt.GetName())
|
||||
err = cli.checkLongTermKey(vlt, ltIdentity)
|
||||
if err != nil {
|
||||
release()
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
// checkLongTermKey checks, for the vault vlt unlocked as ltIdentity before
|
||||
// the state directory lock was taken, that vlt still exists and that its
|
||||
// long-term public key is still that of ltIdentity. Another command may have
|
||||
// removed the vault in the meantime, or removed it and created it again with
|
||||
// another long-term key. The caller holds the lock.
|
||||
func (cli *Instance) checkLongTermKey(
|
||||
vlt *vault.Vault, ltIdentity *age.X25519Identity,
|
||||
) error {
|
||||
_, err := cli.existingVault(vlt.GetName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vaultDir, _ := vlt.GetDirectory()
|
||||
|
||||
ltPubKey, err := afero.ReadFile(cli.fs, filepath.Join(vaultDir, "pub.age"))
|
||||
if err != nil {
|
||||
release()
|
||||
|
||||
return nil, fmt.Errorf("failed to read long-term public key: %w", err)
|
||||
return fmt.Errorf("failed to read long-term public key: %w", err)
|
||||
}
|
||||
|
||||
if string(ltPubKey) != ltIdentity.Recipient().String() {
|
||||
release()
|
||||
|
||||
return nil, fmt.Errorf("vault '%s' %w", vlt.GetName(), errVaultCreatedAgain)
|
||||
return fmt.Errorf("vault '%s' %w", vlt.GetName(), errVaultCreatedAgain)
|
||||
}
|
||||
|
||||
return release, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// autoSelectUnlocker selects the newly created unlocker as current,
|
||||
|
||||
Reference in New Issue
Block a user