Files
keyfunc/internal/agekey/agekey_test.go
clawbot 5bbeec86d6
All checks were successful
check / check (push) Successful in 28s
The age commands: pub, priv, encrypt and decrypt (closes #3)
keyfunc age derives an age identity at the generic path the way secret's agehd does, prints the recipient or the identity, and encrypts to or decrypts with it, the derived recipient always among encrypt's recipients. Two review rounds; the second passed with no findings, the clamping step now pinned by a fixed identity test.

Model: opus-5 (implementation and review); fable-5-1 (landing)
2026-09-07 18:35:11 +02:00

170 lines
4.1 KiB
Go

package agekey_test
import (
"bytes"
"strings"
"testing"
"git.eeqj.de/sneak/keyfunc/internal/agekey"
"git.eeqj.de/sneak/keyfunc/internal/derive"
"github.com/stretchr/testify/require"
)
// The recipients the example mnemonic produces at the first two
// indexes, and the secret key behind the first of them. They are what
// makes the derivation reproducible: if the recipients change, every
// file anyone encrypted becomes unreadable, and if the secret key
// changes, the key is no longer the one other tools derive from the
// same mnemonic.
const (
recipientZero = "age1xwdy9y6ckyfsgjc8k02e9uhsf3fmjy0ufysew" +
"lj68kmx5n67e3nsg2mftq"
recipientOne = "age1pmm92sxaf5mazjwvjph7dx2zq9r5p8l3rarfg" +
"qm7hmakqhvgyy4q5p3w7j"
identityZero = "AGE-SECRET-KEY-19QKK2P38598XLXMQFFU3P7J9PLDD" +
"7527T70JDHGDJ7AMNF3XT44S00JFU5"
)
// example returns the mnemonic every BIP-39 document uses to show its
// test vectors: eleven abandons and about.
func example() string {
return strings.Repeat("abandon ", 11) + "about"
}
func TestTooFewBytesAreRefused(t *testing.T) {
t.Parallel()
_, err := agekey.New([]byte("short"))
require.ErrorIs(t, err, agekey.ErrSize)
}
func TestTheSameMnemonicAlwaysGivesTheSameKey(t *testing.T) {
t.Parallel()
require.Equal(t, recipientZero, forIndex(t, 0).Recipient())
require.Equal(t, recipientOne, forIndex(t, 1).Recipient())
}
func TestTheSameMnemonicAlwaysGivesTheSameSecretKey(t *testing.T) {
t.Parallel()
require.Equal(t, identityZero, forIndex(t, 0).Identity())
}
func TestWhatWasEncryptedComesBack(t *testing.T) {
t.Parallel()
// Every byte value, so nothing assumes the input is text, and
// then text, which is what most of it will be.
payloads := map[string][]byte{
"every byte": everyByte(),
"text": []byte("the quick brown fox\nand a second line\n"),
}
forms := map[string]bool{"binary": false, "armored": true}
for name, payload := range payloads {
for form, armored := range forms {
t.Run(name+" "+form, func(t *testing.T) {
t.Parallel()
key := forIndex(t, 0)
var sealed, opened bytes.Buffer
err := key.Encrypt(
&sealed, bytes.NewReader(payload), nil, armored,
)
require.NoError(t, err)
err = key.Decrypt(&opened, &sealed)
require.NoError(t, err)
require.Equal(t, payload, opened.Bytes())
})
}
}
}
func TestTheArmoredFormIsText(t *testing.T) {
t.Parallel()
var sealed bytes.Buffer
err := forIndex(t, 0).Encrypt(
&sealed, strings.NewReader("hello"), nil, true,
)
require.NoError(t, err)
require.True(t, strings.HasPrefix(
sealed.String(), "-----BEGIN AGE ENCRYPTED FILE-----",
))
}
func TestAFileForSomebodyElseIsRefused(t *testing.T) {
t.Parallel()
var sealed, opened bytes.Buffer
err := forIndex(t, 1).Encrypt(
&sealed, strings.NewReader("hello"), nil, false,
)
require.NoError(t, err)
err = forIndex(t, 0).Decrypt(&opened, &sealed)
require.ErrorIs(t, err, agekey.ErrNotRecipient)
require.Empty(t, opened.Bytes())
}
func TestAnExtraRecipientCanReadItTooAndSoCanTheDerivedOne(t *testing.T) {
t.Parallel()
mine, theirs := forIndex(t, 0), forIndex(t, 1)
var sealed bytes.Buffer
err := mine.Encrypt(
&sealed, strings.NewReader("hello"),
[]string{theirs.Recipient()}, false,
)
require.NoError(t, err)
for _, key := range []*agekey.Key{mine, theirs} {
var opened bytes.Buffer
require.NoError(t, key.Decrypt(&opened, bytes.NewReader(sealed.Bytes())))
require.Equal(t, "hello", opened.String())
}
}
func TestARecipientThatIsNotOneIsRefused(t *testing.T) {
t.Parallel()
err := forIndex(t, 0).Encrypt(
&bytes.Buffer{}, strings.NewReader("hello"),
[]string{"not a recipient"}, false,
)
require.Error(t, err)
}
// everyByte returns a payload holding all 256 byte values.
func everyByte() []byte {
out := make([]byte, 256)
for i := range out {
out[i] = byte(i)
}
return out
}
// forIndex derives the key for one index.
func forIndex(t *testing.T, index uint32) *agekey.Key {
t.Helper()
material, err := derive.Bytes(example(), agekey.Application, index)
require.NoError(t, err)
key, err := agekey.New(material)
require.NoError(t, err)
return key
}