The age commands: pub, priv, encrypt and decrypt (closes #3)
All checks were successful
check / check (push) Successful in 3m57s
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
This commit is contained in:
166
internal/agekey/agekey_test.go
Normal file
166
internal/agekey/agekey_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user