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
109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
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)
|
|
}
|