check / check (push) Successful in 1m33s
The module path becomes sneak.berlin/go/keyfunc, as the repo policy sets for Go modules: go.mod, every import and the -X path in the Makefile. The old path gets no alias. The README gives a go install line for the new path, which resolves with @latest only once main carries the move, and its TODO list now names the open 1.0 issues. Model: opus-5-5
170 lines
4.1 KiB
Go
170 lines
4.1 KiB
Go
package agekey_test
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/keyfunc/internal/agekey"
|
|
"sneak.berlin/go/keyfunc/internal/derive"
|
|
)
|
|
|
|
// 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
|
|
}
|