Parse the age identity key once and accept every identity in it (closes #165)
Restore and verify --deep now parse the configured age secret key a single time through a new helper that uses age.ParseIdentities and hands every identity to age.Decrypt. A key file with several identities (a whole age-keygen file) is fully accepted, so a blob encrypted to any of its recipients decrypts, not just the first. The helper is the first step of both commands, so a missing or unparseable key fails before anything is downloaded. Its error names the config source and never echoes the key value. config.extractAgeSecretKey and its silent fallback are removed; the key is stored raw and parsed only where decryption happens. README, the restore help, and the missing-key error now read the key from a file with \$(cat ...) rather than typed literally, keeping it out of shell history. Model: opus-4-8
This commit was merged in pull request #196.
This commit is contained in:
1 parent
bd9656dbd4
commit
d88ed64489
11 files changed
+329
-116
No files matched your search
@@ -0,0 +1,108 @@
|
||||
package vaultik //nolint:testpackage // exercises unexported restoreIdentities
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/vaultik/internal/blobgen"
|
||||
"sneak.berlin/go/vaultik/internal/config"
|
||||
)
|
||||
|
||||
// encryptBlobTo returns a blobgen blob of plaintext encrypted to exactly
|
||||
// one recipient, so a decryptor succeeds only if it holds that recipient's
|
||||
// identity.
|
||||
func encryptBlobTo(t *testing.T, recipient string, plaintext []byte) []byte {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
writer, err := blobgen.NewWriter(&buf, 1, []string{recipient})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = writer.Write(plaintext)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, writer.Close())
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// decryptBlobWith reads a blob back through the identities and returns its
|
||||
// plaintext.
|
||||
func decryptBlobWith(t *testing.T, blob []byte, identities []age.Identity) []byte {
|
||||
t.Helper()
|
||||
|
||||
reader, err := blobgen.NewReader(bytes.NewReader(blob), identities...)
|
||||
require.NoError(t, err)
|
||||
|
||||
plaintext, err := io.ReadAll(reader)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, reader.Close())
|
||||
|
||||
return plaintext
|
||||
}
|
||||
|
||||
// TestRestoreIdentitiesAcceptsEveryIdentity proves a key file holding two
|
||||
// identities yields both, so a blob encrypted only to the second
|
||||
// recipient — the one the previous single-identity parse dropped — still
|
||||
// decrypts.
|
||||
func TestRestoreIdentitiesAcceptsEveryIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, err := age.GenerateX25519Identity()
|
||||
require.NoError(t, err)
|
||||
|
||||
second, err := age.GenerateX25519Identity()
|
||||
require.NoError(t, err)
|
||||
|
||||
// A whole age-keygen-style file: comment lines plus two identity lines.
|
||||
keyFile := "# public key: " + first.Recipient().String() + "\n" +
|
||||
first.String() + "\n" +
|
||||
"# public key: " + second.Recipient().String() + "\n" +
|
||||
second.String() + "\n"
|
||||
|
||||
v := &Vaultik{Config: &config.Config{AgeSecretKey: keyFile}}
|
||||
|
||||
identities, err := v.restoreIdentities()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, identities, 2)
|
||||
|
||||
plaintext := []byte("payload encrypted only to the second identity")
|
||||
blob := encryptBlobTo(t, second.Recipient().String(), plaintext)
|
||||
|
||||
require.Equal(t, plaintext, decryptBlobWith(t, blob, identities))
|
||||
}
|
||||
|
||||
// TestRestoreIdentitiesAcceptsTrailingNewline mirrors a YAML
|
||||
// age_secret_key value that carries a trailing newline: it must still
|
||||
// parse to its one identity and decrypt a blob encrypted to it.
|
||||
func TestRestoreIdentitiesAcceptsTrailingNewline(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := age.GenerateX25519Identity()
|
||||
require.NoError(t, err)
|
||||
|
||||
v := &Vaultik{Config: &config.Config{AgeSecretKey: id.String() + "\n"}}
|
||||
|
||||
identities, err := v.restoreIdentities()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, identities, 1)
|
||||
|
||||
plaintext := []byte("value with a trailing newline")
|
||||
blob := encryptBlobTo(t, id.Recipient().String(), plaintext)
|
||||
|
||||
require.Equal(t, plaintext, decryptBlobWith(t, blob, identities))
|
||||
}
|
||||
|
||||
// TestRestoreIdentitiesMissingKey reports the dedicated missing-key error
|
||||
// rather than a parse failure, so the user is told to set the key.
|
||||
func TestRestoreIdentitiesMissingKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := &Vaultik{Config: &config.Config{}}
|
||||
|
||||
_, err := v.restoreIdentities()
|
||||
require.ErrorIs(t, err, errDecryptionKeyRequired)
|
||||
}
|
||||
Reference in new issue
Block a user