All checks were successful
check / check (push) Successful in 3m57s
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
167 lines
3.8 KiB
Go
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
|
|
}
|