|
|
//go:build ignore
|
|
|
|
|
|
//go:debug cryptocustomrand=1
|
|
|
|
|
|
// Writes test/vectors/authorkey.json and authorkey.g.dart, the vectors of
|
|
|
// the author keys of datekeys-dart, stage 7b of docs/PLAN_dart.md: the
|
|
|
// Ed25519 signatures of Go's crypto/ed25519 and the package authorkey of
|
|
|
// datekeys-go, with the texts of its errors.
|
|
|
//
|
|
|
// - 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
|
|
|
// the keystream of SeededRandomSource of lib/src/random.dart (ChaCha20
|
|
|
// under SHA-256(seed), zero nonce), with each draw; 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, which the tests write again with
|
|
|
// SeededRandomSource. authorkey.g.dart is the same document with only the
|
|
|
// signatures marked node and one in eight of the runes, as a Dart
|
|
|
// constant, for the tests that also run compiled to JavaScript. Arrays of
|
|
|
// numbers are written on one line.
|
|
|
//
|
|
|
// It imports only public packages, so it runs in the module of the
|
|
|
// reference implementation, without changing anything there, from the
|
|
|
// datekeys-go next to this repository, on the branch v0.12 at c531e93:
|
|
|
//
|
|
|
// cd ../datekeys-go && go run ../datekeys-dart/tool/authorkey_go_vectors.go \
|
|
|
// -source $(git rev-parse v0.12) -out ../datekeys-dart/test/vectors
|
|
|
//
|
|
|
// The output is the same on every run.
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
"bufio"
|
|
|
"bytes"
|
|
|
"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-dart stage 7b: " + 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), "node": node}
|
|
|
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()), "node": i == 0})
|
|
|
}
|
|
|
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 := sealed(seed, pass, wf, join(plain))
|
|
|
return part{"sealed": obj{"seed": seed, "passphrase": pass, "work_factor": wf, "plain": plain}, "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), "node": node}
|
|
|
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 {
|
|
|
var buf bytes.Buffer
|
|
|
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()
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
// 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))
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
var testdata = flag.String("testdata", "", "the testdata of this repository")
|
|
|
|
|
|
func main() {
|
|
|
out := flag.String("out", "", "where the vectors go")
|
|
|
src := flag.String("source", "", "the commit of datekeys-go")
|
|
|
flag.Parse()
|
|
|
if *out == "" || *src == "" || *testdata == "" {
|
|
|
log.Fatal("usage: -source <commit> -testdata <dir> -out <dir>")
|
|
|
}
|
|
|
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 tool/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}, length, sha256}, the age file of plain, a list of parts, for a scrypt recipient while crypto/rand reads the keystream of the seed. A message_pattern of n is n bytes with (31·i + 7) mod 256 as byte i. Draws are those of crypto/rand reading the keystream of SeededRandomSource. A case marked node false is left out compiled to JavaScript.",
|
|
|
"sign": signSection(),
|
|
|
"scalars": scalarSection(),
|
|
|
"keys": keySection(),
|
|
|
"generate": generateSection(),
|
|
|
"encrypt": encryptSection(),
|
|
|
"public": publicSection(pub),
|
|
|
"secret": secretSection(sec),
|
|
|
"read": readSection(),
|
|
|
}
|
|
|
write := func(runes int) []byte {
|
|
|
doc["runes"] = runeSection(runes)
|
|
|
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) })
|
|
|
buf.Reset()
|
|
|
buf.Write(b)
|
|
|
if bytes.Contains(buf.Bytes(), []byte("'''")) {
|
|
|
log.Fatal("the JSON holds three quotes")
|
|
|
}
|
|
|
return buf.Bytes()
|
|
|
}
|
|
|
full := write(1)
|
|
|
path := filepath.Join(*out, "authorkey.json")
|
|
|
check(os.WriteFile(path, full, 0o644))
|
|
|
fmt.Printf("wrote %s, %d bytes\n", path, len(full))
|
|
|
// The Dart constant: one in eight of the seeded signatures and of the
|
|
|
// rune cases, the long messages left out.
|
|
|
signs := doc["sign"].([]obj)
|
|
|
var some []obj
|
|
|
for _, c := range signs {
|
|
|
if c["node"].(bool) {
|
|
|
some = append(some, c)
|
|
|
}
|
|
|
}
|
|
|
doc["sign"] = some
|
|
|
part := write(8)
|
|
|
dart := "// Generated by tool/authorkey_go_vectors.go: authorkey.json with a part of\n" +
|
|
|
"// its signatures and rune cases, for the tests that also run compiled to\n" +
|
|
|
"// JavaScript, where no file can be read. Do not edit.\n\n" +
|
|
|
"/// A part of test/vectors/authorkey.json.\n" +
|
|
|
"const authorKeyJson = r'''\n" + string(part) + "''';\n"
|
|
|
dpath := filepath.Join(*out, "authorkey.g.dart")
|
|
|
check(os.WriteFile(dpath, []byte(dart), 0o644))
|
|
|
fmt.Printf("wrote %s, %d bytes\n", dpath, len(dart))
|
|
|
}
|
|
|
|
|
|
// 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+`)
|
|
|
)
|