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) }