Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cad38994dd |
No files matched your search
@@ -18,14 +18,17 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-14: A wrong passphrase, from `SB_UNLOCK_PASSPHRASE` or typed at the
|
- 2026-10-14: `secret list` gives each secret's own creation and last-update
|
||||||
prompt, gives the error `wrong passphrase for unlocker <id>`
|
times (https://git.eeqj.de/sneak/secret/issues/142). It gave the time of the
|
||||||
(https://git.eeqj.de/sneak/secret/issues/157). It used to be age's "no
|
listing for both, which `Secret.LoadMetadata` set to the current time.
|
||||||
identity matched any of the recipients", followed by advice to give the vault
|
`LoadMetadata` now reads file modification times, so listing still needs no
|
||||||
a new unlocker from its mnemonic, which would have replaced an unlocker that
|
passphrase: the last update is when the secret's `current` file was written,
|
||||||
works. The error now wraps `secret.ErrWrongPassphrase` and carries no advice
|
and the creation is when its oldest version's `value.age` was written. That
|
||||||
to use the mnemonic. A `priv.age` that is not an age file still gets that
|
version's directory would be wrong, because the next add rewrites the
|
||||||
advice.
|
version's `metadata.age` in it. `GetSecretObject`, which `secret get` also
|
||||||
|
uses, no longer loads the times; `secret list` loads them itself. A move to
|
||||||
|
another vault gives each copied `value.age` and `current` the modification
|
||||||
|
time of the file it was copied from, so the moved secret keeps its times.
|
||||||
- 2026-10-14: The passphrase and mnemonic prompts ask on the controlling
|
- 2026-10-14: The passphrase and mnemonic prompts ask on the controlling
|
||||||
terminal (`/dev/tty`) instead of stdin, so `echo v | secret add s1 --force`,
|
terminal (`/dev/tty`) instead of stdin, so `echo v | secret add s1 --force`,
|
||||||
`cat file | secret encrypt key` and `cat file.age | secret decrypt key` ask
|
`cat file | secret encrypt key` and `cat file.age | secret decrypt key` ask
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Unlock Failure Tests
|
// Unlock Failure Tests
|
||||||
//
|
//
|
||||||
// When a vault cannot be opened through its current unlocker, because a
|
// When a vault cannot be opened through its current unlocker, because a
|
||||||
// file the unlocker needs is missing, the error keeps its cause and ends by
|
// file the unlocker needs is missing or the passphrase is wrong, the error
|
||||||
// saying that the mnemonic still opens that vault, but only for a vault that
|
// keeps its cause and ends by saying that the mnemonic still opens that
|
||||||
// the mnemonic does open, and not when the passphrase could not be read at
|
// vault, but only for a vault that the mnemonic does open, and not when the
|
||||||
// all. A wrong passphrase gives an error that says so, without that advice.
|
// passphrase could not be read at all. When the current unlocker does not
|
||||||
// When the current unlocker does not exist and the vault has another that
|
// exist and the vault has another that `secret unlocker select` accepts, the
|
||||||
// `secret unlocker select` accepts, 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.
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
"github.com/awnumar/memguard"
|
"github.com/awnumar/memguard"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -164,48 +165,26 @@ func TestUnlockFailureNamesMnemonic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWrongPassphraseNamesPassphrase checks the error of `secret get` given
|
// TestWrongPassphraseNamesMnemonic checks the error of `secret get` given a
|
||||||
// a passphrase that does not decrypt the passphrase unlocker: it says that
|
// passphrase that does not decrypt the passphrase unlocker: it keeps age's
|
||||||
// the passphrase is wrong, without the advice to use the mnemonic, since the
|
// error and ends with the advice that the mnemonic still opens the vault.
|
||||||
// unlocker still opens the vault with the right passphrase. The test then
|
func TestWrongPassphraseNamesMnemonic(t *testing.T) {
|
||||||
// gives the right passphrase.
|
|
||||||
func TestWrongPassphraseNamesPassphrase(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
unlockerDir, c := newUnlockTestCLI(t, newTwoVaultFs(t))
|
_, c := newUnlockTestCLI(t, newTwoVaultFs(t))
|
||||||
c.UnlockPassphrase = memguard.NewBufferFromBytes([]byte("wrong passphrase"))
|
c.UnlockPassphrase = memguard.NewBufferFromBytes([]byte("wrong passphrase"))
|
||||||
t.Cleanup(c.UnlockPassphrase.Destroy)
|
t.Cleanup(c.UnlockPassphrase.Destroy)
|
||||||
|
|
||||||
err := c.GetSecret(discardCmd(), "x")
|
err := c.GetSecret(discardCmd(), "x")
|
||||||
|
|
||||||
require.ErrorIs(t, err, secret.ErrWrongPassphrase)
|
var noMatch *age.NoIdentityMatchError
|
||||||
assert.NotContains(t, err.Error(), mnemonicAdvice)
|
|
||||||
|
require.ErrorAs(t, err, &noMatch)
|
||||||
|
|
||||||
require.EqualError(t, err, "failed to unlock vault: "+
|
require.EqualError(t, err, "failed to unlock vault: "+
|
||||||
"failed to get long-term key: failed to get unlocker identity: "+
|
"failed to get long-term key: failed to get unlocker identity: "+
|
||||||
"wrong passphrase for unlocker "+filepath.Base(unlockerDir))
|
"failed to decrypt unlocker private key: failed to create decryptor: "+
|
||||||
|
noMatch.Error()+mnemonicAdvice)
|
||||||
c.UnlockPassphrase = memguard.NewBufferFromBytes([]byte(testPassphrase))
|
|
||||||
t.Cleanup(c.UnlockPassphrase.Destroy)
|
|
||||||
assert.Equal(t, "value", getSecret(t, c, "x"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDamagedUnlockerNamesMnemonic checks the error of `secret get` when the
|
|
||||||
// passphrase unlocker's priv.age is not an age file: the passphrase is not
|
|
||||||
// blamed, and the error ends with the advice that the mnemonic still opens
|
|
||||||
// the vault.
|
|
||||||
func TestDamagedUnlockerNamesMnemonic(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := newTwoVaultFs(t)
|
|
||||||
unlockerDir, c := newUnlockTestCLI(t, fs)
|
|
||||||
|
|
||||||
require.NoError(t, afero.WriteFile(fs, filepath.Join(unlockerDir, "priv.age"),
|
|
||||||
[]byte("damaged"), secret.FilePerms))
|
|
||||||
|
|
||||||
err := c.GetSecret(discardCmd(), "x")
|
|
||||||
|
|
||||||
require.NotErrorIs(t, err, secret.ErrWrongPassphrase)
|
|
||||||
require.ErrorContains(t, err, mnemonicAdvice)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMoveUnlockFailureNamesVault checks the error of `secret move` into
|
// TestMoveUnlockFailureNamesVault checks the error of `secret move` into
|
||||||
|
|||||||
@@ -16,10 +16,6 @@ import (
|
|||||||
// empty. A passphrase unlocker that fails with it was not tried.
|
// empty. A passphrase unlocker that fails with it was not tried.
|
||||||
var ErrPassphraseNotRead = errors.New("failed to read passphrase")
|
var ErrPassphraseNotRead = errors.New("failed to read passphrase")
|
||||||
|
|
||||||
// ErrWrongPassphrase is wrapped in the error of GetIdentity when the
|
|
||||||
// passphrase does not decrypt the unlocker's private key.
|
|
||||||
var ErrWrongPassphrase = errors.New("wrong passphrase")
|
|
||||||
|
|
||||||
// PassphraseUnlocker represents a passphrase-protected unlocker
|
// PassphraseUnlocker represents a passphrase-protected unlocker
|
||||||
type PassphraseUnlocker struct {
|
type PassphraseUnlocker struct {
|
||||||
Directory string
|
Directory string
|
||||||
@@ -77,12 +73,6 @@ func (p *PassphraseUnlocker) GetIdentity() (*age.X25519Identity, error) {
|
|||||||
Debug("Failed to decrypt unlocker private key",
|
Debug("Failed to decrypt unlocker private key",
|
||||||
"error", err, "unlocker_id", p.GetID())
|
"error", err, "unlocker_id", p.GetID())
|
||||||
|
|
||||||
// age says that no identity matched when the passphrase is wrong
|
|
||||||
var noMatch *age.NoIdentityMatchError
|
|
||||||
if errors.As(err, &noMatch) {
|
|
||||||
return nil, fmt.Errorf("%w for unlocker %s", ErrWrongPassphrase, p.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
return nil, fmt.Errorf("failed to decrypt unlocker private key: %w", err)
|
||||||
}
|
}
|
||||||
defer privKeyBuffer.Destroy()
|
defer privKeyBuffer.Destroy()
|
||||||
|
|||||||
@@ -291,13 +291,10 @@ func (v *Vault) unlockLongTermKey(
|
|||||||
// without a mnemonic records none, and without its metadata the key cannot
|
// without a mnemonic records none, and without its metadata the key cannot
|
||||||
// be derived. It is not added when the passphrase could not be read: the
|
// be derived. It is not added when the passphrase could not be read: the
|
||||||
// unlocker was not tried, and adding one would need a passphrase read the
|
// unlocker was not tried, and adding one would need a passphrase read the
|
||||||
// same way. Nor is it added when the passphrase is wrong: the unlocker still
|
// same way. Nor is it added when the current unlocker does not exist and the
|
||||||
// opens the vault with the right one. Nor is it added when the current
|
// vault has others: the error already says to select one of them.
|
||||||
// unlocker does not exist and the vault has others: the error already says
|
|
||||||
// to select one of them.
|
|
||||||
func (v *Vault) withMnemonicAdvice(err error) error {
|
func (v *Vault) withMnemonicAdvice(err error) error {
|
||||||
if errors.Is(err, secret.ErrPassphraseNotRead) ||
|
if errors.Is(err, secret.ErrPassphraseNotRead) ||
|
||||||
errors.Is(err, secret.ErrWrongPassphrase) ||
|
|
||||||
errors.Is(err, ErrUnlockerNotFound) {
|
errors.Is(err, ErrUnlockerNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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"
|
||||||
@@ -139,7 +140,9 @@ func TestAddSecretForceWithWrongPassphraseChangesNothing(t *testing.T) {
|
|||||||
|
|
||||||
err = vlt.AddSecret(testSecretName, second, true)
|
err = vlt.AddSecret(testSecretName, second, true)
|
||||||
|
|
||||||
require.ErrorIs(t, err, secret.ErrWrongPassphrase)
|
var noMatch *age.NoIdentityMatchError
|
||||||
|
|
||||||
|
require.ErrorAs(t, err, &noMatch)
|
||||||
assert.Equal(t, before, dirContents(t, fs, secretDir))
|
assert.Equal(t, before, dirContents(t, fs, secretDir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user