check / check (push) Waiting to run
age decrypt took the text form only when the file began exactly with -----BEGIN AGE ENCRYPTED FILE-----, so a blank line in front made it read the file as the binary form and refuse it. It now looks for that line past up to 1024 bytes of whitespace, as the age tool does, and the text form reader skips the blank lines. The README's age decrypt section says so. Judgement call: that first line with spaces in front of it on the same line is still refused, as age refuses it. Model: opus-5-5 Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
189 lines
4.6 KiB
Go
189 lines
4.6 KiB
Go
package agekey_test
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"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 TestTheTextFormIsReadWithWhitespaceInFront(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := forIndex(t, 0)
|
|
|
|
var sealed bytes.Buffer
|
|
|
|
err := key.Encrypt(&sealed, strings.NewReader("hello"), nil, true)
|
|
require.NoError(t, err)
|
|
|
|
// A blank line and a line of two spaces, as pasting can leave. age
|
|
// refuses spaces on the first line of the text form itself, and so
|
|
// does this.
|
|
pasted := io.MultiReader(strings.NewReader("\n \n"), &sealed)
|
|
|
|
var opened bytes.Buffer
|
|
|
|
require.NoError(t, key.Decrypt(&opened, pasted))
|
|
require.Equal(t, "hello", opened.String())
|
|
}
|
|
|
|
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)
|
|
|
|
to, err := agekey.Recipients([]string{theirs.Recipient()})
|
|
require.NoError(t, err)
|
|
|
|
var sealed bytes.Buffer
|
|
|
|
err = mine.Encrypt(&sealed, strings.NewReader("hello"), to, 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 := agekey.Recipients([]string{"not a recipient"})
|
|
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
|
|
}
|