Author keys of alg 1 and Ed25519 signing in own code, as Go's authorkey
authorkey.ts ports the package authorkey of datekeys-go at spec-v0.12:
generate with an injectable random source, fromSeed, publicKey, sign,
clear, secret, a toString that hides the secret, publicString, parsePublic
(canonical, on the curve, not of small order), parseSecret, marshal, and
the key file encrypted with age and scrypt of work factor 16, read with a
maximum of 16, 64 KiB and the lines of bufio.Scanner, with the error texts
of Go and of Go's age byte for byte. Key strings are read as Go strings,
with the case and space tables of Go's package unicode (gounicode.ts,
generated by scripts/go-unicode-tables.go).
ed25519sign.ts is crypto_sign of TweetNaCl, as the Dart port, with the
SHA-512 of @noble/hashes: exact arithmetic in Float64Array, secrets never
in BigInt. No new package or module: age-encryption writes the scrypt
stanza, and the STREAM of a key file uses the ChaCha20-Poly1305 and HKDF
already imported.
scripts/authorkey-go-vectors.go, the generator of the Dart port with this
library's output, writes testing/authorkey-vectors.json in an export of
datekeys-go at spec-v0.12: 234 signatures, scalars, keys, Generate and
Encrypt with Go's draws (reproduced byte for byte), 1288 key strings,
3240 runes at the edges of the tables and 130 key files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
|
|
|
|
//go:build ignore
|
|
|
|
|
|
|
|
|
|
|
|
//go:debug cryptocustomrand=1
|
|
|
|
|
|
|
|
|
|
|
|
// Writes src/lib/dkc/testing/authorkey-vectors.json, the vectors of the
|
|
|
|
|
|
// author keys of authorkey.ts and of the signatures of ed25519sign.ts: the
|
|
|
|
|
|
// Ed25519 signatures of Go's crypto/ed25519 and the package authorkey of
|
|
|
|
|
|
// datekeys-go, with the texts of its errors. It is the generator of the Dart
|
|
|
|
|
|
// port (datekeys-dart, tool/authorkey_go_vectors.go), with the output that
|
|
|
|
|
|
// the tests of this library read:
|
|
|
|
|
|
//
|
|
|
|
|
|
// - sign: crypto/ed25519.Sign over seeds and messages. The first 64 lines
|
|
|
|
|
|
// of sign.input of Go's crypto/ed25519 (SUPERCOP), whose lines 0, 1 and
|
|
|
|
|
|
// 2 are tests 1 to 3 of RFC 8032, 7.1, every 64th line after them, and
|
|
|
|
|
|
// line 1023, whose message of 1023 bytes is the one of its TEST 1024;
|
|
|
|
|
|
// TEST SHA(abc), the message SHA-512("abc") under the key of
|
|
|
|
|
|
// TestSignVerifyHashed of Go; seeds of a fixed seed with messages of 0
|
|
|
|
|
|
// bytes to 1 MiB; and private keys whose second half is another public
|
|
|
|
|
|
// key, which Go hashes as it is given;
|
|
|
|
|
|
// - scalars: x mod ℓ of 64-byte numbers and (a·b + c) mod ℓ of 32-byte
|
|
|
|
|
|
// ones, little-endian, with math/big, in the corners and at random;
|
|
|
|
|
|
// - keys: NewFromSeed, Public, PublicString, Secret, Marshal and String,
|
|
|
|
|
|
// and the errors of NewFromSeed and PublicString;
|
|
|
|
|
|
// - generate and encrypt: Generate and Encrypt while crypto/rand reads a
|
|
|
|
|
|
// ChaCha20 keystream under SHA-256(seed), zero nonce, with each draw in
|
|
|
|
|
|
// hexadecimal, which the tests hand to authorkey.ts in the same order;
|
|
|
|
|
|
// Generate reads it through the GODEBUG cryptocustomrand=1 of this file;
|
|
|
|
|
|
// - public and secret: ParsePublic and ParseSecret over strings, as bytes:
|
|
|
|
|
|
// valid, in the other case or mixed, of other lengths, with each Bech32
|
|
|
|
|
|
// error, other prefixes, data of other lengths and paddings, keys that
|
|
|
|
|
|
// are not canonical, not on the curve or of small order, and bytes that
|
|
|
|
|
|
// are not UTF-8;
|
|
|
|
|
|
// - runes: the same over a valid string where one character is replaced
|
|
|
|
|
|
// by a rune of as many bytes, in the prefix and in the data, for the
|
|
|
|
|
|
// code points at each edge of the sets of unicode.ToLower,
|
|
|
|
|
|
// unicode.ToUpper and unicode.IsSpace of Go: [kind, position, rune,
|
|
|
|
|
|
// text], kind 0 for ParsePublic and 1 for ParseSecret;
|
|
|
|
|
|
// - read: Read of plain and encrypted files, with the result or the text
|
|
|
|
|
|
// of the error.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Every expected value is what Go gives; none is written by hand. A text is
|
|
|
|
|
|
// an index into texts, whose first entry, "", stands for no error. Binary
|
|
|
|
|
|
// values are lower-case hexadecimal. A file is a list of parts, each
|
|
|
|
|
|
// {"hex": …}, {"byte": b, "n": count} or {"sealed": …, "length", "sha256"},
|
|
|
|
|
|
// the age file of a recipe with the draws that age made, which the tests
|
|
|
|
|
|
// write again with age-encryption and those draws. Arrays of numbers are
|
|
|
|
|
|
// written on one line.
|
|
|
|
|
|
//
|
|
|
|
|
|
// It imports only public packages, so it runs in the module of the
|
|
|
|
|
|
// reference implementation, in an export of datekeys-go at the tag
|
|
|
|
|
|
// spec-v0.12 made with git archive, which it does not change. From the root
|
|
|
|
|
|
// of this repository:
|
|
|
|
|
|
//
|
|
|
|
|
|
// tmp=$(mktemp -d)
|
|
|
|
|
|
// git -C ../datekeys-go archive spec-v0.12 | tar -x -C "$tmp"
|
|
|
|
|
|
// cp scripts/authorkey-go-vectors.go "$tmp/"
|
|
|
|
|
|
// src=$(git -C ../datekeys-go rev-parse 'spec-v0.12^{commit}')
|
|
|
|
|
|
// (cd "$tmp" && go run authorkey-go-vectors.go -source "$src" \
|
|
|
|
|
|
// -testdata "$OLDPWD/testdata" \
|
|
|
|
|
|
// -out "$OLDPWD/src/lib/dkc/testing/authorkey-vectors.json")
|
|
|
|
|
|
// rm -rf "$tmp"
|
|
|
|
|
|
//
|
|
|
|
|
|
// The output is the same on every run.
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bufio"
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
|
"compress/gzip"
|
|
|
|
|
|
"crypto/ed25519"
|
|
|
|
|
|
cryptorand "crypto/rand"
|
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
|
"crypto/sha512"
|
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"flag"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"math/big"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"runtime"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"unicode"
|
|
|
|
|
|
"unicode/utf8"
|
|
|
|
|
|
_ "unsafe"
|
|
|
|
|
|
|
|
|
|
|
|
"filippo.io/age"
|
|
|
|
|
|
"golang.org/x/crypto/chacha20"
|
|
|
|
|
|
|
|
|
|
|
|
"g.activething.com/go/DateKeys/authorkey"
|
|
|
|
|
|
_ "g.activething.com/go/DateKeys/codec/bech32"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
//go:linkname createChecksum g.activething.com/go/DateKeys/codec/bech32.createChecksum
|
|
|
|
|
|
func createChecksum(hrp string, data []byte) []byte
|
|
|
|
|
|
|
|
|
|
|
|
type obj = map[string]any
|
|
|
|
|
|
|
|
|
|
|
|
func h(b []byte) string { return hex.EncodeToString(b) }
|
|
|
|
|
|
|
|
|
|
|
|
func check(err error) {
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
_, file, line, _ := runtime.Caller(1)
|
|
|
|
|
|
log.Fatalf("%s:%d: %v", filepath.Base(file), line, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func mustHex(s string) []byte {
|
|
|
|
|
|
b, err := hex.DecodeString(s)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
return b
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func label(s string) []byte {
|
|
|
|
|
|
b := sha256.Sum256([]byte("datekeys-ts authorkey: " + s))
|
|
|
|
|
|
return b[:]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// pattern is a plaintext of n bytes: byte i is (31·i + 7) mod 256.
|
|
|
|
|
|
func pattern(n int) []byte {
|
|
|
|
|
|
b := make([]byte, n)
|
|
|
|
|
|
for i := range b {
|
|
|
|
|
|
b[i] = byte(31*i + 7)
|
|
|
|
|
|
}
|
|
|
|
|
|
return b
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// texts are the error texts, indexed; 0 is no error.
|
|
|
|
|
|
var texts = []string{""}
|
|
|
|
|
|
var textIndex = map[string]int{"": 0}
|
|
|
|
|
|
|
|
|
|
|
|
func t(err error) int {
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
s := err.Error()
|
|
|
|
|
|
if i, ok := textIndex[s]; ok {
|
|
|
|
|
|
return i
|
|
|
|
|
|
}
|
|
|
|
|
|
texts = append(texts, s)
|
|
|
|
|
|
textIndex[s] = len(texts) - 1
|
|
|
|
|
|
return len(texts) - 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// crypto/rand from a seed, as in tool/age_writer_go_vectors.go
|
|
|
|
|
|
|
|
|
|
|
|
type seeded struct {
|
|
|
|
|
|
c *chacha20.Cipher
|
|
|
|
|
|
draws [][]byte
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *seeded) Read(p []byte) (int, error) {
|
|
|
|
|
|
clear(p)
|
|
|
|
|
|
s.c.XORKeyStream(p, p)
|
|
|
|
|
|
s.draws = append(s.draws, bytes.Clone(p))
|
|
|
|
|
|
return len(p), nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func with(seed string, f func()) [][]byte {
|
|
|
|
|
|
key := sha256.Sum256([]byte(seed))
|
|
|
|
|
|
c, err := chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
s := &seeded{c: c}
|
|
|
|
|
|
old := cryptorand.Reader
|
|
|
|
|
|
cryptorand.Reader = s
|
|
|
|
|
|
defer func() { cryptorand.Reader = old }()
|
|
|
|
|
|
f()
|
|
|
|
|
|
return s.draws
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func drawsOf(d [][]byte) []obj {
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
for _, b := range d {
|
|
|
|
|
|
out = append(out, obj{"n": len(b), "hex": h(b)})
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Signatures
|
|
|
|
|
|
|
|
|
|
|
|
func signCase(name string, seed, pub, msg []byte, node bool) obj {
|
|
|
|
|
|
priv := append(bytes.Clone(seed), pub...)
|
|
|
|
|
|
sig := ed25519.Sign(priv, msg)
|
|
|
|
|
|
c := obj{"name": name, "seed": h(seed), "public_key": h(pub), "signature": h(sig)}
|
|
|
|
|
|
if len(msg) > 4096 {
|
|
|
|
|
|
if !bytes.Equal(msg, pattern(len(msg))) {
|
|
|
|
|
|
log.Fatal("a long message must be a pattern")
|
|
|
|
|
|
}
|
|
|
|
|
|
c["message_pattern"] = len(msg)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
c["message"] = h(msg)
|
|
|
|
|
|
}
|
|
|
|
|
|
ownPub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
|
|
|
|
|
|
c["valid"] = ed25519.Verify(ownPub, msg, sig)
|
|
|
|
|
|
return c
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func signSection() []obj {
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
f, err := os.Open(filepath.Join(runtime.GOROOT(), "src", "crypto", "ed25519", "testdata", "sign.input.gz"))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
defer f.Close()
|
|
|
|
|
|
gz, err := gzip.NewReader(f)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
sc := bufio.NewScanner(gz)
|
|
|
|
|
|
sc.Buffer(nil, 1<<20)
|
|
|
|
|
|
for line := 0; sc.Scan(); line++ {
|
|
|
|
|
|
if line >= 64 && line%64 != 0 && line != 1023 {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
parts := strings.Split(sc.Text(), ":")
|
|
|
|
|
|
seed := mustHex(parts[0])[:32]
|
|
|
|
|
|
pub := mustHex(parts[1])
|
|
|
|
|
|
msg := mustHex(parts[2])
|
|
|
|
|
|
sig := mustHex(parts[3])[:64]
|
|
|
|
|
|
if !bytes.Equal(ed25519.Sign(append(bytes.Clone(seed), pub...), msg), sig) {
|
|
|
|
|
|
log.Fatalf("sign.input line %d", line)
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, signCase(fmt.Sprintf("sign.input line %d", line), seed, pub, msg, line < 4 || line%16 == 0))
|
|
|
|
|
|
}
|
|
|
|
|
|
check(sc.Err())
|
|
|
|
|
|
|
|
|
|
|
|
// TEST SHA(abc): the key of TestSignVerifyHashed of Go, the private key
|
|
|
|
|
|
// of RFC 8032, 7.3, which 7.1 signs SHA-512("abc") with.
|
|
|
|
|
|
src, err := os.ReadFile(filepath.Join(runtime.GOROOT(), "src", "crypto", "ed25519", "ed25519_test.go"))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
m := regexp.MustCompile(`func TestSignVerifyHashed[^{]*\{[^"]*key, _ := hex\.DecodeString\("([0-9a-f]{128})"\)`).FindSubmatch(src)
|
|
|
|
|
|
if m == nil {
|
|
|
|
|
|
log.Fatal("no key in TestSignVerifyHashed")
|
|
|
|
|
|
}
|
|
|
|
|
|
key := mustHex(string(m[1]))
|
|
|
|
|
|
abc := sha512.Sum512([]byte("abc"))
|
|
|
|
|
|
out = append(out, signCase("RFC 8032 TEST SHA(abc)", key[:32], key[32:], abc[:], true))
|
|
|
|
|
|
|
|
|
|
|
|
lengths := []int{0, 1, 2, 31, 32, 33, 63, 64, 65, 99, 111, 112, 113, 127, 128, 129, 200, 255, 256, 1000, 4096}
|
|
|
|
|
|
for i := 0; i < 160; i++ {
|
|
|
|
|
|
seed := label(fmt.Sprintf("sign seed %d", i))
|
|
|
|
|
|
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
|
|
|
|
|
|
n := lengths[i%len(lengths)]
|
|
|
|
|
|
msg := label(fmt.Sprintf("sign message %d", i))
|
|
|
|
|
|
for len(msg) < n {
|
|
|
|
|
|
msg = append(msg, label(fmt.Sprintf("sign message %d %d", i, len(msg)))...)
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, signCase(fmt.Sprintf("seeded %d, %d bytes", i, n), seed, pub, msg[:n], i%8 == 0))
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, n := range []int{64 << 10, 1 << 20} {
|
|
|
|
|
|
seed := label(fmt.Sprintf("sign long %d", n))
|
|
|
|
|
|
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
|
|
|
|
|
|
out = append(out, signCase(fmt.Sprintf("a message of %d bytes", n), seed, pub, pattern(n), n < 1<<20))
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, b := range []byte{0, 0xff} {
|
|
|
|
|
|
seed := bytes.Repeat([]byte{b}, 32)
|
|
|
|
|
|
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
|
|
|
|
|
|
out = append(out, signCase(fmt.Sprintf("seed of 0x%02x", b), seed, pub, []byte("DateKeys"), true))
|
|
|
|
|
|
}
|
|
|
|
|
|
// Go hashes the second half of the private key as the public key,
|
|
|
|
|
|
// whatever it is.
|
|
|
|
|
|
for i := 0; i < 4; i++ {
|
|
|
|
|
|
seed := label(fmt.Sprintf("other key seed %d", i))
|
|
|
|
|
|
other := ed25519.NewKeyFromSeed(label(fmt.Sprintf("other key %d", i))).Public().(ed25519.PublicKey)
|
|
|
|
|
|
if i == 3 {
|
|
|
|
|
|
other = make([]byte, 32)
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, signCase(fmt.Sprintf("the public key of another seed, %d", i), seed, other, []byte("message"), true))
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Scalars
|
|
|
|
|
|
|
|
|
|
|
|
var order, _ = new(big.Int).SetString("7237005577332262213973186563042994240857116359379907606001950938285454250989", 10)
|
|
|
|
|
|
|
|
|
|
|
|
func le(x *big.Int, n int) []byte {
|
|
|
|
|
|
b := x.FillBytes(make([]byte, n))
|
|
|
|
|
|
for i, j := 0, n-1; i < j; i, j = i+1, j-1 {
|
|
|
|
|
|
b[i], b[j] = b[j], b[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
return b
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func fromLE(b []byte) *big.Int {
|
|
|
|
|
|
r := bytes.Clone(b)
|
|
|
|
|
|
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
|
|
|
|
|
r[i], r[j] = r[j], r[i]
|
|
|
|
|
|
}
|
|
|
|
|
|
return new(big.Int).SetBytes(r)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func scalarSection() obj {
|
|
|
|
|
|
// ℓ is checked against the order of crypto/ed25519: [ℓ]B is the
|
|
|
|
|
|
// identity, through a signature whose S is ℓ - 1 + 1.
|
|
|
|
|
|
two := big.NewInt(2)
|
|
|
|
|
|
if new(big.Int).Sub(order, new(big.Int).Exp(two, big.NewInt(252), nil)).String() != "27742317777372353535851937790883648493" {
|
|
|
|
|
|
log.Fatal("ℓ")
|
|
|
|
|
|
}
|
|
|
|
|
|
max512 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 512), big.NewInt(1))
|
|
|
|
|
|
top := new(big.Int).Mul(new(big.Int).Div(max512, order), order)
|
|
|
|
|
|
reduceIn := []*big.Int{
|
|
|
|
|
|
big.NewInt(0), big.NewInt(1), new(big.Int).Sub(order, big.NewInt(1)), order,
|
|
|
|
|
|
new(big.Int).Add(order, big.NewInt(1)), new(big.Int).Mul(order, two),
|
|
|
|
|
|
new(big.Int).Lsh(big.NewInt(1), 252), new(big.Int).Lsh(big.NewInt(1), 253),
|
|
|
|
|
|
new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1)),
|
|
|
|
|
|
new(big.Int).Lsh(big.NewInt(1), 511), max512, top, new(big.Int).Sub(top, big.NewInt(1)),
|
|
|
|
|
|
new(big.Int).Add(top, big.NewInt(1)),
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := 0; i < 200; i++ {
|
|
|
|
|
|
x := new(big.Int).SetBytes(append(label(fmt.Sprintf("reduce %d a", i)), label(fmt.Sprintf("reduce %d b", i))...))
|
|
|
|
|
|
if i%4 == 1 {
|
|
|
|
|
|
x.Rsh(x, uint(i%512))
|
|
|
|
|
|
}
|
|
|
|
|
|
if i%4 == 2 {
|
|
|
|
|
|
x.Add(x.Mul(new(big.Int).Rsh(x, 260), order), big.NewInt(int64(i%3)-1))
|
|
|
|
|
|
x.And(x, max512)
|
|
|
|
|
|
}
|
|
|
|
|
|
reduceIn = append(reduceIn, x)
|
|
|
|
|
|
}
|
|
|
|
|
|
reduce := []obj{}
|
|
|
|
|
|
for _, x := range reduceIn {
|
|
|
|
|
|
reduce = append(reduce, obj{"in": h(le(x, 64)), "out": h(le(new(big.Int).Mod(x, order), 32))})
|
|
|
|
|
|
}
|
|
|
|
|
|
max256 := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 256), big.NewInt(1))
|
|
|
|
|
|
corner := []*big.Int{big.NewInt(0), big.NewInt(1), new(big.Int).Sub(order, big.NewInt(1)), order, max256, new(big.Int).Lsh(big.NewInt(1), 255)}
|
|
|
|
|
|
muladd := []obj{}
|
|
|
|
|
|
add := func(a, b, c *big.Int) {
|
|
|
|
|
|
r := new(big.Int).Mul(a, b)
|
|
|
|
|
|
r.Add(r, c).Mod(r, order)
|
|
|
|
|
|
muladd = append(muladd, obj{"a": h(le(a, 32)), "b": h(le(b, 32)), "c": h(le(c, 32)), "out": h(le(r, 32))})
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, a := range corner {
|
|
|
|
|
|
for _, b := range corner {
|
|
|
|
|
|
add(a, b, corner[(len(muladd))%len(corner)])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := 0; i < 100; i++ {
|
|
|
|
|
|
a := new(big.Int).SetBytes(label(fmt.Sprintf("muladd %d a", i)))
|
|
|
|
|
|
b := new(big.Int).SetBytes(label(fmt.Sprintf("muladd %d b", i)))
|
|
|
|
|
|
c := new(big.Int).SetBytes(label(fmt.Sprintf("muladd %d c", i)))
|
|
|
|
|
|
add(a, b, c)
|
|
|
|
|
|
}
|
|
|
|
|
|
return obj{"reduce": reduce, "muladd": muladd, "order": h(le(order, 32))}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Keys
|
|
|
|
|
|
|
|
|
|
|
|
func keySection() obj {
|
|
|
|
|
|
keys := []obj{}
|
|
|
|
|
|
for i := 0; i < 24; i++ {
|
|
|
|
|
|
seed := label(fmt.Sprintf("key %d", i))
|
|
|
|
|
|
if i == 0 {
|
|
|
|
|
|
seed = make([]byte, 32)
|
|
|
|
|
|
}
|
|
|
|
|
|
k, err := authorkey.NewFromSeed(seed)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
ps, err := authorkey.PublicString(k.Public())
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
keys = append(keys, obj{"seed": h(seed), "public_key": h(k.Public()), "public": ps, "secret": k.Secret(), "marshal": string(authorkey.Marshal(k)), "string": k.String(), "gostring": fmt.Sprintf("%#v", k)})
|
|
|
|
|
|
}
|
|
|
|
|
|
seedErrors := []obj{}
|
|
|
|
|
|
for _, n := range []int{0, 31, 33, 64} {
|
|
|
|
|
|
_, err := authorkey.NewFromSeed(make([]byte, n))
|
|
|
|
|
|
seedErrors = append(seedErrors, obj{"length": n, "error": err.Error()})
|
|
|
|
|
|
}
|
|
|
|
|
|
publicErrors := []obj{}
|
|
|
|
|
|
for _, n := range []int{0, 31, 33, 64} {
|
|
|
|
|
|
_, err := authorkey.PublicString(make([]byte, n))
|
|
|
|
|
|
publicErrors = append(publicErrors, obj{"length": n, "error": err.Error()})
|
|
|
|
|
|
}
|
|
|
|
|
|
return obj{"keys": keys, "seed_errors": seedErrors, "public_errors": publicErrors}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func generateSection() []obj {
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
for i := 0; i < 3; i++ {
|
|
|
|
|
|
seed := fmt.Sprintf("authorkey generate %d", i)
|
|
|
|
|
|
var k *authorkey.Key
|
|
|
|
|
|
d := with(seed, func() {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
k, err = authorkey.Generate()
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
})
|
|
|
|
|
|
out = append(out, obj{"seed": seed, "draws": drawsOf(d), "secret": k.Secret(), "public_key": h(k.Public())})
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func encryptSection() []obj {
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
for i, pass := range []string{"correct horse battery staple", "contraseña ñ €", "x"} {
|
|
|
|
|
|
k, err := authorkey.NewFromSeed(label(fmt.Sprintf("encrypt key %d", i)))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
seed := fmt.Sprintf("authorkey encrypt %d", i)
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
d := with(seed, func() { check(authorkey.Encrypt(&buf, k, pass)) })
|
|
|
|
|
|
back, err := authorkey.Read(bytes.NewReader(buf.Bytes()), pass)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
if back.Secret() != k.Secret() {
|
|
|
|
|
|
log.Fatal("Read does not give the key back")
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, obj{"seed": seed, "key_seed": h(label(fmt.Sprintf("encrypt key %d", i))), "passphrase": pass, "draws": drawsOf(d), "file": h(buf.Bytes())})
|
|
|
|
|
|
}
|
|
|
|
|
|
k, err := authorkey.NewFromSeed(label("encrypt key 0"))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
err = authorkey.Encrypt(&bytes.Buffer{}, k, "")
|
|
|
|
|
|
out = append(out, obj{"passphrase": "", "error": err.Error()})
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Strings
|
|
|
|
|
|
|
|
|
|
|
|
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
|
|
|
|
|
|
|
|
|
|
|
// encode5 writes hrp and the 5-bit values with a valid checksum, in lower
|
|
|
|
|
|
// case: a Bech32 string whose data part need not be 8-bit data.
|
|
|
|
|
|
func encode5(hrp string, values []byte) string {
|
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
|
b.WriteString(hrp)
|
|
|
|
|
|
b.WriteString("1")
|
|
|
|
|
|
for _, v := range values {
|
|
|
|
|
|
b.WriteByte(charset[v])
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, v := range createChecksum(hrp, values) {
|
|
|
|
|
|
b.WriteByte(charset[v])
|
|
|
|
|
|
}
|
|
|
|
|
|
return b.String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func to5(data []byte) []byte {
|
|
|
|
|
|
var out []byte
|
|
|
|
|
|
acc, bits := 0, 0
|
|
|
|
|
|
for _, v := range data {
|
|
|
|
|
|
acc = acc<<8 | int(v)
|
|
|
|
|
|
bits += 8
|
|
|
|
|
|
for bits >= 5 {
|
|
|
|
|
|
bits -= 5
|
|
|
|
|
|
out = append(out, byte(acc>>bits)&31)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if bits > 0 {
|
|
|
|
|
|
out = append(out, byte(acc<<(5-bits))&31)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func enc(hrp string, data []byte) string {
|
|
|
|
|
|
v := to5(data)
|
|
|
|
|
|
s := encode5(strings.ToLower(hrp), v)
|
|
|
|
|
|
if strings.ToUpper(hrp) == hrp {
|
|
|
|
|
|
return strings.ToUpper(s)
|
|
|
|
|
|
}
|
|
|
|
|
|
return s
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// variants are the strings of a valid key string s, of the prefix hrp and
|
|
|
|
|
|
// the data data: other cases, lengths, characters, prefixes, paddings.
|
|
|
|
|
|
func variants(s, hrp string, data []byte, full bool) []string {
|
|
|
|
|
|
out := []string{s, strings.ToUpper(s), strings.ToLower(s), s[:1] + strings.ToLower(s[1:]), s[:1] + strings.ToUpper(s[1:]),
|
|
|
|
|
|
s[:len(s)-1] + strings.ToUpper(s[len(s)-1:]), s[:len(s)-1] + strings.ToLower(s[len(s)-1:]),
|
|
|
|
|
|
"", s[:1], s[:len(s)-1], s + "q", s + s, " " + s[1:], s[:len(s)-1] + " ", s[:len(s)-1] + "\n"}
|
|
|
|
|
|
// Each position changed to another character of the charset, to one
|
|
|
|
|
|
// out of it and to the separator.
|
|
|
|
|
|
for i := 0; full && i < len(s); i++ {
|
|
|
|
|
|
for _, c := range []byte{'q', 'p', 'b', 'i', 'o', '1', '0', 'Z', ' ', 0, 0x7f, '"', '\\'} {
|
|
|
|
|
|
if s[i] == c {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if c != 'q' && c != 'p' && i%5 != 0 && c != 'b' {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, s[:i]+string([]byte{c})+s[i+1:])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lower := strings.ToLower(hrp) == hrp
|
|
|
|
|
|
casing := func(x string) string {
|
|
|
|
|
|
if lower {
|
|
|
|
|
|
return strings.ToLower(x)
|
|
|
|
|
|
}
|
|
|
|
|
|
return strings.ToUpper(x)
|
|
|
|
|
|
}
|
|
|
|
|
|
n := len(hrp)
|
|
|
|
|
|
// Other prefixes of the same length and of a length one less or more,
|
|
|
|
|
|
// with data of the length that keeps the string length.
|
|
|
|
|
|
out = append(out, enc(casing(hrp[:n-1]+"q"), data))
|
|
|
|
|
|
out = append(out, enc(casing(hrp[:n-1]+"Q"), data))
|
|
|
|
|
|
out = append(out, enc(casing("x"+hrp[1:]), data))
|
|
|
|
|
|
v := to5(data)
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp[:n-1]), append(bytes.Clone(v), 0))))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp[:n-1]), append(bytes.Clone(v), 1))))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp+"x"), v[:len(v)-1])))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp), append(bytes.Clone(v[:len(v)-1]), v[len(v)-1]|1))))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp), append(bytes.Clone(v[:len(v)-1]), 31))))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp+"x"), v[:len(v)-2])))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp[:n-2]), append(bytes.Clone(v), 0, 0))))
|
|
|
|
|
|
out = append(out, casing(encode5(strings.ToLower(hrp[:n-1]+"1"), v[:len(v)-1])))
|
|
|
|
|
|
out = append(out, casing(encode5("", append(bytes.Clone(v), bytes.Repeat([]byte{0}, n+1)...))))
|
|
|
|
|
|
// A byte that is not ASCII and a separator 6, 7 or 8 bytes before the
|
|
|
|
|
|
// end: the position of the separator is checked first.
|
|
|
|
|
|
for _, bad := range []string{"\xff", "é"} {
|
|
|
|
|
|
for _, back := range []int{6, 7, 8} {
|
|
|
|
|
|
b := []byte(s[:3] + bad + s[3+len(bad):])
|
|
|
|
|
|
b[len(b)-back] = '1'
|
|
|
|
|
|
out = append(out, string(b))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Bytes that are not ASCII or not UTF-8, in place of as many bytes.
|
|
|
|
|
|
for _, bad := range []string{"\xff", "\x80", "\xc0\x80", "\xe0\x80\x80", "\xed\xa0\x80", "\xf4\x90\x80\x80", "\xc3", "é", "€", "İ", "ß", "Dž", " ", "<22>", "\U0001f600"} {
|
|
|
|
|
|
for _, at := range []int{0, 3, n, n + 1, len(s) - len(bad)} {
|
|
|
|
|
|
if at+len(bad) <= len(s) {
|
|
|
|
|
|
out = append(out, s[:at]+bad+s[at+len(bad):])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func keyStrings() ([]string, []string) {
|
|
|
|
|
|
var pub, sec []string
|
|
|
|
|
|
for i := 0; i < 6; i++ {
|
|
|
|
|
|
k, err := authorkey.NewFromSeed(label(fmt.Sprintf("strings %d", i)))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
ps, err := authorkey.PublicString(k.Public())
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
if i < 2 {
|
|
|
|
|
|
pub = append(pub, variants(ps, authorkey.PublicPrefix, k.Public(), i == 0)...)
|
|
|
|
|
|
seed := label(fmt.Sprintf("strings %d", i))
|
|
|
|
|
|
sec = append(sec, variants(k.Secret(), authorkey.SecretPrefix, seed, i == 0)...)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
pub = append(pub, ps)
|
|
|
|
|
|
sec = append(sec, k.Secret())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Keys that the strict profile rejects: the public keys of
|
|
|
|
|
|
// ed25519_strict.json, encodings that are not canonical, and random
|
|
|
|
|
|
// encodings, about half of them off the curve.
|
|
|
|
|
|
var raws [][]byte
|
|
|
|
|
|
var strict struct {
|
|
|
|
|
|
Vectors []struct {
|
|
|
|
|
|
PublicKey string `json:"public_key"`
|
|
|
|
|
|
} `json:"vectors"`
|
|
|
|
|
|
}
|
|
|
|
|
|
check(json.Unmarshal(mustRead(filepath.Join(*testdata, "vectors", "ed25519_strict.json")), &strict))
|
|
|
|
|
|
for _, v := range strict.Vectors {
|
|
|
|
|
|
raws = append(raws, mustHex(v.PublicKey))
|
|
|
|
|
|
}
|
|
|
|
|
|
p := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
|
|
|
|
|
|
for d := int64(-1); d <= 19; d++ {
|
|
|
|
|
|
y := le(new(big.Int).Add(p, big.NewInt(d)), 32)
|
|
|
|
|
|
raws = append(raws, bytes.Clone(y))
|
|
|
|
|
|
y[31] |= 0x80
|
|
|
|
|
|
raws = append(raws, y)
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, y := range []*big.Int{big.NewInt(0), big.NewInt(1), new(big.Int).Sub(p, big.NewInt(1))} {
|
|
|
|
|
|
b := le(y, 32)
|
|
|
|
|
|
raws = append(raws, bytes.Clone(b))
|
|
|
|
|
|
b[31] |= 0x80
|
|
|
|
|
|
raws = append(raws, b)
|
|
|
|
|
|
}
|
|
|
|
|
|
for i := 0; i < 48; i++ {
|
|
|
|
|
|
raws = append(raws, label(fmt.Sprintf("random key %d", i)))
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, r := range raws {
|
|
|
|
|
|
s, err := authorkey.PublicString(r)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
pub = append(pub, s)
|
|
|
|
|
|
}
|
|
|
|
|
|
return pub, sec
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func mustRead(path string) []byte {
|
|
|
|
|
|
b, err := os.ReadFile(path)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
return b
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func publicSection(in []string) []obj {
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
for _, s := range in {
|
|
|
|
|
|
k, err := authorkey.ParsePublic(s)
|
|
|
|
|
|
c := obj{"in": h([]byte(s)), "text": t(err)}
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
c["public_key"] = h(k)
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, c)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func secretSection(in []string) []obj {
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
for _, s := range in {
|
|
|
|
|
|
k, err := authorkey.ParseSecret(s)
|
|
|
|
|
|
c := obj{"in": h([]byte(s)), "text": t(err)}
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
c["public_key"] = h(k.Public())
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, c)
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// edges returns the code points at each edge of a set: the last one out
|
|
|
|
|
|
// and the first one in, the last one in and the first one out.
|
|
|
|
|
|
func edges(in func(rune) bool, add func(rune)) {
|
|
|
|
|
|
prev := in(0)
|
|
|
|
|
|
for r := rune(1); r <= unicode.MaxRune; r++ {
|
|
|
|
|
|
if r >= 0xd800 && r <= 0xdfff {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
cur := in(r)
|
|
|
|
|
|
if cur != prev {
|
|
|
|
|
|
add(r - 1)
|
|
|
|
|
|
add(r)
|
|
|
|
|
|
}
|
|
|
|
|
|
prev = cur
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func runeSection(n int) obj {
|
|
|
|
|
|
seen := map[rune]bool{}
|
|
|
|
|
|
var runes []rune
|
|
|
|
|
|
add := func(r rune) {
|
|
|
|
|
|
if r < 0x80 || (r >= 0xd800 && r <= 0xdfff) || seen[r] {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
seen[r] = true
|
|
|
|
|
|
runes = append(runes, r)
|
|
|
|
|
|
}
|
|
|
|
|
|
edges(func(r rune) bool { return unicode.ToLower(r) != r }, add)
|
|
|
|
|
|
edges(func(r rune) bool { return unicode.ToUpper(r) != r }, add)
|
|
|
|
|
|
edges(unicode.IsSpace, add)
|
|
|
|
|
|
add(utf8.MaxRune)
|
|
|
|
|
|
add(0xfffd)
|
|
|
|
|
|
k, err := authorkey.NewFromSeed(label("runes"))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
ps, err := authorkey.PublicString(k.Public())
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
sec := k.Secret()
|
|
|
|
|
|
cases := [][]any{}
|
|
|
|
|
|
for i, r := range runes {
|
|
|
|
|
|
e := string(r)
|
|
|
|
|
|
for kind, s := range []string{ps, sec} {
|
|
|
|
|
|
for _, at := range []int{[]int{3, len(s) - 9}[i%2]} {
|
|
|
|
|
|
in := s[:at] + e + s[at+len(e):]
|
|
|
|
|
|
var err error
|
|
|
|
|
|
if kind == 0 {
|
|
|
|
|
|
_, err = authorkey.ParsePublic(in)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_, err = authorkey.ParseSecret(in)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
log.Fatalf("U+%04X passes", r)
|
|
|
|
|
|
}
|
|
|
|
|
|
cases = append(cases, []any{kind, at, r, t(err)})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if n > 1 {
|
|
|
|
|
|
var some [][]any
|
|
|
|
|
|
for i := 0; i < len(cases); i += n * 2 {
|
|
|
|
|
|
some = append(some, cases[i:i+2]...)
|
|
|
|
|
|
}
|
|
|
|
|
|
cases = some
|
|
|
|
|
|
}
|
|
|
|
|
|
return obj{"public": ps, "secret": sec, "cases": cases}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Files
|
|
|
|
|
|
|
|
|
|
|
|
type part = obj
|
|
|
|
|
|
|
|
|
|
|
|
func sum(b []byte) string {
|
|
|
|
|
|
s := sha256.Sum256(b)
|
|
|
|
|
|
return h(s[:])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sealedPart is the file of sealed(seed, pass, wf, plain), written as its
|
|
|
|
|
|
// recipe, its length and its SHA-256: the tests write it again with
|
|
|
|
|
|
// SeededRandomSource, as age writes it here.
|
|
|
|
|
|
func sealedPart(seed, pass string, wf int, plain []part) part {
|
|
|
|
|
|
f, d := sealedDraws(seed, pass, wf, join(plain))
|
|
|
|
|
|
return part{"sealed": obj{"seed": seed, "passphrase": pass, "work_factor": wf, "plain": plain, "draws": drawsOf(d)}, "length": len(f), "sha256": sum(f)}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func hx(b []byte) part { return part{"hex": h(b)} }
|
|
|
|
|
|
func rep(b byte, n int) part { return part{"byte": int(b), "n": n} }
|
|
|
|
|
|
|
|
|
|
|
|
func join(parts []part) []byte {
|
|
|
|
|
|
var out []byte
|
|
|
|
|
|
for _, p := range parts {
|
|
|
|
|
|
if x, ok := p["hex"]; ok {
|
|
|
|
|
|
out = append(out, mustHex(x.(string))...)
|
|
|
|
|
|
} else if s, ok := p["sealed"]; ok {
|
|
|
|
|
|
r := s.(obj)
|
|
|
|
|
|
f := sealed(r["seed"].(string), r["passphrase"].(string), r["work_factor"].(int), join(r["plain"].([]part)))
|
|
|
|
|
|
if len(f) != p["length"].(int) || sum(f) != p["sha256"].(string) {
|
|
|
|
|
|
log.Fatal("a sealed part")
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, f...)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
out = append(out, bytes.Repeat([]byte{byte(p["byte"].(int))}, p["n"].(int))...)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func readCase(name string, parts []part, pass string, node bool) obj {
|
|
|
|
|
|
k, err := authorkey.Read(bytes.NewReader(join(parts)), pass)
|
|
|
|
|
|
c := obj{"name": name, "file": parts, "passphrase": pass, "text": t(err)}
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
c["public_key"] = h(k.Public())
|
|
|
|
|
|
c["secret"] = k.Secret()
|
|
|
|
|
|
}
|
|
|
|
|
|
return c
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sealed encrypts plain with a scrypt recipient of work factor wf while
|
|
|
|
|
|
// crypto/rand reads the keystream of seed.
|
|
|
|
|
|
func sealed(seed, pass string, wf int, plain []byte) []byte {
|
|
|
|
|
|
f, _ := sealedDraws(seed, pass, wf, plain)
|
|
|
|
|
|
return f
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sealedDraws is sealed with the draws of crypto/rand.
|
|
|
|
|
|
func sealedDraws(seed, pass string, wf int, plain []byte) ([]byte, [][]byte) {
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
d := with(seed, func() {
|
|
|
|
|
|
r, err := age.NewScryptRecipient(pass)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
r.SetWorkFactor(wf)
|
|
|
|
|
|
w, err := age.Encrypt(&buf, r)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
_, err = w.Write(plain)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
check(w.Close())
|
|
|
|
|
|
})
|
|
|
|
|
|
return buf.Bytes(), d
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func readSection() []obj {
|
|
|
|
|
|
k1, err := authorkey.NewFromSeed(label("read 1"))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
k2, err := authorkey.NewFromSeed(label("read 2"))
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
s1, s2 := k1.Secret(), k2.Secret()
|
|
|
|
|
|
p1, err := authorkey.PublicString(k1.Public())
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
out := []obj{}
|
|
|
|
|
|
plain := func(name, s string) {
|
|
|
|
|
|
out = append(out, readCase(name, []part{hx([]byte(s))}, "", true))
|
|
|
|
|
|
}
|
|
|
|
|
|
plain("Marshal", string(authorkey.Marshal(k1)))
|
|
|
|
|
|
plain("the line alone", s1)
|
|
|
|
|
|
plain("the line and LF", s1+"\n")
|
|
|
|
|
|
plain("CR LF", "# c\r\n"+s1+"\r\n\r\n")
|
|
|
|
|
|
plain("CR alone", s1+"\r")
|
|
|
|
|
|
plain("CR inside", s1[:10]+"\r"+s1[10:])
|
|
|
|
|
|
plain("spaces and tabs", " \t "+s1+" \t\v\f\r\n")
|
|
|
|
|
|
plain("comments and empty lines", "\n\n# one\n # two\n\n"+s1+"\n# three\n\n")
|
|
|
|
|
|
plain("a comment without the space", "#"+s1+"\n"+s1+"\n")
|
|
|
|
|
|
plain("a comment that is not UTF-8", "# \xff\xfe\n"+s1)
|
|
|
|
|
|
plain("two keys", s1+"\n"+s2+"\n")
|
|
|
|
|
|
plain("the same key twice", s1+"\n"+s1+"\n")
|
|
|
|
|
|
plain("a key and something else", s1+"\nsomething\n")
|
|
|
|
|
|
plain("something else and a key", "something\n"+s1+"\n")
|
|
|
|
|
|
plain("a key in lower case", strings.ToLower(s1))
|
|
|
|
|
|
plain("a public key", p1)
|
|
|
|
|
|
plain("an age identity", "AGE-SECRET-KEY-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ")
|
|
|
|
|
|
plain("empty", "")
|
|
|
|
|
|
plain("LF", "\n")
|
|
|
|
|
|
plain("only comments", "# a\n# b\n")
|
|
|
|
|
|
plain("only spaces", " \n\t\n\v\f\n")
|
|
|
|
|
|
plain("NUL before the key", "\x00"+s1)
|
|
|
|
|
|
plain("a BOM before the key", bom+s1)
|
|
|
|
|
|
plain("a byte that is not UTF-8 before the key", "\xff"+s1)
|
|
|
|
|
|
plain("NEL alone, not UTF-8", "\x85"+s1)
|
|
|
|
|
|
plain("NEL in UTF-8", "\u0085"+s1+"\u0085")
|
|
|
|
|
|
// The ends of a line that are not quite a space: utf8.DecodeLastRune
|
|
|
|
|
|
// and DecodeRune give U+FFFD for them, which TrimSpace keeps.
|
|
|
|
|
|
plain("a space and a stray continuation byte at the end", s1+ideographicSpace+"\x80")
|
|
|
|
|
|
plain("a stray continuation byte and a space at the start", "\x80"+ideographicSpace+s1)
|
|
|
|
|
|
plain("a space cut at the end", s1+ideographicSpace[:2])
|
|
|
|
|
|
plain("a space cut at the start", ideographicSpace[1:]+s1)
|
|
|
|
|
|
plain("four continuation bytes after a space", s1+ideographicSpace+"\x80\x80\x80\x80")
|
|
|
|
|
|
plain("a space after the key and a stray byte", s1+" \x80")
|
|
|
|
|
|
plain("a key cut", s1[:78])
|
|
|
|
|
|
plain("a key and a byte", s1+"x")
|
|
|
|
|
|
plain("age-encryption.org/v1 without LF", "age-encryption.org/v1")
|
|
|
|
|
|
plain("age-encryption.org/v1 and a key", "age-encryption.org/v1 \n"+s1)
|
|
|
|
|
|
plain("a stray continuation byte alone on a line", "\x80\n"+s1)
|
|
|
|
|
|
plain("two stray continuation bytes before a key", "\x80\x80"+s1)
|
|
|
|
|
|
// Every space of Go, and its neighbours, around the line.
|
|
|
|
|
|
seen := map[rune]bool{}
|
|
|
|
|
|
for r := rune(0); r <= 0x3001; r++ {
|
|
|
|
|
|
if !unicode.IsSpace(r) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, x := range []rune{r - 1, r, r + 1} {
|
|
|
|
|
|
if seen[x] || x == '\n' || (x >= 0x21 && x < 0x7f && x != r) {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
seen[x] = true
|
|
|
|
|
|
e := string(x)
|
|
|
|
|
|
out = append(out, readCase(fmt.Sprintf("U+%04X around the line", x), []part{hx([]byte(e + e + s1 + e + "\n"))}, "", true))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// The limits: 64 KiB, the bufio.Scanner and its token of 64 KiB.
|
|
|
|
|
|
out = append(out, readCase("64 KiB of comment without LF", []part{hx([]byte("#")), rep('x', 65535)}, "", true))
|
|
|
|
|
|
out = append(out, readCase("64 KiB of comment with LF", []part{hx([]byte("#")), rep('x', 65534), hx([]byte("\n"))}, "", true))
|
|
|
|
|
|
out = append(out, readCase("a key, then a comment, 64 KiB in all", []part{hx([]byte(s1 + "\n#")), rep('x', 65536-82), hx([]byte("\n"))}, "", true))
|
|
|
|
|
|
out = append(out, readCase("64 KiB of spaces without LF", []part{rep(' ', 65536)}, "", true))
|
|
|
|
|
|
out = append(out, readCase("64 KiB and a byte", []part{hx([]byte(s1 + "\n#")), rep('x', 65536-80)}, "", true))
|
|
|
|
|
|
out = append(out, readCase("128 KiB", []part{rep('#', 128<<10)}, "", true))
|
|
|
|
|
|
out = append(out, readCase("64 KiB and a byte, encrypted", []part{hx([]byte("age-encryption.org/v1\n")), rep('x', 65536-21)}, "p", true))
|
|
|
|
|
|
|
|
|
|
|
|
// Encrypted files, with work factors of 1 and 2, cheap for the tests.
|
|
|
|
|
|
enc := func(name, seed, pass string, wf int, plain []byte, read string, node bool) {
|
|
|
|
|
|
out = append(out, readCase(name, []part{hx(sealed(seed, pass, wf, plain))}, read, node))
|
|
|
|
|
|
}
|
|
|
|
|
|
m1 := authorkey.Marshal(k1)
|
|
|
|
|
|
enc("encrypted, work factor 1", "read enc 1", "p", 1, m1, "p", true)
|
|
|
|
|
|
enc("encrypted, work factor 2, UTF-8 passphrase", "read enc 2", "pässwörd €", 2, m1, "pässwörd €", true)
|
|
|
|
|
|
enc("encrypted, wrong passphrase", "read enc 3", "p", 1, m1, "q", true)
|
|
|
|
|
|
enc("encrypted, no passphrase", "read enc 4", "p", 1, m1, "", true)
|
|
|
|
|
|
enc("encrypted, two keys", "read enc 5", "p", 1, []byte(s1+"\n"+s2+"\n"), "p", true)
|
|
|
|
|
|
enc("encrypted, no key", "read enc 6", "p", 1, []byte("# nothing\n"), "p", true)
|
|
|
|
|
|
enc("encrypted, empty", "read enc 7", "p", 1, nil, "p", true)
|
|
|
|
|
|
enc("encrypted, a key in lower case", "read enc 8", "p", 1, []byte(strings.ToLower(s1)), "p", true)
|
|
|
|
|
|
enc("encrypted, spaces around", "read enc 9", "p", 1, []byte(ideographicSpace+s1+paragraphSeparator+"\r"+lf), "p", true)
|
|
|
|
|
|
enc("encrypted, work factor 17", "read enc 10", "p", 17, m1, "p", false)
|
|
|
|
|
|
// Other work factors, edited into a file of work factor 1: age reads
|
|
|
|
|
|
// the work factor before it runs scrypt, and its MAC after.
|
|
|
|
|
|
w1 := sealed("read enc 11", "p", 1, m1)
|
|
|
|
|
|
for _, wf := range []string{"22", "0", "01", "31", "-1", "1 ", "16"} {
|
|
|
|
|
|
e := bytes.Replace(w1, []byte(" 1"+lf), []byte(" "+wf+lf), 1)
|
|
|
|
|
|
out = append(out, readCase("encrypted, work factor edited to "+wf, []part{hx(e)}, "p", wf != "16"))
|
|
|
|
|
|
}
|
|
|
|
|
|
large := sealedPart("read enc 12", "p", 1, []part{hx([]byte(s1 + "\n#")), rep('x', 65000), hx([]byte("\n"))})
|
|
|
|
|
|
out = append(out, readCase("encrypted, 64 KiB of plaintext", []part{large}, "p", true))
|
|
|
|
|
|
tooBig := sealedPart("read enc 13", "p", 1, []part{hx([]byte(s1 + "\n#")), rep('x', 65536)})
|
|
|
|
|
|
out = append(out, readCase("encrypted, more than 64 KiB", []part{tooBig}, "p", true))
|
|
|
|
|
|
f := sealed("read enc 14", "p", 1, m1)
|
|
|
|
|
|
out = append(out, readCase("encrypted, cut", []part{hx(f[:len(f)-1])}, "p", true))
|
|
|
|
|
|
out = append(out, readCase("encrypted, header only", []part{hx(f[:bytes.Index(f, []byte("\n--- "))+1])}, "p", true))
|
|
|
|
|
|
g := bytes.Clone(f)
|
|
|
|
|
|
g[len(g)-1] ^= 1
|
|
|
|
|
|
out = append(out, readCase("encrypted, last byte changed", []part{hx(g)}, "p", true))
|
|
|
|
|
|
g = bytes.Clone(f)
|
|
|
|
|
|
i := bytes.Index(g, []byte("\n--- ")) + 6
|
|
|
|
|
|
g[i] ^= 1
|
|
|
|
|
|
out = append(out, readCase("encrypted, MAC changed", []part{hx(g)}, "p", true))
|
|
|
|
|
|
out = append(out, readCase("encrypted, garbage", []part{hx([]byte("age-encryption.org/v1\n-> what\n"))}, "p", true))
|
|
|
|
|
|
// The stanza of a file of work factor 1 edited, which age reads before
|
|
|
|
|
|
// scrypt, or the bytes after its header, which it reads after the MAC.
|
|
|
|
|
|
hdrEnd := bytes.Index(f, []byte("\n--- ")) + 1
|
|
|
|
|
|
hdrEnd += bytes.IndexByte(f[hdrEnd:], '\n') + 1
|
|
|
|
|
|
lines := strings.SplitN(string(f[:hdrEnd]), "\n", 4) // intro, stanza, body, MAC and the rest
|
|
|
|
|
|
stanza, body := lines[1], lines[2]
|
|
|
|
|
|
args := strings.Fields(stanza) // "->", "scrypt", salt, work factor
|
|
|
|
|
|
edited := func(name, st, bd string, after []byte) {
|
|
|
|
|
|
e := []byte(lines[0] + "\n" + st + "\n" + bd + "\n" + lines[3])
|
|
|
|
|
|
out = append(out, readCase("encrypted, "+name, []part{hx(append(e, after...))}, "p", true))
|
|
|
|
|
|
}
|
|
|
|
|
|
rest := f[hdrEnd:]
|
|
|
|
|
|
edited("an X25519 stanza besides scrypt", stanza, body+"\n-> X25519 "+args[2]+"\n"+body, rest)
|
|
|
|
|
|
edited("a second scrypt stanza", stanza, body+"\n"+stanza+"\n"+body, rest)
|
|
|
|
|
|
edited("scrypt with one argument", "-> scrypt "+args[2], body, rest)
|
|
|
|
|
|
edited("scrypt with three arguments", stanza+" 1", body, rest)
|
|
|
|
|
|
edited("a salt that is not Base64", "-> scrypt !"+args[2][1:]+" 1", body, rest)
|
|
|
|
|
|
edited("a salt with bits after its end", "-> scrypt "+args[2][:21]+"B 1", body, rest)
|
|
|
|
|
|
edited("a salt of 15 bytes", "-> scrypt "+b64.EncodeToString(make([]byte, 15))+" 1", body, rest)
|
|
|
|
|
|
edited("a salt of 17 bytes", "-> scrypt "+b64.EncodeToString(make([]byte, 17))+" 1", body, rest)
|
|
|
|
|
|
edited("a work factor beyond 64 bits", "-> scrypt "+args[2]+" 99999999999999999999", body, rest)
|
|
|
|
|
|
edited("a work factor of 2^63", "-> scrypt "+args[2]+" 9223372036854775808", body, rest)
|
|
|
|
|
|
edited("a work factor of 2^63 - 1", "-> scrypt "+args[2]+" 9223372036854775807", body, rest)
|
|
|
|
|
|
raw, err := b64.DecodeString(body)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
edited("a body of 31 bytes", stanza, b64.EncodeToString(raw[:31]), rest)
|
|
|
|
|
|
edited("a body of 33 bytes", stanza, b64.EncodeToString(append(bytes.Clone(raw), 0)), rest)
|
|
|
|
|
|
out = append(out, readCase("encrypted, the header alone", []part{hx(f[:hdrEnd])}, "p", true))
|
|
|
|
|
|
out = append(out, readCase("encrypted, five bytes of the nonce", []part{hx(f[:hdrEnd+5])}, "p", true))
|
|
|
|
|
|
out = append(out, readCase("encrypted, the header and the nonce", []part{hx(f[:hdrEnd+16])}, "p", true))
|
|
|
|
|
|
out = append(out, readCase("encrypted, the header, the nonce and a byte", []part{hx(f[:hdrEnd+17])}, "p", true))
|
|
|
|
|
|
// An X25519 recipient instead of scrypt.
|
|
|
|
|
|
var xbuf bytes.Buffer
|
|
|
|
|
|
with("read enc x25519", func() {
|
|
|
|
|
|
id, err := age.GenerateX25519Identity()
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
w, err := age.Encrypt(&xbuf, id.Recipient())
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
_, err = w.Write(m1)
|
|
|
|
|
|
check(err)
|
|
|
|
|
|
check(w.Close())
|
|
|
|
|
|
})
|
|
|
|
|
|
out = append(out, readCase("encrypted for X25519", []part{hx(xbuf.Bytes())}, "p", true))
|
|
|
|
|
|
return out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// b64 is the Base64 of the stanzas of age: standard, without padding.
|
|
|
|
|
|
var b64 = base64.RawStdEncoding.Strict()
|
|
|
|
|
|
|
|
|
|
|
|
var testdata = flag.String("testdata", "", "the testdata of this repository")
|
|
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
out := flag.String("out", "", "the JSON file to write")
|
|
|
|
|
|
src := flag.String("source", "", "the commit of datekeys-go")
|
|
|
|
|
|
flag.Parse()
|
|
|
|
|
|
if *out == "" || *src == "" || *testdata == "" {
|
|
|
|
|
|
log.Fatal("usage: -source <commit> -testdata <dir> -out <file>")
|
|
|
|
|
|
}
|
|
|
|
|
|
pub, sec := keyStrings()
|
|
|
|
|
|
doc := obj{
|
|
|
|
|
|
"source": *src,
|
|
|
|
|
|
"go": runtime.Version(),
|
|
|
|
|
|
"unicode": unicode.Version,
|
|
|
|
|
|
"description": "The author keys of package authorkey of datekeys-go and the signatures of crypto/ed25519, by scripts/authorkey-go-vectors.go. A text is an index into texts. A file is a list of parts: {hex}, {byte, n}, or {sealed: {seed, passphrase, work_factor, plain, draws}, length, sha256}, the age file of plain, a list of parts, for a scrypt recipient with the draws of crypto/rand in their order. A message_pattern of n is n bytes with (31·i + 7) mod 256 as byte i. Draws are those of crypto/rand, in their order.",
|
|
|
|
|
|
"sign": signSection(),
|
|
|
|
|
|
"scalars": scalarSection(),
|
|
|
|
|
|
"keys": keySection(),
|
|
|
|
|
|
"generate": generateSection(),
|
|
|
|
|
|
"encrypt": encryptSection(),
|
|
|
|
|
|
"public": publicSection(pub),
|
|
|
|
|
|
"secret": secretSection(sec),
|
|
|
|
|
|
"read": readSection(),
|
|
|
|
|
|
"runes": runeSection(1),
|
|
|
|
|
|
}
|
|
|
|
|
|
doc["texts"] = texts
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
e := json.NewEncoder(&buf)
|
|
|
|
|
|
e.SetEscapeHTML(false)
|
|
|
|
|
|
e.SetIndent("", " ")
|
|
|
|
|
|
check(e.Encode(doc))
|
|
|
|
|
|
// Arrays of numbers on one line each.
|
|
|
|
|
|
b := numbers.ReplaceAllFunc(buf.Bytes(), func(m []byte) []byte { return spaces.ReplaceAll(m, nil) })
|
|
|
|
|
|
check(os.WriteFile(*out, b, 0o644))
|
|
|
|
|
|
fmt.Printf("wrote %s, %d bytes\n", *out, len(b))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Characters written by their code points, so that the source stays ASCII
|
|
|
|
|
|
// where they matter.
|
|
|
|
|
|
var (
|
|
|
|
|
|
lf = string(rune(0x0a))
|
|
|
|
|
|
bom = string(rune(0xfeff))
|
|
|
|
|
|
ideographicSpace = string(rune(0x3000))
|
|
|
|
|
|
paragraphSeparator = string(rune(0x2029))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
numbers = regexp.MustCompile(`\[\s*-?[0-9]+(,\s*-?[0-9]+)*\s*\]`)
|
|
|
|
|
|
spaces = regexp.MustCompile(`\s+`)
|
|
|
|
|
|
)
|