Files
keyfunc/internal/agekey/agekey_test.go
sneak 73c80ab173
All checks were successful
check / check (push) Successful in 3m57s
The age commands: pub, priv, encrypt and decrypt (closes #3)
The application number is 657169, so the path is
m/83696968'/657169'/<n>'. The 32 derived bytes are clamped the way
X25519 requires and go through bech32 into an age identity, which is
the only route age offers from raw bytes to a key; these are the steps
sneak/secret takes in its agehd package.

The derived recipient is always first in the recipient list, so the
mnemonic that encrypted a file can always read it back. Decrypting
recognises the text form by the line it starts with, so it needs no
flag. A file named with -o is created readable only by its owner,
since a decrypted one is as secret as what went into it.

Model: opus-5
2026-09-07 15:44:08 +00:00

167 lines
3.8 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. They are what makes the derivation reproducible: if these
// change, every file anyone encrypted becomes unreadable.
const (
recipientZero = "age1xwdy9y6ckyfsgjc8k02e9uhsf3fmjy0ufysew" +
"lj68kmx5n67e3nsg2mftq"
recipientOne = "age1pmm92sxaf5mazjwvjph7dx2zq9r5p8l3rarfg" +
"qm7hmakqhvgyy4q5p3w7j"
)
// 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 TestTheIdentityIsWrittenTheWayAgeWritesOne(t *testing.T) {
t.Parallel()
require.True(t,
strings.HasPrefix(forIndex(t, 0).Identity(), "AGE-SECRET-KEY-1"),
)
}
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
}