All checks were successful
check / check (push) Successful in 28s
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)
170 lines
4.1 KiB
Go
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
|
|
}
|