Stage 2: the primitives, against vectors that Go computes

SHA-256 with its own compression, and on it HMAC-SHA256 with the inner and
outer states of the key computed once, HKDF-SHA256 and PBKDF2-HMAC-SHA256,
whose iterations are two compressions over words; scrypt with Salsa20/8;
ChaCha20, Poly1305 in 13-bit limbs and ChaCha20-Poly1305 with the tag
compared in constant time; X25519 on the field of TweetNaCl in doubles,
with the all-zero secret refused; the strict Ed25519 verification of
internal/ed25519strict; Go's Base64 with the offsets of its errors, and
age's Bech32.

tool/gen_primitive_vectors.go computes every expected value with Go and
x/crypto, from the inputs of RFC 5869, 7748, 7914, 8032 and 8439, edge
cases and seeded random ones. The tests also run compiled to JavaScript,
from a Dart copy of the JSON, without the cases that would take too long.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 2 days ago
parent c8bf3c0eab
commit 0c21f551c6

@ -0,0 +1,145 @@
/// Base64 decoding as Go's encoding/base64, with its strict mode and the
/// offset of its errors (CorruptInputError): the alphabet of age, of the
/// DateKey and of the other encodings of the protocol is decoded with the
/// same acceptance and the same error texts as the Go reference.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
/// Bytes that Go's base64 rejects: its CorruptInputError, with the text
/// `illegal base64 data at input byte N`.
final class Base64Exception implements Exception {
/// The error at byte [offset] of the input.
const Base64Exception(this.offset);
/// The offset that Go reports.
final int offset;
/// The text of Go's error.
String get message => 'illegal base64 data at input byte $offset';
@override
String toString() => message;
}
const _std = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const _url = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
Int16List _decodeMap(String alphabet) {
final m = Int16List(256)..fillRange(0, 256, -1);
for (var i = 0; i < alphabet.length; i++) {
m[alphabet.codeUnitAt(i)] = i;
}
return m;
}
final Int16List _stdMap = _decodeMap(_std);
final Int16List _urlMap = _decodeMap(_url);
bool _isNewline(int c) => c == 0x0a || c == 0x0d;
/// Decodes [src] as Go's base64 Encoding of the standard alphabet, or the
/// URL one when [url], with `=` padding when [padded], and in Go's strict
/// mode, which rejects non-zero trailing bits, when [strict]. As in Go, CR
/// and LF anywhere are skipped. Throws a [Base64Exception] with Go's offset.
Uint8List goBase64Decode(
List<int> src, {
bool url = false,
bool padded = true,
bool strict = false,
}) {
final map = url ? _urlMap : _stdMap;
final out = BytesBuilder(copy: false);
final q = Int32List(4);
var si = 0;
while (si < src.length) {
// Go's decodeQuantum.
var dlen = 4;
int? trailing;
var j = 0;
for (; j < 4; j++) {
if (si == src.length) {
if (j == 0) return out.takeBytes();
if (j == 1 || padded) throw Base64Exception(si - j);
dlen = j;
break;
}
final c = src[si++];
final v = c >= 0 && c < 256 ? map[c] : -1;
if (v >= 0) {
q[j] = v;
continue;
}
if (_isNewline(c)) {
j--;
continue;
}
if (!padded || c != 0x3d) throw Base64Exception(si - 1);
// The end, with padding.
if (j < 2) throw Base64Exception(si - 1);
if (j == 2) {
// "==" is expected, the first "=" is already consumed.
while (si < src.length && _isNewline(src[si])) {
si++;
}
if (si == src.length) throw Base64Exception(src.length);
if (src[si] != 0x3d) throw Base64Exception(si - 1);
si++;
}
while (si < src.length && _isNewline(src[si])) {
si++;
}
if (si < src.length) trailing = si;
dlen = j;
break;
}
for (var k = dlen; k < 4; k++) {
q[k] = 0;
}
final b0 = q[0] << 2 | q[1] >> 4;
final b1 = (q[1] & 15) << 4 | q[2] >> 2;
final b2 = (q[2] & 3) << 6 | q[3];
if (strict && dlen == 3 && b2 != 0) throw Base64Exception(si - 1);
if (strict && dlen == 2 && (b1 != 0 || b2 != 0)) {
throw Base64Exception(si - 2);
}
final bytes = [b0, b1, b2];
out.add(Uint8List.fromList(bytes.sublist(0, dlen - 1)));
if (trailing != null) throw Base64Exception(trailing);
}
return out.takeBytes();
}
/// Encodes [bytes] in Base64 as Go's Encoding: the standard alphabet, or the
/// URL one when [url], with `=` padding when [padded].
String goBase64Encode(List<int> bytes, {bool url = false, bool padded = true}) {
final alphabet = url ? _url : _std;
final out = StringBuffer();
var i = 0;
for (; i + 3 <= bytes.length; i += 3) {
final v = bytes[i] << 16 | bytes[i + 1] << 8 | bytes[i + 2];
out
..write(alphabet[v >> 18])
..write(alphabet[v >> 12 & 63])
..write(alphabet[v >> 6 & 63])
..write(alphabet[v & 63]);
}
final rest = bytes.length - i;
if (rest == 1) {
final v = bytes[i] << 16;
out
..write(alphabet[v >> 18])
..write(alphabet[v >> 12 & 63]);
if (padded) out.write('==');
} else if (rest == 2) {
final v = bytes[i] << 16 | bytes[i + 1] << 8;
out
..write(alphabet[v >> 18])
..write(alphabet[v >> 12 & 63])
..write(alphabet[v >> 6 & 63]);
if (padded) out.write('=');
}
return out.toString();
}

@ -0,0 +1,192 @@
/// Bech32 (BIP 173) as the internal/bech32 package of filippo.io/age v1.3.2,
/// which datekeys-go copies verbatim as codec/bech32: the encoding of the age
/// X25519 identities (AGE-SECRET-KEY-1…) and recipients (age1…). An
/// encoding, not cryptography. Like age, it accepts strings longer than the
/// 90 characters of BIP 173.
///
/// Ported from internal/bech32 of filippo.io/age v1.3.2
/// (https://github.com/FiloSottile/age), under its license:
///
/// Copyright (c) 2017 Takatoshi Nakagawa
/// Copyright (c) 2019 The age Authors
///
/// Permission is hereby granted, free of charge, to any person obtaining a
/// copy of this software and associated documentation files (the
/// "Software"), to deal in the Software without restriction, including
/// without limitation the rights to use, copy, modify, merge, publish,
/// distribute, sublicense, and/or sell copies of the Software, and to
/// permit persons to whom the Software is furnished to do so, subject to
/// the following conditions:
///
/// The above copyright notice and this permission notice shall be included
/// in all copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
/// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
/// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
/// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
/// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
/// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
/// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
///
/// Go strings are bytes: the positions in the error texts are offsets in the
/// UTF-8 of the string, as in Go. The mixed-case check of a string to decode
/// folds it with the Unicode case mapping of the platform, as Go does with
/// its own tables; they differ only for the few characters whose case
/// mapping is not one to one, which Bech32 rejects anyway, maybe with
/// another text.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'bytes.dart';
/// A string or data that Bech32 rejects, with the text of age's error.
final class Bech32Exception implements Exception {
/// An exception with age's [message].
const Bech32Exception(this.message);
/// The text of age's error.
final String message;
@override
String toString() => message;
}
const _charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
const _generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
int _polymod(List<int> values) {
var chk = 1;
for (final v in values) {
final top = chk >>> 25;
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (var i = 0; i < 5; i++) {
if ((top >>> i) & 1 == 1) chk ^= _generator[i];
}
}
return chk;
}
String _lowerAscii(String s) => String.fromCharCodes([
for (final c in s.codeUnits) c >= 0x41 && c <= 0x5a ? c + 0x20 : c,
]);
String _upperAscii(String s) => String.fromCharCodes([
for (final c in s.codeUnits) c >= 0x61 && c <= 0x7a ? c - 0x20 : c,
]);
List<int> _hrpExpand(String hrp) {
final h = utf8Bytes(_lowerAscii(hrp));
return [for (final c in h) c >> 5, 0, for (final c in h) c & 31];
}
List<int> _convertBits(List<int> data, int from, int to, bool pad) {
final out = <int>[];
var acc = 0;
var bits = 0;
final maxv = (1 << to) - 1;
for (var idx = 0; idx < data.length; idx++) {
final value = data[idx];
if (value >> from != 0) {
throw Bech32Exception(
'invalid data range: data[$idx]=$value (frombits=$from)',
);
}
// Go keeps acc in a uint32; only its low bits are ever read.
acc = ((acc << from) | value) & 0xffffff;
bits += from;
while (bits >= to) {
bits -= to;
out.add((acc >> bits) & maxv);
}
}
if (pad) {
if (bits > 0) out.add((acc << (to - bits)) & maxv);
} else if (bits >= from) {
throw const Bech32Exception('illegal zero padding');
} else if ((acc << (to - bits)) & maxv != 0) {
throw const Bech32Exception('non-zero padding');
}
return out;
}
/// Encodes [hrp] and [data] in Bech32, as age's bech32.Encode: an uppercase
/// HRP gives an uppercase string.
String bech32Encode(String hrp, List<int> data) {
final values = _convertBits(data, 8, 5, true);
final h = utf8Bytes(hrp);
if (h.isEmpty) throw Bech32Exception('invalid HRP: ${goQuote(h)}');
for (var p = 0; p < h.length;) {
final (c, size) = decodeRune(h, p);
if (c < 33 || c > 126) {
throw Bech32Exception('invalid HRP character: hrp[$p]=$c');
}
p += size;
}
if (_upperAscii(hrp) != hrp && _lowerAscii(hrp) != hrp) {
throw Bech32Exception('mixed case HRP: ${goQuote(h)}');
}
final lower = _lowerAscii(hrp) == hrp;
final lh = _lowerAscii(hrp);
final mod = _polymod([..._hrpExpand(lh), ...values, 0, 0, 0, 0, 0, 0]) ^ 1;
final out = StringBuffer(lh)..write('1');
for (final v in values) {
out.write(_charset[v]);
}
for (var p = 0; p < 6; p++) {
out.write(_charset[(mod >>> (5 * (5 - p))) & 31]);
}
final s = out.toString();
return lower ? s : _upperAscii(s);
}
/// Decodes the Bech32 string [s], as age's bech32.Decode: the HRP keeps the
/// case of the string.
({String hrp, Uint8List data}) bech32Decode(String s) {
// Go compares the string with its Unicode lower and upper case.
if (s.toLowerCase() != s && s.toUpperCase() != s) {
throw const Bech32Exception('mixed case');
}
final b = utf8Bytes(s);
final pos = b.lastIndexOf(0x31);
if (pos < 1 || pos + 7 > b.length) {
throw Bech32Exception(
"separator '1' at invalid position: pos=$pos, len=${b.length}",
);
}
final hrp = Uint8List.sublistView(b, 0, pos);
for (var p = 0; p < hrp.length;) {
final (c, size) = decodeRune(hrp, p);
if (c < 33 || c > 126) {
throw Bech32Exception('invalid character human-readable part: s[$p]=$c');
}
p += size;
}
final rest = Uint8List.sublistView(b, pos + 1);
final data = <int>[];
for (var p = 0; p < rest.length;) {
var (c, size) = decodeRune(rest, p);
// Fold ASCII explicitly, as age does.
if (c >= 0x41 && c <= 0x5a) c += 0x20;
final d = c < 0x80 ? _charset.indexOf(String.fromCharCode(c)) : -1;
if (d == -1) {
throw Bech32Exception('invalid character data part: s[$p]=$c');
}
data.add(d);
p += size;
}
if (data.length < 6) throw const Bech32Exception('data part too short');
final hrpString = String.fromCharCodes(hrp);
if (_polymod([..._hrpExpand(hrpString), ...data]) != 1) {
throw const Bech32Exception('invalid checksum');
}
return (
hrp: hrpString,
data: Uint8List.fromList(
_convertBits(data.sublist(0, data.length - 6), 5, 8, false),
),
);
}

@ -0,0 +1,537 @@
/// ChaCha20, Poly1305 and the AEAD ChaCha20-Poly1305 of RFC 8439, which age
/// uses to wrap file keys and for its STREAM.
///
/// The arithmetic is exact on the VM and when compiled to JavaScript:
/// - ChaCha20 adds two 32-bit words at a time and masks the sum, and its
/// rotations are of 32-bit values, so that the 32-bit bit operators of the
/// web give what the 64-bit ones of the VM give;
/// - Poly1305 keeps its accumulator and r in ten limbs of 13 bits. A product
/// of a limb of h (below 2^15) and one of 5·r (below 2^16) is below 2^31,
/// and a sum of ten of them and a carry is below 2^35: far from 2^53, where
/// a double stops being exact. The carries of those sums, which may pass
/// 2^32, are taken with `~/` and the low bits with `&`, never with a shift
/// of a value above 2^32, which the web would truncate.
///
/// The tag is compared in constant time. The rest is written without
/// branches or indexes that depend on secrets, but neither the VM nor a
/// JavaScript engine promises constant time.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
const _mask32 = 0xffffffff;
/// The size of a ChaCha20 key.
const chachaKeySize = 32;
/// The size of the nonce of ChaCha20-Poly1305 (RFC 8439).
const chachaNonceSize = 12;
/// The size of a Poly1305 tag, the overhead of ChaCha20-Poly1305.
const poly1305TagSize = 16;
// ---------------------------------------------------------------------------
// ChaCha20
int _le32(List<int> b, int o) =>
b[o] | b[o + 1] << 8 | b[o + 2] << 16 | (b[o + 3] << 24 & _mask32);
/// The ChaCha20 keystream of RFC 8439, 2.4: [key] (32 bytes), [nonce] (12
/// bytes) and the block counter from [counter].
final class ChaCha20 {
/// The cipher of [key] and [nonce], from block [counter].
ChaCha20(List<int> key, List<int> nonce, [int counter = 0]) {
if (key.length != chachaKeySize) {
throw ArgumentError.value(key.length, 'key', 'not 32 bytes');
}
if (nonce.length != chachaNonceSize) {
throw ArgumentError.value(nonce.length, 'nonce', 'not 12 bytes');
}
if (counter < 0 || counter > _mask32) {
throw RangeError.range(counter, 0, _mask32, 'counter');
}
_input[0] = 0x61707865;
_input[1] = 0x3320646e;
_input[2] = 0x79622d32;
_input[3] = 0x6b206574;
for (var i = 0; i < 8; i++) {
_input[4 + i] = _le32(key, 4 * i);
}
_input[12] = counter;
for (var i = 0; i < 3; i++) {
_input[13 + i] = _le32(nonce, 4 * i);
}
}
final Uint32List _input = Uint32List(16);
final Uint32List _x = Uint32List(16);
final Uint8List _stream = Uint8List(64);
int _used = 64;
bool _overflow = false;
// The next 64 bytes of keystream into _stream, and the counter advanced.
void _block() {
// Go's chacha20 refuses a block past counter 2^32 - 1, which RFC 8439
// leaves undefined.
if (_overflow) throw StateError('chacha20: counter overflow');
final s = _input;
var x0 = s[0], x1 = s[1], x2 = s[2], x3 = s[3];
var x4 = s[4], x5 = s[5], x6 = s[6], x7 = s[7];
var x8 = s[8], x9 = s[9], x10 = s[10], x11 = s[11];
var x12 = s[12], x13 = s[13], x14 = s[14], x15 = s[15];
for (var i = 0; i < 10; i++) {
// Column rounds.
x0 = (x0 + x4) & _mask32;
x12 ^= x0;
x12 = (x12 << 16 & _mask32) | x12 >>> 16;
x8 = (x8 + x12) & _mask32;
x4 ^= x8;
x4 = (x4 << 12 & _mask32) | x4 >>> 20;
x0 = (x0 + x4) & _mask32;
x12 ^= x0;
x12 = (x12 << 8 & _mask32) | x12 >>> 24;
x8 = (x8 + x12) & _mask32;
x4 ^= x8;
x4 = (x4 << 7 & _mask32) | x4 >>> 25;
x1 = (x1 + x5) & _mask32;
x13 ^= x1;
x13 = (x13 << 16 & _mask32) | x13 >>> 16;
x9 = (x9 + x13) & _mask32;
x5 ^= x9;
x5 = (x5 << 12 & _mask32) | x5 >>> 20;
x1 = (x1 + x5) & _mask32;
x13 ^= x1;
x13 = (x13 << 8 & _mask32) | x13 >>> 24;
x9 = (x9 + x13) & _mask32;
x5 ^= x9;
x5 = (x5 << 7 & _mask32) | x5 >>> 25;
x2 = (x2 + x6) & _mask32;
x14 ^= x2;
x14 = (x14 << 16 & _mask32) | x14 >>> 16;
x10 = (x10 + x14) & _mask32;
x6 ^= x10;
x6 = (x6 << 12 & _mask32) | x6 >>> 20;
x2 = (x2 + x6) & _mask32;
x14 ^= x2;
x14 = (x14 << 8 & _mask32) | x14 >>> 24;
x10 = (x10 + x14) & _mask32;
x6 ^= x10;
x6 = (x6 << 7 & _mask32) | x6 >>> 25;
x3 = (x3 + x7) & _mask32;
x15 ^= x3;
x15 = (x15 << 16 & _mask32) | x15 >>> 16;
x11 = (x11 + x15) & _mask32;
x7 ^= x11;
x7 = (x7 << 12 & _mask32) | x7 >>> 20;
x3 = (x3 + x7) & _mask32;
x15 ^= x3;
x15 = (x15 << 8 & _mask32) | x15 >>> 24;
x11 = (x11 + x15) & _mask32;
x7 ^= x11;
x7 = (x7 << 7 & _mask32) | x7 >>> 25;
// Diagonal rounds.
x0 = (x0 + x5) & _mask32;
x15 ^= x0;
x15 = (x15 << 16 & _mask32) | x15 >>> 16;
x10 = (x10 + x15) & _mask32;
x5 ^= x10;
x5 = (x5 << 12 & _mask32) | x5 >>> 20;
x0 = (x0 + x5) & _mask32;
x15 ^= x0;
x15 = (x15 << 8 & _mask32) | x15 >>> 24;
x10 = (x10 + x15) & _mask32;
x5 ^= x10;
x5 = (x5 << 7 & _mask32) | x5 >>> 25;
x1 = (x1 + x6) & _mask32;
x12 ^= x1;
x12 = (x12 << 16 & _mask32) | x12 >>> 16;
x11 = (x11 + x12) & _mask32;
x6 ^= x11;
x6 = (x6 << 12 & _mask32) | x6 >>> 20;
x1 = (x1 + x6) & _mask32;
x12 ^= x1;
x12 = (x12 << 8 & _mask32) | x12 >>> 24;
x11 = (x11 + x12) & _mask32;
x6 ^= x11;
x6 = (x6 << 7 & _mask32) | x6 >>> 25;
x2 = (x2 + x7) & _mask32;
x13 ^= x2;
x13 = (x13 << 16 & _mask32) | x13 >>> 16;
x8 = (x8 + x13) & _mask32;
x7 ^= x8;
x7 = (x7 << 12 & _mask32) | x7 >>> 20;
x2 = (x2 + x7) & _mask32;
x13 ^= x2;
x13 = (x13 << 8 & _mask32) | x13 >>> 24;
x8 = (x8 + x13) & _mask32;
x7 ^= x8;
x7 = (x7 << 7 & _mask32) | x7 >>> 25;
x3 = (x3 + x4) & _mask32;
x14 ^= x3;
x14 = (x14 << 16 & _mask32) | x14 >>> 16;
x9 = (x9 + x14) & _mask32;
x4 ^= x9;
x4 = (x4 << 12 & _mask32) | x4 >>> 20;
x3 = (x3 + x4) & _mask32;
x14 ^= x3;
x14 = (x14 << 8 & _mask32) | x14 >>> 24;
x9 = (x9 + x14) & _mask32;
x4 ^= x9;
x4 = (x4 << 7 & _mask32) | x4 >>> 25;
}
final x = _x;
x[0] = x0 + s[0];
x[1] = x1 + s[1];
x[2] = x2 + s[2];
x[3] = x3 + s[3];
x[4] = x4 + s[4];
x[5] = x5 + s[5];
x[6] = x6 + s[6];
x[7] = x7 + s[7];
x[8] = x8 + s[8];
x[9] = x9 + s[9];
x[10] = x10 + s[10];
x[11] = x11 + s[11];
x[12] = x12 + s[12];
x[13] = x13 + s[13];
x[14] = x14 + s[14];
x[15] = x15 + s[15];
// A Uint32List keeps the low 32 bits of each sum, on the VM and on the
// web alike.
final out = _stream;
for (var i = 0; i < 16; i++) {
final v = x[i];
out[4 * i] = v & 0xff;
out[4 * i + 1] = v >>> 8 & 0xff;
out[4 * i + 2] = v >>> 16 & 0xff;
out[4 * i + 3] = v >>> 24;
}
if (s[12] == _mask32) {
_overflow = true;
} else {
s[12] = s[12] + 1;
}
_used = 0;
}
/// XORs the keystream into [data] from [start] to [end], in place.
void xorInPlace(Uint8List data, [int start = 0, int? end]) {
final stop = end ?? data.length;
RangeError.checkValidRange(start, stop, data.length);
for (var i = start; i < stop; i++) {
if (_used == 64) _block();
data[i] ^= _stream[_used++];
}
}
/// The next [n] bytes of keystream.
Uint8List keystream(int n) {
final out = Uint8List(n);
xorInPlace(out);
return out;
}
/// Clears the key and the keystream.
void wipe() {
_input.fillRange(0, 16, 0);
_x.fillRange(0, 16, 0);
_stream.fillRange(0, 64, 0);
_used = 64;
}
}
// ---------------------------------------------------------------------------
// Poly1305
const _limbBits = 13;
const _limbMask = 0x1fff;
const _limbRadix = 0x2000;
const _limbs = 10;
// The ten 13-bit limbs of the little-endian integer in pad[0..16]; pad has
// at least 19 bytes, those above the integer zero.
void _limbsOf(Uint8List pad, Int32List out) {
for (var i = 0; i < _limbs; i++) {
final bit = _limbBits * i;
final byte = bit >>> 3;
final v = pad[byte] | pad[byte + 1] << 8 | pad[byte + 2] << 16;
out[i] = v >>> (bit & 7) & _limbMask;
}
}
/// Poly1305 (RFC 8439, 2.5) with a one-time key of 32 bytes: r and s.
final class Poly1305 {
/// The MAC of the one-time [key], 32 bytes.
Poly1305(List<int> key) {
if (key.length != 32) {
throw ArgumentError.value(key.length, 'key', 'not 32 bytes');
}
final pad = _pad;
for (var i = 0; i < 16; i++) {
pad[i] = key[i];
}
// Clamping, RFC 8439 2.5.1.
pad[3] &= 15;
pad[7] &= 15;
pad[11] &= 15;
pad[15] &= 15;
pad[4] &= 252;
pad[8] &= 252;
pad[12] &= 252;
_limbsOf(pad, _r);
for (var i = 0; i < 16; i++) {
_s[i] = key[16 + i];
}
pad.fillRange(0, pad.length, 0);
}
final Int32List _r = Int32List(_limbs);
final Int32List _h = Int32List(_limbs);
final Int32List _m = Int32List(_limbs);
final Uint8List _s = Uint8List(16);
final Uint8List _pad = Uint8List(20);
int _buffered = 0;
/// Adds [data] to the message.
void add(List<int> data, [int start = 0, int? end]) {
final stop = end ?? data.length;
RangeError.checkValidRange(start, stop, data.length);
var i = start;
final pad = _pad;
if (_buffered > 0) {
while (_buffered < 16 && i < stop) {
pad[_buffered++] = data[i++];
}
if (_buffered < 16) return;
_block(16);
}
for (; i + 16 <= stop; i += 16) {
for (var k = 0; k < 16; k++) {
pad[k] = data[i + k];
}
_block(16);
}
while (i < stop) {
pad[_buffered++] = data[i++];
}
}
// h = (h + the block of the first n bytes of _pad, with the byte 1 after
// them) · r mod 2^130 - 5.
void _block(int n) {
final pad = _pad;
pad[n] = 1;
pad.fillRange(n + 1, pad.length, 0);
final h = _h;
final r = _r;
final m = _m;
_limbsOf(pad, m);
pad.fillRange(0, pad.length, 0);
_buffered = 0;
for (var i = 0; i < _limbs; i++) {
h[i] += m[i];
}
// d_i = the sum over j of h_j · r_(i-j), where a limb past 2^130 comes
// back multiplied by 5, as 2^130 = 5 mod p. Each d_i is below 2^35.
var carry = 0;
for (var i = 0; i < _limbs; i++) {
var d = carry;
for (var j = 0; j <= i; j++) {
d += h[j] * r[i - j];
}
for (var j = i + 1; j < _limbs; j++) {
d += h[j] * 5 * r[i - j + _limbs];
}
carry = d ~/ _limbRadix;
m[i] = d & _limbMask;
}
// The carry out of limb 9 is a multiple of 2^130: it comes back · 5.
final v = m[0] + carry * 5;
h[0] = v & _limbMask;
h[1] = m[1] + v ~/ _limbRadix;
for (var i = 2; i < _limbs; i++) {
h[i] = m[i];
}
}
/// The 16-byte tag of the message. The object is wiped and cannot be used
/// again.
Uint8List finish() {
if (_buffered > 0) _block(_buffered);
final h = _h;
// Two full carries: every limb below 2^13, but for h_1, which may reach
// 2^13 by one, and h below 2^130 + 2^26.
for (var round = 0; round < 2; round++) {
var c = 0;
for (var i = 0; i < _limbs; i++) {
final v = h[i] + c;
c = v ~/ _limbRadix;
h[i] = v & _limbMask;
}
final v = h[0] + c * 5;
h[0] = v & _limbMask;
h[1] += v ~/ _limbRadix;
}
// g = h + 5 - 2^130. The carry out of limb 9 is 1 exactly when
// h + 5 >= 2^130, that is h >= p, and then h mod p = g.
final g = _m;
var c = 5;
for (var i = 0; i < _limbs; i++) {
final v = h[i] + c;
c = v ~/ _limbRadix;
g[i] = v & _limbMask;
}
final keep = 1 - c;
for (var i = 0; i < _limbs; i++) {
h[i] = keep * h[i] + c * g[i];
}
// (h + s) mod 2^128, little-endian. The limbs are added, not ORed, so
// that h_1 = 2^13 carries into the next one.
final tag = Uint8List(16);
var acc = 0;
var bits = 0;
var limb = 0;
var carry = 0;
for (var i = 0; i < 16; i++) {
while (bits < 8) {
acc += h[limb++] << bits;
bits += _limbBits;
}
final v = (acc & 0xff) + _s[i] + carry;
tag[i] = v & 0xff;
carry = v >>> 8;
acc >>>= 8;
bits -= 8;
}
wipe();
return tag;
}
/// Clears the key and the state.
void wipe() {
_r.fillRange(0, _limbs, 0);
_h.fillRange(0, _limbs, 0);
_m.fillRange(0, _limbs, 0);
_s.fillRange(0, 16, 0);
_pad.fillRange(0, _pad.length, 0);
_buffered = 0;
}
}
/// The Poly1305 tag of [message] under the one-time [key].
Uint8List poly1305(List<int> key, List<int> message) =>
(Poly1305(key)..add(message)).finish();
/// Whether [a] and [b] are equal, in a time that depends only on their
/// lengths.
bool constantTimeEquals(List<int> a, List<int> b) {
if (a.length != b.length) return false;
var d = 0;
for (var i = 0; i < a.length; i++) {
d |= a[i] ^ b[i];
}
return d == 0;
}
// ---------------------------------------------------------------------------
// ChaCha20-Poly1305
final _zeros = Uint8List(16);
Uint8List _tag(
ChaCha20 cipher,
List<int> aad,
Uint8List ciphertext,
int start,
int end,
) =>
// The one-time key is the first 32 bytes of block 0 (RFC 8439, 2.6); the
// rest of that block is discarded, and the message starts at block 1.
_tagWith(cipher.keystream(64), aad, ciphertext, start, end);
// An int below 2^53 as 8 little-endian bytes, without a 64-bit shift.
Uint8List _le64(int v) {
final out = Uint8List(8);
var x = v;
for (var i = 0; i < 8; i++) {
out[i] = x & 0xff;
x = x ~/ 256;
}
return out;
}
/// The AEAD ChaCha20-Poly1305 of RFC 8439, 2.8, as Go's chacha20poly1305:
/// the ciphertext followed by the 16-byte tag.
Uint8List chacha20Poly1305Seal(
List<int> key,
List<int> nonce,
List<int> plaintext, [
List<int> aad = const [],
]) {
final cipher = ChaCha20(key, nonce);
final out = Uint8List(plaintext.length + poly1305TagSize);
out.setRange(0, plaintext.length, plaintext);
try {
// Block 0 gives the one-time key; the message starts at block 1.
final otk = cipher.keystream(64);
cipher.xorInPlace(out, 0, plaintext.length);
final tag = _tagWith(otk, aad, out, 0, plaintext.length);
out.setRange(plaintext.length, out.length, tag);
return out;
} finally {
cipher.wipe();
}
}
/// Opens the [ciphertext] (with its tag) of ChaCha20-Poly1305: the plaintext,
/// or null when the tag does not verify. The tag is compared in constant
/// time, and nothing is decrypted before it verifies.
Uint8List? chacha20Poly1305Open(
List<int> key,
List<int> nonce,
Uint8List ciphertext, [
List<int> aad = const [],
]) {
if (ciphertext.length < poly1305TagSize) return null;
final n = ciphertext.length - poly1305TagSize;
final cipher = ChaCha20(key, nonce);
try {
final want = _tag(cipher, aad, ciphertext, 0, n);
final got = Uint8List.sublistView(ciphertext, n);
if (!constantTimeEquals(want, got)) return null;
final out = Uint8List.fromList(Uint8List.sublistView(ciphertext, 0, n));
cipher.xorInPlace(out);
return out;
} finally {
cipher.wipe();
}
}
Uint8List _tagWith(
Uint8List otk,
List<int> aad,
Uint8List ciphertext,
int start,
int end,
) {
final mac = Poly1305(otk.sublist(0, 32));
otk.fillRange(0, otk.length, 0);
final n = end - start;
mac
..add(aad)
..add(_zeros, 0, (16 - aad.length % 16) % 16)
..add(ciphertext, start, end)
..add(_zeros, 0, (16 - n % 16) % 16)
..add(_le64(aad.length))
..add(_le64(n));
return mac.finish();
}

@ -0,0 +1,583 @@
/// X25519 (RFC 7748) and the strict verification of Ed25519 signatures of
/// the author signature (spec v0.11, §29.9), as the Go package
/// internal/ed25519strict.
///
/// The field arithmetic modulo p = 2^255 - 19 is the one of TweetNaCl, in
/// its JavaScript port: an element is sixteen limbs of 16 bits in a
/// Float64List, a double that holds integers exactly below 2^53. The bounds
/// are TweetNaCl's: the limbs that enter a product are below 2^17 in
/// absolute value, a product below 2^34, a sum of sixteen of them below
/// 2^38, and folding the upper half (· 38) below 2^44. The carries divide by
/// 2^16 and round down, which is exact for those values. The same code runs
/// on the VM and on the web, and the conditional swaps of the ladder are
/// arithmetic, without branches on the secret scalar; neither platform
/// promises constant time, though.
///
/// Ed25519 only verifies, over public values. The reduction of scalars
/// modulo the group order and the checks of [onCurve] use BigInt, which is
/// not constant time: they never see a secret.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'bytes.dart';
// ---------------------------------------------------------------------------
// The field GF(2^255 - 19)
final BigInt _p = (BigInt.one << 255) - BigInt.from(19);
Float64List _gf([List<int>? init]) {
final r = Float64List(16);
if (init != null) {
for (var i = 0; i < init.length; i++) {
r[i] = init[i].toDouble();
}
}
return r;
}
// The element of a BigInt in 0..p-1.
Float64List _gfOf(BigInt v) {
final r = Float64List(16);
var x = v;
final mask = BigInt.from(0xffff);
for (var i = 0; i < 16; i++) {
r[i] = (x & mask).toInt().toDouble();
x >>= 16;
}
return r;
}
BigInt _bigOf(Uint8List le) {
var n = BigInt.zero;
for (var i = le.length - 1; i >= 0; i--) {
n = (n << 8) | BigInt.from(le[i]);
}
return n;
}
Uint8List _leOf(BigInt v, int length) {
final out = Uint8List(length);
var x = v;
final mask = BigInt.from(0xff);
for (var i = 0; i < length; i++) {
out[i] = (x & mask).toInt();
x >>= 8;
}
return out;
}
void _set(Float64List r, Float64List a) {
for (var i = 0; i < 16; i++) {
r[i] = a[i];
}
}
// Carries every limb into the next, and the top one back into limb 0 · 38.
void _car(Float64List o) {
var c = 1.0;
for (var i = 0; i < 16; i++) {
final v = o[i] + c + 65535;
c = (v / 65536).floorToDouble();
o[i] = v - c * 65536;
}
o[0] += c - 1 + 37 * (c - 1);
}
// Swaps p and q when b is 1 and leaves them when it is 0, without a branch.
void _sel(Float64List p, Float64List q, int b) {
final f = b.toDouble();
for (var i = 0; i < 16; i++) {
final t = f * (p[i] - q[i]);
p[i] -= t;
q[i] += t;
}
}
// The canonical 32 little-endian bytes of n.
void _pack(Uint8List o, Float64List n) {
final t = _gf();
_set(t, n);
_car(t);
_car(t);
_car(t);
final ti = Int32List(16);
for (var i = 0; i < 16; i++) {
ti[i] = t[i].toInt();
}
final m = Int32List(16);
for (var j = 0; j < 2; j++) {
m[0] = ti[0] - 0xffed;
for (var i = 1; i < 15; i++) {
m[i] = ti[i] - 0xffff - ((m[i - 1] >> 16) & 1);
m[i - 1] &= 0xffff;
}
m[15] = ti[15] - 0x7fff - ((m[14] >> 16) & 1);
final b = (m[15] >> 16) & 1;
m[14] &= 0xffff;
// ti = m unless m borrowed (b = 1).
final keep = 1 - b;
for (var i = 0; i < 16; i++) {
ti[i] = ti[i] + keep * (m[i] - ti[i]);
}
}
for (var i = 0; i < 16; i++) {
o[2 * i] = ti[i] & 0xff;
o[2 * i + 1] = (ti[i] >> 8) & 0xff;
}
}
bool _neq(Float64List a, Float64List b) {
final c = Uint8List(32);
final d = Uint8List(32);
_pack(c, a);
_pack(d, b);
return !equalBytes(c, d);
}
int _par(Float64List a) {
final d = Uint8List(32);
_pack(d, a);
return d[0] & 1;
}
// The element of 32 little-endian bytes, bit 255 ignored.
void _unpack(Float64List o, Uint8List n) {
for (var i = 0; i < 16; i++) {
o[i] = (n[2 * i] + (n[2 * i + 1] << 8)).toDouble();
}
o[15] = (n[30] + ((n[31] & 0x7f) << 8)).toDouble();
}
void _add(Float64List o, Float64List a, Float64List b) {
for (var i = 0; i < 16; i++) {
o[i] = a[i] + b[i];
}
}
void _sub(Float64List o, Float64List a, Float64List b) {
for (var i = 0; i < 16; i++) {
o[i] = a[i] - b[i];
}
}
final Float64List _t = Float64List(31);
// o = a · b. o may be a or b.
void _mul(Float64List o, Float64List a, Float64List b) {
final t = _t;
for (var i = 0; i < 31; i++) {
t[i] = 0;
}
for (var i = 0; i < 16; i++) {
final ai = a[i];
for (var j = 0; j < 16; j++) {
t[i + j] += ai * b[j];
}
}
// 2^256 = 38 mod p.
for (var i = 0; i < 15; i++) {
t[i] += 38 * t[i + 16];
}
for (var round = 0; round < 2; round++) {
var c = 1.0;
for (var i = 0; i < 16; i++) {
final v = t[i] + c + 65535;
c = (v / 65536).floorToDouble();
t[i] = v - c * 65536;
}
t[0] += c - 1 + 37 * (c - 1);
}
for (var i = 0; i < 16; i++) {
o[i] = t[i];
}
}
void _sq(Float64List o, Float64List a) => _mul(o, a, a);
// o = i^(p - 2) = 1/i.
void _inv(Float64List o, Float64List i) {
final c = _gf();
_set(c, i);
for (var a = 253; a >= 0; a--) {
_sq(c, c);
if (a != 2 && a != 4) _mul(c, c, i);
}
_set(o, c);
}
// o = i^((p - 5) / 8).
void _pow2523(Float64List o, Float64List i) {
final c = _gf();
_set(c, i);
for (var a = 250; a >= 0; a--) {
_sq(c, c);
if (a != 1) _mul(c, c, i);
}
_set(o, c);
}
// ---------------------------------------------------------------------------
// X25519
/// The size of an X25519 scalar, of a u-coordinate and of a shared secret.
const x25519Size = 32;
final Float64List _a24 = _gf(const [0xdb41, 1]); // 121665
/// The function X25519 of RFC 7748, 5: the u-coordinate of [scalar], clamped,
/// times the point of u-coordinate [u], whose bit 255 is ignored. It returns
/// the all-zero string for a point of low order; [x25519Agree] rejects it.
Uint8List x25519(List<int> scalar, List<int> u) {
if (scalar.length != x25519Size) {
throw ArgumentError.value(scalar.length, 'scalar', 'not 32 bytes');
}
if (u.length != x25519Size) {
throw ArgumentError.value(u.length, 'u', 'not 32 bytes');
}
final z = Uint8List.fromList(scalar);
z[31] = (z[31] & 127) | 64;
z[0] &= 248;
final x = _gf();
_unpack(x, Uint8List.fromList(u));
final a = _gf(), b = _gf(), c = _gf(), d = _gf(), e = _gf(), f = _gf();
_set(b, x);
a[0] = 1;
d[0] = 1;
for (var i = 254; i >= 0; i--) {
final r = (z[i >>> 3] >>> (i & 7)) & 1;
_sel(a, b, r);
_sel(c, d, r);
_add(e, a, c);
_sub(a, a, c);
_add(c, b, d);
_sub(b, b, d);
_sq(d, e);
_sq(f, a);
_mul(a, c, a);
_mul(c, b, e);
_add(e, a, c);
_sub(a, a, c);
_sq(b, a);
_sub(c, d, f);
_mul(a, c, _a24);
_add(a, a, d);
_mul(c, c, a);
_mul(a, d, f);
_mul(d, b, x);
_sq(b, e);
_sel(a, b, r);
_sel(c, d, r);
}
_inv(c, c);
_mul(a, a, c);
final out = Uint8List(32);
_pack(out, a);
z.fillRange(0, 32, 0);
for (final v in [a, b, c, d, e, f, x]) {
v.fillRange(0, 16, 0);
}
return out;
}
final Uint8List _basePointU = Uint8List(32)..[0] = 9;
/// The public key of the X25519 [scalar]: X25519 of it and the base point,
/// u = 9.
Uint8List x25519PublicKey(List<int> scalar) => x25519(scalar, _basePointU);
/// A point of low order offered to X25519: the shared secret would be zero
/// whatever the scalar (RFC 7748, 6.1). The message is the one of Go's
/// crypto/ecdh, which age quotes.
final class X25519LowOrderException implements Exception {
/// The exception.
const X25519LowOrderException();
/// The text of Go's error.
String get message =>
'crypto/ecdh: bad X25519 remote ECDH input: low order point';
@override
String toString() => message;
}
/// The shared secret of [scalar] and the public key [u], as Go's crypto/ecdh
/// X25519 ECDH: throws an [X25519LowOrderException] when it is all zeros.
/// The check reads every byte.
Uint8List x25519Agree(List<int> scalar, List<int> u) {
final s = x25519(scalar, u);
var acc = 0;
for (final b in s) {
acc |= b;
}
if (acc == 0) throw const X25519LowOrderException();
return s;
}
// ---------------------------------------------------------------------------
// Ed25519, strict verification
/// The order of the prime subgroup of edwards25519, ℓ = 2^252 +
/// 27742317777372353535851937790883648493.
final BigInt ed25519Order =
(BigInt.one << 252) +
BigInt.parse('27742317777372353535851937790883648493');
// d = -121665/121666, 2d, sqrt(-1) and the base point B = (x, 4/5) with x
// even (RFC 8032, 5.1), computed rather than copied.
final BigInt _dBig =
(-BigInt.from(121665) * BigInt.from(121666).modInverse(_p)) % _p;
final Float64List _d = _gfOf(_dBig);
final Float64List _d2 = _gfOf((_dBig * BigInt.two) % _p);
final BigInt _sqrtM1Big = BigInt.two.modPow(
(_p - BigInt.one) ~/ BigInt.from(4),
_p,
);
final Float64List _sqrtM1 = _gfOf(_sqrtM1Big);
final List<Float64List> _base = () {
final y = (BigInt.from(4) * BigInt.from(5).modInverse(_p)) % _p;
final x = _recoverX(y, 0)!;
return [
_gfOf(x),
_gfOf(y),
_gf(const [1]),
_gfOf((x * y) % _p),
];
}();
// The x of y on the curve whose low bit is [sign], or null: RFC 8032, 5.1.3.
BigInt? _recoverX(BigInt y, int sign) {
final y2 = (y * y) % _p;
final u = (y2 - BigInt.one) % _p;
final v = (_dBig * y2 + BigInt.one) % _p;
final v3 = (v * v % _p) * v % _p;
final v7 = (v3 * v3 % _p) * v % _p;
var x =
(u * v3 % _p) *
(u * v7 % _p).modPow((_p - BigInt.from(5)) ~/ BigInt.from(8), _p) %
_p;
final vx2 = v * x % _p * x % _p;
if (vx2 == u) {
// x is a root.
} else if (vx2 == (_p - u) % _p) {
x = x * _sqrtM1Big % _p;
} else {
return null;
}
if (x == BigInt.zero && sign == 1) return null;
if (x.isOdd != (sign == 1)) x = (_p - x) % _p;
return x;
}
// A point in extended coordinates (X, Y, Z, T).
List<Float64List> _point() => [_gf(), _gf(), _gf(), _gf()];
// p = p + q, the unified addition of TweetNaCl (also a doubling).
void _padd(List<Float64List> p, List<Float64List> q) {
final a = _gf(), b = _gf(), c = _gf(), d = _gf();
final e = _gf(), f = _gf(), g = _gf(), h = _gf(), t = _gf();
_sub(a, p[1], p[0]);
_sub(t, q[1], q[0]);
_mul(a, a, t);
_add(b, p[0], p[1]);
_add(t, q[0], q[1]);
_mul(b, b, t);
_mul(c, p[3], q[3]);
_mul(c, c, _d2);
_mul(d, p[2], q[2]);
_add(d, d, d);
_sub(e, b, a);
_sub(f, d, c);
_add(g, d, c);
_add(h, b, a);
_mul(p[0], e, f);
_mul(p[1], h, g);
_mul(p[2], g, f);
_mul(p[3], e, h);
}
void _cswap(List<Float64List> p, List<Float64List> q, int b) {
for (var i = 0; i < 4; i++) {
_sel(p[i], q[i], b);
}
}
void _ppack(Uint8List r, List<Float64List> p) {
final tx = _gf(), ty = _gf(), zi = _gf();
_inv(zi, p[2]);
_mul(tx, p[0], zi);
_mul(ty, p[1], zi);
_pack(r, ty);
r[31] ^= _par(tx) << 7;
}
// p = [s]q, s 32 little-endian bytes; q is consumed.
void _scalarMult(List<Float64List> p, List<Float64List> q, Uint8List s) {
_set(p[0], _gf());
_set(p[1], _gf(const [1]));
_set(p[2], _gf(const [1]));
_set(p[3], _gf());
for (var i = 255; i >= 0; i--) {
final b = (s[i >>> 3] >>> (i & 7)) & 1;
_cswap(p, q, b);
_padd(q, p);
_padd(p, p);
_cswap(p, q, b);
}
}
// r = -A for the encoding [a], or false when it is not a point of the curve.
// Non-canonical encodings decode reduced; the caller rejects them first.
bool _unpackNeg(List<Float64List> r, Uint8List a) {
final t = _gf(), chk = _gf(), num = _gf(), den = _gf();
final den2 = _gf(), den4 = _gf(), den6 = _gf();
_set(r[2], _gf(const [1]));
_unpack(r[1], a);
_sq(num, r[1]);
_mul(den, num, _d);
_sub(num, num, r[2]);
_add(den, r[2], den);
_sq(den2, den);
_sq(den4, den2);
_mul(den6, den4, den2);
_mul(t, den6, num);
_mul(t, t, den);
_pow2523(t, t);
_mul(t, t, num);
_mul(t, t, den);
_mul(t, t, den);
_mul(r[0], t, den);
_sq(chk, r[0]);
_mul(chk, chk, den);
if (_neq(chk, num)) _mul(r[0], r[0], _sqrtM1);
_sq(chk, r[0]);
_mul(chk, chk, den);
if (_neq(chk, num)) return false;
if (_par(r[0]) == (a[31] >>> 7)) _sub(r[0], _gf(), r[0]);
_mul(r[3], r[0], r[1]);
return true;
}
/// The size of an Ed25519 public key.
const ed25519PublicKeySize = 32;
/// The size of an Ed25519 signature.
const ed25519SignatureSize = 64;
// The canonical encodings of the eight points of small order of
// edwards25519, as smallOrder of internal/ed25519strict of datekeys-go: the
// identity, the point of order 2, the two of order 4 and the four of order
// 8. The tests compute them again.
final List<Uint8List> _smallOrder = [
Uint8List(32),
Uint8List(32)..[31] = 0x80,
Uint8List(32)..[0] = 0x01,
fromHex('26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05'),
fromHex('26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85'),
fromHex('c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a'),
fromHex('c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa'),
fromHex('ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f'),
];
/// The canonical encodings of the eight points of small order.
List<Uint8List> smallOrderPoints() => [
for (final p in _smallOrder) Uint8List.fromList(p),
];
/// Whether the 32 bytes [a] are a canonical encoding: their y, the low 255
/// bits, is below p = 2^255 - 19, and their sign bit is clear when y is 1 or
/// p - 1, the two values whose x is 0 (spec §29.9, rule 1). It does not tell
/// whether y belongs to a point of the curve. Go's ed25519strict.Canonical.
bool canonical(List<int> a) {
if (a.length != 32) return false;
final high = a[31] & 0x7f;
var ones = true;
for (var i = 1; i < 31; i++) {
if (a[i] != 0xff) {
ones = false;
break;
}
}
// y >= p: 7f ff…ff and a first byte of 0xed or more.
if (high == 0x7f && ones && a[0] >= 0xed) return false;
if (a[31] & 0x80 == 0) return true;
// x = 0: y = 1, 01 00…00, or y = p - 1, ec ff…ff 7f.
var zeros = high == 0;
for (var i = 1; i < 31; i++) {
if (a[i] != 0) {
zeros = false;
break;
}
}
final isOne = zeros && a[0] == 0x01;
final isMinusOne = high == 0x7f && ones && a[0] == 0xec;
return !isOne && !isMinusOne;
}
/// Whether the canonical encoding [a] is one of the eight points of small
/// order (spec §29.9, rule 2). Go's ed25519strict.SmallOrder.
bool smallOrder(List<int> a) {
if (a.length != 32) return false;
for (final s in _smallOrder) {
if (equalBytes(a, s)) return true;
}
return false;
}
/// Whether the canonical encoding [a] is a point of the curve: whether x² =
/// (y² - 1)/(d·y² + 1) has a solution modulo p (RFC 8032, 5.1.3). Go's
/// ed25519strict.OnCurve, with BigInt as Go uses math/big.
bool onCurve(List<int> a) {
if (a.length != 32) return false;
final b = Uint8List.fromList(a);
b[31] &= 0x7f;
final y = _bigOf(b);
final y2 = y * y;
final u = (y2 - BigInt.one) % _p;
final v = (_dBig * y2 + BigInt.one) % _p;
final x2 = u * v.modInverse(_p) % _p;
return x2 == BigInt.zero ||
x2.modPow((_p - BigInt.one) >> 1, _p) == BigInt.one;
}
/// Whether [sig] is a valid signature of [message] by the public key
/// [publicKey] under the strict profile of spec §29.9, as Go's
/// ed25519strict.Verify: [publicKey] canonical and not of small order, then
/// crypto/ed25519.Verify, which rejects sig[63] & 0xE0 != 0, S >= ℓ and A
/// not on the curve, and compares the encoding of [S]B - [k]A, k =
/// SHA-512(R || A || message) mod ℓ, with R: the equation without the
/// cofactor. A key or a signature of another length is not valid either.
bool verifyStrict(List<int> publicKey, List<int> message, List<int> sig) {
if (publicKey.length != ed25519PublicKeySize ||
sig.length != ed25519SignatureSize) {
return false;
}
if (!canonical(publicKey) || smallOrder(publicKey)) return false;
if (sig[63] & 0xe0 != 0) return false;
final pub = Uint8List.fromList(publicKey);
final negA = _point();
if (!_unpackNeg(negA, pub)) return false;
final s = _bigOf(Uint8List.fromList(sig.sublist(32)));
if (s >= ed25519Order) return false;
final r = Uint8List.fromList(sig.sublist(0, 32));
final digest = crypto.sha512.convert(concatBytes([r, pub, message])).bytes;
final k = _bigOf(Uint8List.fromList(digest)) % ed25519Order;
// [k](-A) + [S]B.
final p = _point();
_scalarMult(p, negA, _leOf(k, 32));
final q = _point();
final b = _point();
for (var i = 0; i < 4; i++) {
_set(b[i], _base[i]);
}
_scalarMult(q, b, _leOf(s, 32));
_padd(p, q);
final got = Uint8List(32);
_ppack(got, p);
return equalBytes(got, r);
}

@ -0,0 +1,258 @@
/// scrypt (RFC 7914), with Salsa20/8, BlockMix and ROMix as Go's
/// golang.org/x/crypto/scrypt: the key derivation of the scrypt stanza of
/// age, with which the file of an author key is encrypted (spec §29.12,
/// logN = 16).
///
/// The words are 32 bits: sums of two words are masked and rotations are of
/// 32-bit values, exact on the VM and when compiled to JavaScript. The
/// memory is 128 · r · N bytes in one Uint32List: 64 MiB for logN = 16 and
/// r = 8.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'sha256.dart';
const _mask32 = 0xffffffff;
/// The error of Go's scrypt.Key for parameters it refuses.
final class ScryptParameterException implements Exception {
/// An exception with Go's [message].
const ScryptParameterException(this.message);
/// The text of Go's error.
final String message;
@override
String toString() => message;
}
/// scrypt of [password] and [salt] with cost [n], a power of two above 1,
/// block size [r] and parallelism [p], [length] bytes: Go's scrypt.Key, with
/// its checks and error texts. The p blocks are mixed one after the other.
Uint8List scrypt(
List<int> password,
List<int> salt,
int n,
int r,
int p,
int length,
) {
if (n <= 1 || n & (n - 1) != 0) {
throw const ScryptParameterException(
'scrypt: N must be > 1 and a power of 2',
);
}
if (r <= 0 || p <= 0) {
throw const ScryptParameterException('scrypt: parameters must be > 0');
}
// Go's limits with maxInt = 2^53 - 1, the largest exact int of the web,
// instead of 2^63 - 1: memory runs out long before either.
const maxInt = 9007199254740991;
if (r * p >= 1 << 30 ||
r > maxInt ~/ 128 ~/ p ||
r > maxInt ~/ 256 ||
n > maxInt ~/ 128 ~/ r) {
throw const ScryptParameterException('scrypt: parameters are too large');
}
final blockWords = 32 * r;
final b = pbkdf2HmacSha256(password, salt, 1, p * 128 * r);
final xy = Uint32List(2 * blockWords);
final v = Uint32List(blockWords * n);
final tmp = Uint32List(16);
final x = Uint32List(16);
try {
for (var i = 0; i < p; i++) {
_smix(b, i * 128 * r, r, n, v, xy, tmp, x);
}
return pbkdf2HmacSha256(password, b, 1, length);
} finally {
b.fillRange(0, b.length, 0);
xy.fillRange(0, xy.length, 0);
v.fillRange(0, v.length, 0);
tmp.fillRange(0, 16, 0);
x.fillRange(0, 16, 0);
}
}
// ROMix of the block of b at offset, in place.
void _smix(
Uint8List b,
int offset,
int r,
int n,
Uint32List v,
Uint32List xy,
Uint32List tmp,
Uint32List x,
) {
final words = 32 * r;
for (var i = 0; i < words; i++) {
final o = offset + 4 * i;
xy[i] = b[o] | b[o + 1] << 8 | b[o + 2] << 16 | (b[o + 3] << 24 & _mask32);
}
// X is xy[0..words), Y is xy[words..2·words).
for (var i = 0; i < n; i += 2) {
v.setRange(i * words, (i + 1) * words, xy);
_blockMix(tmp, x, xy, 0, xy, words, r);
v.setRange((i + 1) * words, (i + 2) * words, xy, words);
_blockMix(tmp, x, xy, words, xy, 0, r);
}
// Integerify: the first word of the last 64-byte block, below N ≤ 2^30, so
// the second word of the little-endian integer never counts.
final last = (2 * r - 1) * 16;
for (var i = 0; i < n; i += 2) {
var j = xy[last] & (n - 1);
_xorBlock(xy, 0, v, j * words, words);
_blockMix(tmp, x, xy, 0, xy, words, r);
j = xy[words + last] & (n - 1);
_xorBlock(xy, words, v, j * words, words);
_blockMix(tmp, x, xy, words, xy, 0, r);
}
for (var i = 0; i < words; i++) {
final w = xy[i];
final o = offset + 4 * i;
b[o] = w & 0xff;
b[o + 1] = w >>> 8 & 0xff;
b[o + 2] = w >>> 16 & 0xff;
b[o + 3] = w >>> 24;
}
}
void _xorBlock(Uint32List dst, int d, Uint32List src, int s, int n) {
for (var i = 0; i < n; i++) {
dst[d + i] ^= src[s + i];
}
}
// BlockMix with Salsa20/8 of the 2r blocks of 16 words at in[inOff..], into
// out[outOff..]: the even blocks first, then the odd ones, as RFC 7914 and
// Go's blockMix.
void _blockMix(
Uint32List tmp,
Uint32List x,
Uint32List input,
int inOff,
Uint32List out,
int outOff,
int r,
) {
tmp.setRange(0, 16, input, inOff + (2 * r - 1) * 16);
for (var i = 0; i < 2 * r; i += 2) {
_salsaXor(tmp, x, input, inOff + i * 16, out, outOff + i * 8);
_salsaXor(tmp, x, input, inOff + i * 16 + 16, out, outOff + i * 8 + r * 16);
}
}
// tmp = Salsa20/8(tmp ^ in[inOff..+16]), also written to out[outOff..+16].
void _salsaXor(
Uint32List tmp,
Uint32List w,
Uint32List input,
int inOff,
Uint32List out,
int outOff,
) {
for (var i = 0; i < 16; i++) {
w[i] = tmp[i] ^ input[inOff + i];
}
var x0 = w[0], x1 = w[1], x2 = w[2], x3 = w[3];
var x4 = w[4], x5 = w[5], x6 = w[6], x7 = w[7];
var x8 = w[8], x9 = w[9], x10 = w[10], x11 = w[11];
var x12 = w[12], x13 = w[13], x14 = w[14], x15 = w[15];
for (var i = 0; i < 8; i += 2) {
// Columns.
var u = (x0 + x12) & _mask32;
x4 ^= (u << 7 & _mask32) | u >>> 25;
u = (x4 + x0) & _mask32;
x8 ^= (u << 9 & _mask32) | u >>> 23;
u = (x8 + x4) & _mask32;
x12 ^= (u << 13 & _mask32) | u >>> 19;
u = (x12 + x8) & _mask32;
x0 ^= (u << 18 & _mask32) | u >>> 14;
u = (x5 + x1) & _mask32;
x9 ^= (u << 7 & _mask32) | u >>> 25;
u = (x9 + x5) & _mask32;
x13 ^= (u << 9 & _mask32) | u >>> 23;
u = (x13 + x9) & _mask32;
x1 ^= (u << 13 & _mask32) | u >>> 19;
u = (x1 + x13) & _mask32;
x5 ^= (u << 18 & _mask32) | u >>> 14;
u = (x10 + x6) & _mask32;
x14 ^= (u << 7 & _mask32) | u >>> 25;
u = (x14 + x10) & _mask32;
x2 ^= (u << 9 & _mask32) | u >>> 23;
u = (x2 + x14) & _mask32;
x6 ^= (u << 13 & _mask32) | u >>> 19;
u = (x6 + x2) & _mask32;
x10 ^= (u << 18 & _mask32) | u >>> 14;
u = (x15 + x11) & _mask32;
x3 ^= (u << 7 & _mask32) | u >>> 25;
u = (x3 + x15) & _mask32;
x7 ^= (u << 9 & _mask32) | u >>> 23;
u = (x7 + x3) & _mask32;
x11 ^= (u << 13 & _mask32) | u >>> 19;
u = (x11 + x7) & _mask32;
x15 ^= (u << 18 & _mask32) | u >>> 14;
// Rows.
u = (x0 + x3) & _mask32;
x1 ^= (u << 7 & _mask32) | u >>> 25;
u = (x1 + x0) & _mask32;
x2 ^= (u << 9 & _mask32) | u >>> 23;
u = (x2 + x1) & _mask32;
x3 ^= (u << 13 & _mask32) | u >>> 19;
u = (x3 + x2) & _mask32;
x0 ^= (u << 18 & _mask32) | u >>> 14;
u = (x5 + x4) & _mask32;
x6 ^= (u << 7 & _mask32) | u >>> 25;
u = (x6 + x5) & _mask32;
x7 ^= (u << 9 & _mask32) | u >>> 23;
u = (x7 + x6) & _mask32;
x4 ^= (u << 13 & _mask32) | u >>> 19;
u = (x4 + x7) & _mask32;
x5 ^= (u << 18 & _mask32) | u >>> 14;
u = (x10 + x9) & _mask32;
x11 ^= (u << 7 & _mask32) | u >>> 25;
u = (x11 + x10) & _mask32;
x8 ^= (u << 9 & _mask32) | u >>> 23;
u = (x8 + x11) & _mask32;
x9 ^= (u << 13 & _mask32) | u >>> 19;
u = (x9 + x8) & _mask32;
x10 ^= (u << 18 & _mask32) | u >>> 14;
u = (x15 + x14) & _mask32;
x12 ^= (u << 7 & _mask32) | u >>> 25;
u = (x12 + x15) & _mask32;
x13 ^= (u << 9 & _mask32) | u >>> 23;
u = (x13 + x12) & _mask32;
x14 ^= (u << 13 & _mask32) | u >>> 19;
u = (x14 + x13) & _mask32;
x15 ^= (u << 18 & _mask32) | u >>> 14;
}
// A Uint32List keeps the low 32 bits of each sum.
tmp[0] = x0 + w[0];
tmp[1] = x1 + w[1];
tmp[2] = x2 + w[2];
tmp[3] = x3 + w[3];
tmp[4] = x4 + w[4];
tmp[5] = x5 + w[5];
tmp[6] = x6 + w[6];
tmp[7] = x7 + w[7];
tmp[8] = x8 + w[8];
tmp[9] = x9 + w[9];
tmp[10] = x10 + w[10];
tmp[11] = x11 + w[11];
tmp[12] = x12 + w[12];
tmp[13] = x13 + w[13];
tmp[14] = x14 + w[14];
tmp[15] = x15 + w[15];
out.setRange(outOff, outOff + 16, tmp);
}

@ -0,0 +1,423 @@
/// SHA-256 with a compression function of its own, and on it HMAC-SHA256,
/// HKDF-SHA256 (RFC 5869) and PBKDF2-HMAC-SHA256 (RFC 8018).
///
/// package:crypto has SHA-256 and HMAC, but its HMAC computes the states of
/// the two keys again for every message and allocates for every call. The
/// word key of spec §38.1 runs PBKDF2 with 600 000 iterations, two HMACs per
/// iteration: here the inner and outer states of the key are computed once,
/// and each iteration is two compressions over 32-bit words in buffers that
/// are reused, without bytes in between. The app still calls it in an
/// Isolate (docs/PLAN_dart.md, «Rendimiento»).
///
/// The arithmetic is exact on the VM and when compiled to JavaScript: words
/// are kept in 0..2^32-1, sums are of at most five words (below 2^35, exact
/// in a double), and every shift, rotation and mask is of a 32-bit value, so
/// that the 32-bit bit operators of the web give the same result as the
/// 64-bit ones of the VM.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
const _mask32 = 0xffffffff;
/// The size of a SHA-256 digest, of an HMAC-SHA256 tag and of a PRK of HKDF.
const sha256Size = 32;
/// The size of a SHA-256 block.
const sha256BlockSize = 64;
// The round constants of FIPS 180-4, 4.2.2.
final Uint32List _k = Uint32List.fromList(const [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, //
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
// The initial hash value of FIPS 180-4, 5.3.3.
const _iv = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, //
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
];
/// Compresses the 16 words of [w] (big-endian words of one block, w[0..15];
/// w[16..63] are scratch) into the eight words of [state].
void _compress(Uint32List state, Uint32List w) {
for (var t = 16; t < 64; t++) {
final x = w[t - 15];
final y = w[t - 2];
final s0 =
((x >>> 7) | (x << 25) & _mask32) ^
((x >>> 18) | (x << 14) & _mask32) ^
(x >>> 3);
final s1 =
((y >>> 17) | (y << 15) & _mask32) ^
((y >>> 19) | (y << 13) & _mask32) ^
(y >>> 10);
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) & _mask32;
}
var a = state[0];
var b = state[1];
var c = state[2];
var d = state[3];
var e = state[4];
var f = state[5];
var g = state[6];
var h = state[7];
for (var t = 0; t < 64; t++) {
final s1 =
((e >>> 6) | (e << 26) & _mask32) ^
((e >>> 11) | (e << 21) & _mask32) ^
((e >>> 25) | (e << 7) & _mask32);
// Ch(e, f, g) = (e & f) ^ (~e & g), with ~e as 32 bits on the VM too.
final ch = (e & f) ^ ((e ^ _mask32) & g);
final t1 = (h + s1 + ch + _k[t] + w[t]) & _mask32;
final s0 =
((a >>> 2) | (a << 30) & _mask32) ^
((a >>> 13) | (a << 19) & _mask32) ^
((a >>> 22) | (a << 10) & _mask32);
final maj = (a & b) ^ (a & c) ^ (b & c);
final t2 = (s0 + maj) & _mask32;
h = g;
g = f;
f = e;
e = (d + t1) & _mask32;
d = c;
c = b;
b = a;
a = (t1 + t2) & _mask32;
}
state[0] = (state[0] + a) & _mask32;
state[1] = (state[1] + b) & _mask32;
state[2] = (state[2] + c) & _mask32;
state[3] = (state[3] + d) & _mask32;
state[4] = (state[4] + e) & _mask32;
state[5] = (state[5] + f) & _mask32;
state[6] = (state[6] + g) & _mask32;
state[7] = (state[7] + h) & _mask32;
}
/// Loads the block of [bytes] at [offset] into w[0..15], big-endian.
void _load(Uint32List w, List<int> bytes, int offset) {
for (var i = 0; i < 16; i++) {
final o = offset + 4 * i;
w[i] =
bytes[o] << 24 & _mask32 |
bytes[o + 1] << 16 |
bytes[o + 2] << 8 |
bytes[o + 3];
}
}
/// Writes the eight words of [state] big-endian into [out] at [offset].
void _store(Uint32List state, Uint8List out, int offset) {
for (var i = 0; i < 8; i++) {
final v = state[i];
out[offset + 4 * i] = v >>> 24;
out[offset + 4 * i + 1] = v >>> 16 & 0xff;
out[offset + 4 * i + 2] = v >>> 8 & 0xff;
out[offset + 4 * i + 3] = v & 0xff;
}
}
/// An incremental SHA-256 (FIPS 180-4). After [finish] it must not be used
/// again.
final class Sha256 {
/// A hash of nothing yet.
Sha256() : _state = Uint32List.fromList(_iv);
// A hash that continues from [state] after [length] bytes, all of them
// whole blocks: the precomputed states of HMAC.
Sha256._from(Uint32List state, this._length)
: _state = Uint32List.fromList(state);
final Uint32List _state;
final Uint32List _w = Uint32List(64);
final Uint8List _block = Uint8List(sha256BlockSize);
int _buffered = 0;
// The number of bytes hashed: an int, exact up to 2^53 - 1, far beyond any
// input of this library.
int _length = 0;
/// Hashes [data], or its bytes from [start] to [end].
void add(List<int> data, [int start = 0, int? end]) {
final stop = end ?? data.length;
RangeError.checkValidRange(start, stop, data.length);
var i = start;
_length += stop - start;
if (_buffered > 0) {
while (_buffered < sha256BlockSize && i < stop) {
_block[_buffered++] = data[i++];
}
if (_buffered < sha256BlockSize) return;
_load(_w, _block, 0);
_compress(_state, _w);
_buffered = 0;
}
for (; i + sha256BlockSize <= stop; i += sha256BlockSize) {
_load(_w, data, i);
_compress(_state, _w);
}
while (i < stop) {
_block[_buffered++] = data[i++];
}
}
/// The digest of everything added: 32 bytes.
Uint8List finish() {
final out = Uint8List(sha256Size);
finishInto(out, 0);
return out;
}
/// Writes the digest into [out] at [offset].
void finishInto(Uint8List out, int offset) {
// The length in bits, as two 32-bit halves without a 64-bit shift.
final bits = _length * 8;
final high = bits ~/ 0x100000000;
final low = bits - high * 0x100000000;
_block[_buffered++] = 0x80;
if (_buffered > 56) {
_block.fillRange(_buffered, sha256BlockSize, 0);
_load(_w, _block, 0);
_compress(_state, _w);
_buffered = 0;
}
_block.fillRange(_buffered, 56, 0);
_load(_w, _block, 0);
_w[14] = high;
_w[15] = low;
_compress(_state, _w);
_store(_state, out, offset);
_block.fillRange(0, sha256BlockSize, 0);
_w.fillRange(0, 64, 0);
}
}
/// The SHA-256 digest of [data].
Uint8List sha256(List<int> data) => (Sha256()..add(data)).finish();
/// HMAC-SHA256 (RFC 2104) with a key fixed once: the states after the
/// blocks of the key XOR ipad and XOR opad are computed in the constructor,
/// so that each tag costs only the compressions of the message and two of
/// the padding. [wipe] clears them.
final class HmacSha256 {
/// The HMAC of [key], of any length: a key longer than a block is hashed
/// first, as RFC 2104 says.
HmacSha256(List<int> key) {
final k = Uint8List(sha256BlockSize);
if (key.length > sha256BlockSize) {
k.setAll(0, sha256(key));
} else {
k.setAll(0, key);
}
final w = Uint32List(64);
for (var i = 0; i < sha256BlockSize; i++) {
k[i] ^= 0x36;
}
_load(w, k, 0);
_inner.setAll(0, _iv);
_compress(_inner, w);
for (var i = 0; i < sha256BlockSize; i++) {
k[i] ^= 0x36 ^ 0x5c;
}
_load(w, k, 0);
_outer.setAll(0, _iv);
_compress(_outer, w);
k.fillRange(0, sha256BlockSize, 0);
w.fillRange(0, 64, 0);
}
final Uint32List _inner = Uint32List(8);
final Uint32List _outer = Uint32List(8);
/// The tag of [message]: 32 bytes.
Uint8List mac(List<int> message) {
final h = Sha256._from(_inner, sha256BlockSize)..add(message);
final inner = h.finish();
final o = Sha256._from(_outer, sha256BlockSize)..add(inner);
inner.fillRange(0, sha256Size, 0);
return o.finish();
}
/// The tag of the concatenation of [parts].
Uint8List macAll(Iterable<List<int>> parts) {
final h = Sha256._from(_inner, sha256BlockSize);
for (final p in parts) {
h.add(p);
}
final inner = h.finish();
final o = Sha256._from(_outer, sha256BlockSize)..add(inner);
inner.fillRange(0, sha256Size, 0);
return o.finish();
}
/// Clears the states of the key.
void wipe() {
_inner.fillRange(0, 8, 0);
_outer.fillRange(0, 8, 0);
}
}
/// The HMAC-SHA256 of [message] under [key].
Uint8List hmacSha256(List<int> key, List<int> message) {
final h = HmacSha256(key);
try {
return h.mac(message);
} finally {
h.wipe();
}
}
/// HKDF-Extract of RFC 5869 with SHA-256: the PRK of [ikm]. An empty or null
/// [salt] is the string of 32 zero bytes, as the RFC says and as Go's
/// hkdf.Extract does.
Uint8List hkdfExtract(List<int> ikm, List<int>? salt) {
final s = salt == null || salt.isEmpty ? Uint8List(sha256Size) : salt;
return hmacSha256(s, ikm);
}
/// HKDF-Expand of RFC 5869 with SHA-256: [length] bytes, at most 255 · 32,
/// from [prk] and [info].
Uint8List hkdfExpand(List<int> prk, List<int> info, int length) {
if (length < 0 || length > 255 * sha256Size) {
throw RangeError.range(length, 0, 255 * sha256Size, 'length');
}
final h = HmacSha256(prk);
final out = Uint8List(length);
var t = Uint8List(0);
try {
for (var i = 1, o = 0; o < length; i++) {
final next = h.macAll([
t,
info,
[i],
]);
t.fillRange(0, t.length, 0);
t = next;
final n = length - o < sha256Size ? length - o : sha256Size;
out.setRange(o, o + n, t);
o += n;
}
} finally {
t.fillRange(0, t.length, 0);
h.wipe();
}
return out;
}
/// HKDF-SHA256 of RFC 5869, Extract then Expand, as Go's hkdf.New read for
/// [length] bytes.
Uint8List hkdfSha256(
List<int> ikm,
List<int>? salt,
List<int> info,
int length,
) {
final prk = hkdfExtract(ikm, salt);
try {
return hkdfExpand(prk, info, length);
} finally {
prk.fillRange(0, prk.length, 0);
}
}
/// PBKDF2 of RFC 8018 with HMAC-SHA256: [length] bytes of [password] and
/// [salt] after [iterations] iterations, at least 1. Each iteration is two
/// compressions from the precomputed states of the key, over words; spec
/// §38.1 runs it with 600 000 iterations.
Uint8List pbkdf2HmacSha256(
List<int> password,
List<int> salt,
int iterations,
int length,
) {
if (iterations < 1) {
throw RangeError.range(iterations, 1, null, 'iterations');
}
if (length < 0) throw RangeError.range(length, 0, null, 'length');
final h = HmacSha256(password);
final out = Uint8List(length);
// The block of the inner and of the outer hash of a 32-byte message after
// the 64 bytes of the key: the message, 0x80, zeros and the length in bits,
// (64 + 32) · 8 = 768.
final w = Uint32List(64);
final u = Uint32List(8);
final acc = Uint32List(8);
final state = Uint32List(8);
final block = Uint8List(sha256Size);
try {
for (var i = 1, o = 0; o < length; i++) {
// U1 = HMAC(P, S || INT(i)), through the general path.
final u1 = h.macAll([
salt,
[i >>> 24 & 0xff, i >>> 16 & 0xff, i >>> 8 & 0xff, i & 0xff],
]);
_load16(u, u1);
u1.fillRange(0, sha256Size, 0);
acc.setAll(0, u);
for (var j = 1; j < iterations; j++) {
// The inner hash of U, then the outer hash of that.
_hmacWords(h._inner, u, w, state);
_hmacWords(h._outer, state, w, u);
for (var k = 0; k < 8; k++) {
acc[k] ^= u[k];
}
}
_store(acc, block, 0);
final n = length - o < sha256Size ? length - o : sha256Size;
out.setRange(o, o + n, block);
o += n;
}
} finally {
h.wipe();
w.fillRange(0, 64, 0);
u.fillRange(0, 8, 0);
acc.fillRange(0, 8, 0);
state.fillRange(0, 8, 0);
block.fillRange(0, sha256Size, 0);
}
return out;
}
// The eight big-endian words of a 32-byte digest.
void _load16(Uint32List words, Uint8List digest) {
for (var i = 0; i < 8; i++) {
words[i] =
digest[4 * i] << 24 & _mask32 |
digest[4 * i + 1] << 16 |
digest[4 * i + 2] << 8 |
digest[4 * i + 3];
}
}
// out = the compression, from the precomputed state [from], of the block of
// the eight words of [message] and the padding of a 96-byte input.
void _hmacWords(
Uint32List from,
Uint32List message,
Uint32List w,
Uint32List out,
) {
for (var i = 0; i < 8; i++) {
w[i] = message[i];
}
w[8] = 0x80000000;
for (var i = 9; i < 15; i++) {
w[i] = 0;
}
w[15] = 768;
for (var i = 0; i < 8; i++) {
out[i] = from[i];
}
_compress(out, w);
}

@ -0,0 +1,400 @@
// The primitives of stage 2 against test/vectors/primitives.json, whose
// expected values Go computed (tool/gen_primitive_vectors.go): SHA-256,
// HMAC, HKDF, PBKDF2, scrypt, ChaCha20, Poly1305, ChaCha20-Poly1305, X25519,
// strict Ed25519, Go's Base64 and age's Bech32. The vectors come from a Dart
// constant, so that these tests also run compiled to JavaScript, where the
// integers are doubles and the bit operators 32-bit; there the cases that
// would take too long (PBKDF2 at 600 000 iterations, scrypt with logN 16)
// are left out.
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:datekeys/datekeys.dart' show fromHex, toHex;
import 'package:datekeys/src/base64.dart';
import 'package:datekeys/src/bech32.dart';
import 'package:datekeys/src/chacha20poly1305.dart';
import 'package:datekeys/src/curve25519.dart';
import 'package:datekeys/src/scrypt.dart';
import 'package:datekeys/src/sha256.dart';
import 'package:test/test.dart';
import 'vectors/primitives.g.dart';
/// Whether the tests run compiled to JavaScript.
const isWeb = identical(0, 0.0);
final Map<String, Object?> vectors =
jsonDecode(primitivesJson) as Map<String, Object?>;
List<Map<String, Object?>> cases(String name) =>
(vectors[name]! as List).cast<Map<String, Object?>>();
Uint8List hx(Object? v) => fromHex(v! as String);
/// Whether a case is cheap enough for the platform.
bool affordable(Map<String, Object?> c) => !isWeb || c['node'] != false;
void main() {
group('SHA-256 and HMAC', () {
test('the digests of Go', () {
for (final c in cases('sha256')) {
expect(toHex(sha256(hx(c['message']))), c['digest']);
}
});
test('the digest of a message added in pieces', () {
final r = Random(1);
for (var n = 0; n < 300; n += 7) {
final m = Uint8List.fromList([
for (var i = 0; i < n; i++) r.nextInt(256),
]);
final h = Sha256();
for (var i = 0; i < n;) {
final step = 1 + r.nextInt(70);
final end = min(n, i + step);
h.add(m, i, end);
i = end;
}
expect(toHex(h.finish()), crypto.sha256.convert(m).toString());
}
});
test('the tags of Go, and those of package:crypto', () {
for (final c in cases('hmac_sha256')) {
expect(toHex(hmacSha256(hx(c['key']), hx(c['message']))), c['mac']);
}
final r = Random(2);
for (var n = 0; n < 200; n += 13) {
final k = [for (var i = 0; i < n; i++) r.nextInt(256)];
final m = [for (var i = 0; i < 2 * n; i++) r.nextInt(256)];
final h = HmacSha256(k);
expect(
toHex(h.mac(m)),
crypto.Hmac(crypto.sha256, k).convert(m).toString(),
);
expect(
toHex(h.macAll([m.sublist(0, n), m.sublist(n)])),
toHex(h.mac(m)),
);
}
});
});
test('HKDF-SHA256: RFC 5869 and the labels of age', () {
for (final c in cases('hkdf_sha256')) {
final salt = c['salt'] == null ? null : hx(c['salt']);
expect(
toHex(
hkdfSha256(hx(c['ikm']), salt, hx(c['info']), c['length']! as int),
),
c['okm'],
reason: c['name'] as String?,
);
}
expect(() => hkdfExpand(Uint8List(32), [], 255 * 32 + 1), throwsRangeError);
});
test(
'PBKDF2-HMAC-SHA256${isWeb ? ', without the 600 000 iterations' : ''}',
() {
var run = 0;
for (final c in cases('pbkdf2_sha256').where(affordable)) {
final k = pbkdf2HmacSha256(
hx(c['password']),
hx(c['salt']),
c['iterations']! as int,
c['length']! as int,
);
expect(toHex(k), c['key'], reason: c['name'] as String?);
run++;
}
expect(run, isWeb ? 6 : 7);
expect(() => pbkdf2HmacSha256([1], [2], 0, 32), throwsRangeError);
},
);
test(
'scrypt: RFC 7914 and the parameters of age${isWeb ? ', without logN 16' : ''}',
() {
var run = 0;
for (final c in cases('scrypt').where(affordable)) {
final k = scrypt(
hx(c['password']),
hx(c['salt']),
c['n']! as int,
c['r']! as int,
c['p']! as int,
c['length']! as int,
);
expect(toHex(k), c['key'], reason: c['name'] as String?);
run++;
}
expect(run, isWeb ? 6 : 7);
for (final c in cases('scrypt_errors')) {
expect(
() => scrypt(
[1],
[2],
c['n']! as int,
c['r']! as int,
c['p']! as int,
32,
),
throwsA(
isA<ScryptParameterException>().having(
(e) => e.message,
'message',
c['error'],
),
),
);
}
},
);
group('ChaCha20-Poly1305', () {
test('ChaCha20: RFC 8439 and the end of the counter', () {
for (final c in cases('chacha20')) {
final data = hx(c['input']);
ChaCha20(
hx(c['key']),
hx(c['nonce']),
c['counter']! as int,
).xorInPlace(data);
expect(toHex(data), c['output'], reason: c['name'] as String?);
}
// A block past counter 2^32 - 1 is refused, as Go refuses it.
final s = ChaCha20(Uint8List(32), Uint8List(12), 0xffffffff)
..keystream(64);
expect(() => s.keystream(1), throwsStateError);
});
test('Poly1305: RFC 8439 and keys and messages that reach p', () {
for (final c in cases('poly1305')) {
expect(
toHex(poly1305(hx(c['key']), hx(c['message']))),
c['tag'],
reason: c['name'] as String?,
);
// The same tag with the message in pieces of every size.
final m = hx(c['message']);
for (final step in [1, 7, 16, 33]) {
final p = Poly1305(hx(c['key']));
for (var i = 0; i < m.length; i += step) {
p.add(m, i, min(m.length, i + step));
}
expect(toHex(p.finish()), c['tag']);
}
}
});
test('the AEAD: RFC 8439 and Go, both ways, and every tampering fails', () {
for (final c in cases('chacha20poly1305')) {
final key = hx(c['key']);
final nonce = hx(c['nonce']);
final aad = hx(c['aad']);
final ct = hx(c['ciphertext']);
expect(
toHex(chacha20Poly1305Seal(key, nonce, hx(c['plaintext']), aad)),
c['ciphertext'],
reason: c['name'] as String?,
);
expect(
toHex(chacha20Poly1305Open(key, nonce, ct, aad)!),
c['plaintext'],
);
for (var i = 0; i < ct.length; i += 1 + ct.length ~/ 9) {
final bad = Uint8List.fromList(ct)..[i] ^= 0x10;
expect(chacha20Poly1305Open(key, nonce, bad, aad), isNull);
}
expect(
chacha20Poly1305Open(key, nonce, Uint8List.sublistView(ct, 1), aad),
isNull,
);
final badAad = [...aad, 0];
expect(chacha20Poly1305Open(key, nonce, ct, badAad), isNull);
}
expect(
chacha20Poly1305Open(Uint8List(32), Uint8List(12), Uint8List(15)),
isNull,
);
});
test('constantTimeEquals', () {
expect(constantTimeEquals([1, 2], [1, 2]), isTrue);
expect(constantTimeEquals([1, 2], [1, 3]), isFalse);
expect(constantTimeEquals([1, 2], [1]), isFalse);
expect(constantTimeEquals([], []), isTrue);
});
});
group('X25519', () {
test('RFC 7748, BoringSSL, and the points of low order', () {
for (final c in cases('x25519')) {
final scalar = hx(c['scalar']);
final u = hx(c['u']);
expect(
toHex(x25519(scalar, u)),
c['output'],
reason: c['name'] as String?,
);
if (c['error'] != null) {
expect(
() => x25519Agree(scalar, u),
throwsA(
isA<X25519LowOrderException>().having(
(e) => e.message,
'message',
c['error'],
),
),
reason: c['name'] as String?,
);
} else {
expect(toHex(x25519Agree(scalar, u)), c['output']);
}
}
});
test(
'the iterated function of RFC 7748${isWeb ? ', once' : ', 1000 times'}',
() {
final it = vectors['x25519_iterated']! as Map<String, Object?>;
var k = Uint8List(32)..[0] = 9;
var u = Uint8List.fromList(k);
for (var i = 1; i <= (isWeb ? 1 : 1000); i++) {
final out = x25519(k, u);
u = k;
k = out;
if (i == 1) expect(toHex(k), it['1']);
}
if (!isWeb) expect(toHex(k), it['1000']);
},
);
test('lengths other than 32 are refused', () {
expect(() => x25519(Uint8List(31), Uint8List(32)), throwsArgumentError);
expect(() => x25519(Uint8List(32), Uint8List(33)), throwsArgumentError);
});
});
group('Ed25519, strict', () {
test('sign.input of Go, its mutations, S + ℓ and the small order', () {
for (final c in cases('ed25519')) {
expect(
verifyStrict(
hx(c['public_key']),
hx(c['message']),
hx(c['signature']),
),
c['valid'],
reason: c['name'] as String?,
);
}
});
test('Canonical, OnCurve and SmallOrder of ed25519strict', () {
for (final c in cases('ed25519_encodings')) {
final a = hx(c['encoding']);
expect(canonical(a), c['canonical'], reason: '${c['name']}');
expect(onCurve(a), c['on_curve'], reason: '${c['name']}');
expect(smallOrder(a), c['small_order'], reason: '${c['name']}');
}
});
test('lengths other than 32 and 64 are not valid', () {
final c = cases('ed25519').first;
final pub = hx(c['public_key']);
final sig = hx(c['signature']);
final m = hx(c['message']);
expect(verifyStrict(pub, m, sig), isTrue);
expect(verifyStrict(pub.sublist(1), m, sig), isFalse);
expect(verifyStrict(pub, m, sig.sublist(1)), isFalse);
expect(verifyStrict(pub, m, [...sig, 0]), isFalse);
expect(canonical(Uint8List(31)), isFalse);
expect(onCurve(Uint8List(33)), isFalse);
expect(smallOrder(Uint8List(31)), isFalse);
});
test('the eight points of small order are canonical points', () {
// primitives.json checks smallOrder against Go's table; here, that the
// table holds eight canonical points of the curve.
final points = smallOrderPoints();
expect(points, hasLength(8));
for (final p in points) {
expect(canonical(p), isTrue);
expect(onCurve(p), isTrue);
expect(smallOrder(p), isTrue);
}
});
});
test("Go's Base64, with the offsets of its errors", () {
for (final c in cases('base64')) {
final input = hx(c['input']);
Object result;
try {
result = toHex(
goBase64Decode(
input,
url: c['url']! as bool,
padded: c['padded']! as bool,
strict: c['strict']! as bool,
),
);
} on Base64Exception catch (e) {
result = e.message;
}
expect(
result,
c['error'] ?? c['output'],
reason: '${c['encoding']}: ${c['input']}',
);
if (c['error'] == null &&
!(c['padded']! as bool) &&
c['strict']! as bool) {
// A canonical unpadded encoding encodes back to itself.
final s = String.fromCharCodes(input);
if (!s.contains('\n') && !s.contains('\r')) {
expect(
goBase64Encode(
fromHex(c['output']! as String),
url: c['url']! as bool,
padded: false,
),
s,
);
}
}
}
});
test("age's Bech32, with its error texts", () {
for (final c in cases('bech32')) {
Object result;
if (c['op'] == 'decode') {
try {
final d = bech32Decode(c['input']! as String);
result = '${d.hrp} ${toHex(d.data)}';
} on Bech32Exception catch (e) {
result = e.message;
}
expect(
result,
c['error'] ?? '${c['hrp']} ${c['data']}',
reason: c['input'] as String?,
);
} else {
try {
result = bech32Encode(c['hrp']! as String, hx(c['data']));
} on Bech32Exception catch (e) {
result = e.message;
}
expect(result, c['error'] ?? c['output'], reason: '${c['hrp']}');
}
}
});
}

@ -0,0 +1,18 @@
// The copy of test/vectors/primitives.json that primitives_test.dart reads,
// a Dart constant for the tests compiled to JavaScript, is the JSON file
// byte for byte: tool/gen_primitive_vectors.go writes both.
@TestOn('vm')
library;
import 'dart:io';
import 'package:test/test.dart';
import 'vectors/primitives.g.dart';
void main() {
test('primitives.g.dart holds primitives.json', () {
final file = File('test/vectors/primitives.json').readAsStringSync();
expect(primitivesJson, file);
});
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,774 @@
//go:build ignore
// gen_primitive_vectors writes test/vectors/primitives.json: the vectors of
// the primitives of stage 2 of datekeys-dart, every expected value computed
// here with the Go libraries that datekeys-go and filippo.io/age use, never
// written by hand. The inputs are those of the RFCs (5869, 7748, 7914, 8032,
// 8439), taken from the tests and test data of Go and golang.org/x/crypto in
// the module cache where they are there, plus edge cases and seeded random
// ones.
//
// It needs the module context of datekeys-go, for golang.org/x/crypto
// v0.57.0 and the codec/bech32 and profile packages, and changes nothing
// there:
//
// cd ../datekeys-go && go run ../datekeys-dart/tool/gen_primitive_vectors.go -out ../datekeys-dart/test/vectors
//
// The output is deterministic: running it again writes the same bytes.
package main
import (
"bufio"
"bytes"
"compress/gzip"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"math/big"
"math/rand/v2"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/poly1305"
"golang.org/x/crypto/scrypt"
"g.activething.com/go/DateKeys/codec/bech32"
"g.activething.com/go/DateKeys/profile"
)
type obj = map[string]any
var rng = rand.New(rand.NewChaCha8([32]byte([]byte("datekeys-dart stage 2 primitives"))))
func randBytes(n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(rng.Uint32())
}
return b
}
func seq(from, n int) []byte {
b := make([]byte, n)
for i := range b {
b[i] = byte(from + i)
}
return b
}
func h(b []byte) string { return hex.EncodeToString(b) }
func mustHex(s string) []byte {
b, err := hex.DecodeString(s)
if err != nil {
log.Fatal(err)
}
return b
}
func modDir(path string) string {
out, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", path).Output()
if err != nil {
log.Fatalf("go list %s: %v", path, err)
}
return strings.TrimSpace(string(out))
}
// byteArrays returns the [32]byte or []byte literals {0x.., ...} after the
// first occurrence of marker in a Go source file.
func byteArrays(src, marker, end string) [][]byte {
i := strings.Index(src, marker)
if i < 0 {
log.Fatalf("marker %q not found", marker)
}
src = src[i:]
if j := strings.Index(src, end); j >= 0 {
src = src[:j]
}
var out [][]byte
for _, m := range regexp.MustCompile(`\{(0x[0-9a-fA-F]+(?:,\s*0x[0-9a-fA-F]+)*),?\s*\}`).FindAllStringSubmatch(src, -1) {
var b []byte
for _, f := range strings.Split(m[1], ",") {
v, err := strconv.ParseUint(strings.TrimSpace(f), 0, 8)
if err != nil {
log.Fatal(err)
}
b = append(b, byte(v))
}
out = append(out, b)
}
return out
}
func main() {
outDir := flag.String("out", "", "directory of the vectors")
flag.Parse()
if *outDir == "" {
log.Fatal("-out is required")
}
xcrypto := modDir("golang.org/x/crypto")
doc := obj{
"description": "Vectors of the primitives of stage 2 of datekeys-dart. Every expected value is computed by tool/gen_primitive_vectors.go with Go " + runtime.Version() + ", golang.org/x/crypto v0.57.0 and codec/bech32 of datekeys-go; the inputs are those of the RFCs, taken from the tests of Go and x/crypto where they are, plus edge cases and seeded random ones. Binary values are lowercase hex. `node` marks the cases cheap enough to run compiled to JavaScript.",
"generator": "tool/gen_primitive_vectors.go",
}
// SHA-256 and HMAC-SHA256: around the block and padding boundaries.
var shaCases, hmacCases []obj
for _, n := range []int{0, 1, 3, 55, 56, 57, 63, 64, 65, 119, 120, 128, 1000} {
m := randBytes(n)
d := sha256.Sum256(m)
shaCases = append(shaCases, obj{"message": h(m), "digest": h(d[:])})
}
for _, kn := range []int{0, 1, 20, 32, 63, 64, 65, 131} {
for _, mn := range []int{0, 32, 100} {
k, m := randBytes(kn), randBytes(mn)
mac := hmac.New(sha256.New, k)
mac.Write(m)
hmacCases = append(hmacCases, obj{"key": h(k), "message": h(m), "mac": h(mac.Sum(nil))})
}
}
doc["sha256"] = shaCases
doc["hmac_sha256"] = hmacCases
// HKDF-SHA256: RFC 5869 A.1 to A.3 (the inputs of hkdf_test.go of
// x/crypto), a nil salt, and the age labels.
hkdfInputs := []struct {
name string
ikm, salt, info []byte
length int
}{
{"RFC 5869 A.1", bytes.Repeat([]byte{0x0b}, 22), seq(0x00, 13), seq(0xf0, 10), 42},
{"RFC 5869 A.2", seq(0x00, 80), seq(0x60, 80), seq(0xb0, 80), 82},
{"RFC 5869 A.3", bytes.Repeat([]byte{0x0b}, 22), []byte{}, []byte{}, 42},
{"nil salt, age header label", randBytes(16), nil, []byte("header"), 32},
{"age payload label", randBytes(16), randBytes(16), []byte("payload"), 32},
{"255 blocks", randBytes(32), randBytes(32), randBytes(10), 255 * 32},
}
var hkdfCases []obj
for _, c := range hkdfInputs {
out := make([]byte, c.length)
if _, err := io.ReadFull(hkdf.New(sha256.New, c.ikm, c.salt, c.info), out); err != nil {
log.Fatal(err)
}
salt := any(h(c.salt))
if c.salt == nil {
salt = nil
}
hkdfCases = append(hkdfCases, obj{"name": c.name, "ikm": h(c.ikm), "salt": salt, "info": h(c.info), "length": c.length, "okm": h(out)})
}
doc["hkdf_sha256"] = hkdfCases
// PBKDF2-HMAC-SHA256: the inputs of RFC 6070 (written for SHA-1) with
// SHA-256, a password longer than a block, and the word key of spec
// §38.1 with 600 000 iterations.
qn := profile.Quicknet()
wordSalt := "DateKeys llave de palabras v2|" + qn.ChainHashHex() + "|1000|000102030405060708090a0b0c0d0e0f"
pbkdf2Inputs := []struct {
name, password, salt string
iter, length int
node bool
}{
{"RFC 6070 inputs, c = 1", "password", "salt", 1, 32, true},
{"RFC 6070 inputs, c = 2", "password", "salt", 2, 32, true},
{"RFC 6070 inputs, c = 4096", "password", "salt", 4096, 32, true},
{"RFC 6070 inputs, 40 bytes", "passwordPASSWORDpassword", "saltSALTsaltSALTsaltSALTsaltSALTsalt", 4096, 40, true},
{"RFC 6070 inputs, NUL", "pass\x00word", "sa\x00lt", 4096, 16, true},
{"a password of 100 bytes, 3 blocks", strings.Repeat("0123456789", 10), "salt", 3, 70, true},
{"spec §38.1: perro luna casa verde tren mar", "perro luna casa verde tren mar", wordSalt, 600000, 32, false},
}
var pbkdf2Cases []obj
for _, c := range pbkdf2Inputs {
k := pbkdf2.Key([]byte(c.password), []byte(c.salt), c.iter, c.length, sha256.New)
pbkdf2Cases = append(pbkdf2Cases, obj{"name": c.name, "password": h([]byte(c.password)), "salt": h([]byte(c.salt)), "iterations": c.iter, "length": c.length, "key": h(k), "node": c.node})
}
doc["pbkdf2_sha256"] = pbkdf2Cases
// scrypt: RFC 7914 §12 (the inputs of scrypt_test.go of x/crypto, but
// for N = 2^20, 1 GiB), and the parameters of age with logN 10 and 16.
scryptInputs := []struct {
name, password, salt string
n, r, p, length int
node bool
}{
{"RFC 7914 §12, N = 16", "", "", 16, 1, 1, 64, true},
{"RFC 7914 §12, N = 1024, p = 16", "password", "NaCl", 1024, 8, 16, 64, true},
{"RFC 7914 §12, N = 16384", "pleaseletmein", "SodiumChloride", 16384, 8, 1, 64, true},
{"N = 2, r = 1, p = 1", "p", "s", 2, 1, 1, 32, true},
{"N = 4, r = 2, p = 3", "password", "salt", 4, 2, 3, 70, true},
{"age, logN = 10", "passphrase", "age-encryption.org/v1/scrypt" + string(seq(0, 16)), 1 << 10, 8, 1, 32, true},
{"age, logN = 16 (spec §29.12)", "passphrase", "age-encryption.org/v1/scrypt" + string(seq(16, 16)), 1 << 16, 8, 1, 32, false},
}
var scryptCases []obj
for _, c := range scryptInputs {
k, err := scrypt.Key([]byte(c.password), []byte(c.salt), c.n, c.r, c.p, c.length)
if err != nil {
log.Fatal(err)
}
scryptCases = append(scryptCases, obj{"name": c.name, "password": h([]byte(c.password)), "salt": h([]byte(c.salt)), "n": c.n, "r": c.r, "p": c.p, "length": c.length, "key": h(k), "node": c.node})
}
var scryptErrors []obj
for _, c := range []struct{ n, r, p int }{{1, 8, 1}, {0, 8, 1}, {3, 8, 1}, {1 << 10, 0, 1}, {1 << 10, 8, 0}, {1 << 10, 1 << 20, 1 << 10}} {
_, err := scrypt.Key([]byte("p"), []byte("s"), c.n, c.r, c.p, 32)
scryptErrors = append(scryptErrors, obj{"n": c.n, "r": c.r, "p": c.p, "error": err.Error()})
}
doc["scrypt"] = scryptCases
doc["scrypt_errors"] = scryptErrors
// ChaCha20 (RFC 8439 2.3.2 and 2.4.2), Poly1305 (2.5.2 and edge keys)
// and ChaCha20-Poly1305 (2.8.2 and seeded random cases).
vecSrc, err := os.ReadFile(filepath.Join(xcrypto, "chacha20poly1305", "chacha20poly1305_vectors_test.go"))
if err != nil {
log.Fatal(err)
}
// The plaintext of RFC 8439 2.4.2 and 2.8.2, as the vectors of x/crypto
// hold it.
m := regexp.MustCompile(`"(4c616469657320616e642047656e746c656d656e[0-9a-f]*)"`).FindSubmatch(vecSrc)
if m == nil {
log.Fatal("the sunscreen plaintext is not in the vectors of x/crypto")
}
sunscreen := mustHex(string(m[1]))
var chachaCases []obj
for _, c := range []struct {
name string
key, nonce []byte
counter uint32
length int
plaintext []byte
}{
{"RFC 8439 2.3.2", seq(0, 32), mustHex("000000090000004a00000000"), 1, 64, nil},
{"RFC 8439 2.4.2", seq(0, 32), mustHex("000000000000004a00000000"), 1, 0, sunscreen},
{"counter 0, 3 blocks and a half", randBytes(32), randBytes(12), 0, 224, nil},
{"counter near 2^32", randBytes(32), randBytes(12), 0xfffffffe, 128, nil},
} {
s, err := chacha20.NewUnauthenticatedCipher(c.key, c.nonce)
if err != nil {
log.Fatal(err)
}
s.SetCounter(c.counter)
in := c.plaintext
if in == nil {
in = make([]byte, c.length)
}
out := make([]byte, len(in))
s.XORKeyStream(out, in)
chachaCases = append(chachaCases, obj{"name": c.name, "key": h(c.key), "nonce": h(c.nonce), "counter": c.counter, "input": h(in), "output": h(out)})
}
doc["chacha20"] = chachaCases
var polyCases []obj
addPoly := func(name string, key, msg []byte) {
var k [32]byte
copy(k[:], key)
var tag [16]byte
poly1305.Sum(&tag, msg, &k)
polyCases = append(polyCases, obj{"name": name, "key": h(key), "message": h(msg), "tag": h(tag[:])})
}
addPoly("RFC 8439 2.5.2", mustHex("85d6be7857556d337f4452fe42d506a80103808afb0db2fd4abff6af4149f51b"), []byte("Cryptographic Forum Research Group"))
ff := bytes.Repeat([]byte{0xff}, 32)
for _, n := range []int{0, 1, 15, 16, 17, 31, 32, 33, 64, 100, 257} {
addPoly(fmt.Sprintf("r and s all ones, %d bytes of 0xff", n), ff, bytes.Repeat([]byte{0xff}, n))
}
for _, n := range []int{16, 48} {
// r = 2 · (p - 5)… : a key whose r makes h approach p.
k := append(bytes.Repeat([]byte{0xff}, 16), make([]byte, 16)...)
addPoly(fmt.Sprintf("s zero, %d bytes of 0xff", n), k, bytes.Repeat([]byte{0xff}, n))
addPoly(fmt.Sprintf("s all ones, %d zero bytes", n), append(make([]byte, 16), bytes.Repeat([]byte{0xff}, 16)...), make([]byte, n))
}
// r = 1 and s = 0: the tag is the sum of the blocks, so that blocks of
// 0xff take h across p.
r1 := append([]byte{1}, make([]byte, 31)...)
for _, n := range []int{1, 2, 3} {
addPoly(fmt.Sprintf("r = 1, %d blocks of 0xff", n), r1, bytes.Repeat([]byte{0xff}, 16*n))
}
for i := 0; i < 24; i++ {
addPoly(fmt.Sprintf("random %d", i), randBytes(32), randBytes(int(rng.Uint32()%200)))
}
doc["poly1305"] = polyCases
var aeadCases []obj
addAEAD := func(name string, key, nonce, aad, pt []byte) {
a, err := chacha20poly1305.New(key)
if err != nil {
log.Fatal(err)
}
aeadCases = append(aeadCases, obj{"name": name, "key": h(key), "nonce": h(nonce), "aad": h(aad), "plaintext": h(pt), "ciphertext": h(a.Seal(nil, nonce, pt, aad))})
}
addAEAD("RFC 8439 2.8.2", seq(0x80, 32), mustHex("070000004041424344454647"), mustHex("50515253c0c1c2c3c4c5c6c7"), sunscreen)
addAEAD("age: a file key under a zero nonce", randBytes(32), make([]byte, 12), nil, randBytes(16))
for _, n := range []int{0, 1, 15, 16, 17, 63, 64, 65, 127, 128, 129, 300, 1000} {
addAEAD(fmt.Sprintf("random, %d bytes", n), randBytes(32), randBytes(12), randBytes(int(rng.Uint32()%40)), randBytes(n))
}
doc["chacha20poly1305"] = aeadCases
// X25519: RFC 7748 5.2 and 6.1, the iterated function from u = 9, the
// BoringSSL vectors and the points of low order of x/crypto's tests.
cvSrc, err := os.ReadFile(filepath.Join(xcrypto, "curve25519", "vectors_test.go"))
if err != nil {
log.Fatal(err)
}
src := string(cvSrc)
lowOrder := byteArrays(src, "var lowOrderPoints", "// testVectors")
tv := byteArrays(src, "var testVectors", "\n}\n")
if len(lowOrder) != 7 || len(tv)%3 != 0 || len(tv) == 0 {
log.Fatalf("unexpected vectors: %d low order, %d arrays", len(lowOrder), len(tv))
}
var xCases []obj
addX := func(name string, scalar, u []byte) {
out, err := curve25519.X25519(scalar, u)
c := obj{"name": name, "scalar": h(scalar), "u": h(u)}
if err != nil {
c["error"] = err.Error()
// The value of the function itself, before the check.
var dst, s, p [32]byte
copy(s[:], scalar)
copy(p[:], u)
curve25519.ScalarMult(&dst, &s, &p)
c["output"] = h(dst[:])
} else {
c["output"] = h(out)
}
xCases = append(xCases, c)
}
addX("RFC 7748 5.2, first", mustHex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4"), mustHex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c"))
addX("RFC 7748 5.2, second", mustHex("4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d"), mustHex("e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493"))
alice := mustHex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
addX("RFC 7748 6.1, Alice's public key", alice, curve25519.Basepoint)
addX("RFC 7748 6.1, the shared secret", alice, mustHex("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"))
for i := 0; i < len(tv); i += 3 {
addX(fmt.Sprintf("BoringSSL %d", i/3), tv[i], tv[i+1])
}
for i, lo := range lowOrder {
addX(fmt.Sprintf("low order %d", i), randBytes(32), lo)
hi := slices.Clone(lo)
hi[31] |= 0x80
addX(fmt.Sprintf("low order %d, bit 255 set", i), randBytes(32), hi)
}
// u not below p: reduced, and bit 255 ignored.
for _, d := range []int{0, 1, 2, 18, 19, 20, 30} {
p := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
v := new(big.Int).Add(p, big.NewInt(int64(d)))
v.Mod(v, new(big.Int).Lsh(big.NewInt(1), 255))
le := make([]byte, 32)
v.FillBytes(le)
slices.Reverse(le)
addX(fmt.Sprintf("u = p + %d mod 2^255", d), randBytes(32), le)
}
for i := 0; i < 8; i++ {
addX(fmt.Sprintf("random %d", i), randBytes(32), randBytes(32))
}
doc["x25519"] = xCases
iter := func(n int) string {
k := append([]byte{9}, make([]byte, 31)...)
u := slices.Clone(k)
for i := 0; i < n; i++ {
out, err := curve25519.X25519(k, u)
if err != nil {
log.Fatal(err)
}
u, k = k, out
}
return h(k)
}
doc["x25519_iterated"] = obj{"description": "RFC 7748 5.2: k = u = 9, then k, u = X25519(k, u), k n times", "1": iter(1), "1000": iter(1000)}
// Ed25519: the first 64 lines of sign.input of Go's crypto/ed25519 (SUPERCOP;
// the first three are RFC 8032 7.1 TEST 1 to 3), checked here with
// crypto/ed25519 and the strict profile.
f, err := os.Open(filepath.Join(runtime.GOROOT(), "src", "crypto", "ed25519", "testdata", "sign.input.gz"))
if err != nil {
log.Fatal(err)
}
gz, err := gzip.NewReader(f)
if err != nil {
log.Fatal(err)
}
var edCases []obj
sc := bufio.NewScanner(gz)
sc.Buffer(make([]byte, 1<<20), 1<<20)
for line := 0; line < 64 && sc.Scan(); line++ {
parts := strings.Split(sc.Text(), ":")
pub, msg, sm := mustHex(parts[1]), mustHex(parts[2]), mustHex(parts[3])
sig := sm[:64]
priv := ed25519.NewKeyFromSeed(mustHex(parts[0])[:32])
if !bytes.Equal(ed25519.Sign(priv, msg), sig) || !ed25519.Verify(pub, msg, sig) {
log.Fatalf("sign.input line %d does not verify", line)
}
edCases = append(edCases, obj{"name": fmt.Sprintf("sign.input line %d", line), "public_key": h(pub), "message": h(msg), "signature": h(sig), "valid": strictVerify(pub, msg, sig), "stdlib": true})
if line%8 == 0 {
// Mutations of every eighth line: the result of the strict
// profile and of crypto/ed25519.
for _, mut := range []struct {
what string
which int
pos int
}{{"R", 1, 0}, {"S", 1, 32}, {"S top byte", 1, 63}, {"A", 0, 5}, {"message", 2, 0}} {
p2, m2, s2 := slices.Clone(pub), slices.Clone(msg), slices.Clone(sig)
target := [][]byte{p2, s2, m2}[mut.which]
if len(target) == 0 {
continue
}
target[mut.pos%len(target)] ^= 1 << (line % 8)
edCases = append(edCases, obj{"name": fmt.Sprintf("sign.input line %d, %s changed", line, mut.what), "public_key": h(p2), "message": h(m2), "signature": h(s2), "valid": strictVerify(p2, m2, s2), "stdlib": ed25519.Verify(p2, m2, s2)})
}
}
}
if err := sc.Err(); err != nil {
log.Fatal(err)
}
// The copy of the strict profile below gives the results of
// testdata/vectors/ed25519_strict.json of datekeys-go.
var strictFile struct {
Vectors []struct {
Name, Message, PublicKey, Signature string
Valid bool
}
}
raw, err := os.ReadFile(filepath.Join("testdata", "vectors", "ed25519_strict.json"))
if err != nil {
log.Fatal(err)
}
if err := json.Unmarshal(bytes.ReplaceAll(raw, []byte(`"public_key"`), []byte(`"publickey"`)), &strictFile); err != nil {
log.Fatal(err)
}
for _, v := range strictFile.Vectors {
if strictVerify(mustHex(v.PublicKey), mustHex(v.Message), mustHex(v.Signature)) != v.Valid {
log.Fatalf("the copy of ed25519strict disagrees on %q", v.Name)
}
}
if len(strictFile.Vectors) == 0 {
log.Fatal("no vectors in ed25519_strict.json")
}
// S + ℓ and S + 2^253 on a valid signature, and keys of small order.
l := new(big.Int).Add(new(big.Int).Lsh(big.NewInt(1), 252), mustBig("27742317777372353535851937790883648493"))
seed := randBytes(32)
priv := ed25519.NewKeyFromSeed(seed)
pub := priv.Public().(ed25519.PublicKey)
msg := []byte("DateKeys")
sig := ed25519.Sign(priv, msg)
for _, add := range []*big.Int{l, new(big.Int).Lsh(big.NewInt(1), 253), new(big.Int).Lsh(l, 1)} {
s := leBig(sig[32:])
s.Add(s, add)
if s.BitLen() > 256 {
continue
}
s2 := slices.Clone(sig)
copy(s2[32:], leBytes(s, 32))
edCases = append(edCases, obj{"name": "S + " + add.String(), "public_key": h(pub), "message": h(msg), "signature": h(s2), "valid": strictVerify(pub, msg, s2), "stdlib": ed25519.Verify(pub, msg, s2)})
}
for i, so := range smallOrder {
// R = the identity and S = 0: [0]B - [k]A = -[k]A, the identity when
// A has an order that divides k.
s2 := make([]byte, 64)
s2[0] = 1
for j := 0; j < 4; j++ {
m2 := []byte{byte(i), byte(j)}
edCases = append(edCases, obj{"name": fmt.Sprintf("small order point %d, R = identity, S = 0, message %d", i, j), "public_key": h(so[:]), "message": h(m2), "signature": h(s2), "valid": strictVerify(so[:], m2, s2), "stdlib": ed25519.Verify(so[:], m2, s2)})
}
}
doc["ed25519"] = edCases
// The encodings of points: Canonical, OnCurve and SmallOrder of the
// strict profile.
var encCases []obj
addEnc := func(name string, a []byte) {
encCases = append(encCases, obj{"name": name, "encoding": h(a), "canonical": canonical(a), "on_curve": onCurve(a), "small_order": isSmallOrder(a)})
}
pBig := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
for d := 0; d < 20; d++ {
for _, sign := range []byte{0, 0x80} {
v := new(big.Int).Add(pBig, big.NewInt(int64(d)))
e := leBytes(v, 32)
e[31] |= sign
addEnc(fmt.Sprintf("y = p + %d, sign %d", d, sign>>7), e)
e2 := leBytes(big.NewInt(int64(d)), 32)
e2[31] |= sign
addEnc(fmt.Sprintf("y = %d, sign %d", d, sign>>7), e2)
}
}
v := new(big.Int).Sub(pBig, big.NewInt(1))
for _, sign := range []byte{0, 0x80} {
e := leBytes(v, 32)
e[31] |= sign
addEnc(fmt.Sprintf("y = p - 1, sign %d", sign>>7), e)
}
for i, so := range smallOrder {
addEnc(fmt.Sprintf("small order %d", i), so[:])
e := slices.Clone(so[:])
e[31] ^= 0x80
addEnc(fmt.Sprintf("small order %d, sign flipped", i), e)
}
for i := 0; i < 24; i++ {
addEnc(fmt.Sprintf("random %d", i), randBytes(32))
}
addEnc("a public key", pub)
doc["ed25519_encodings"] = encCases
// Base64 as Go decodes it, with the offsets of its errors, in the
// encodings that the protocol uses.
encodings := []struct {
name string
enc *base64.Encoding
url, padded, strict bool
}{
{"std raw strict (age)", base64.RawStdEncoding.Strict(), false, false, true},
{"std padded strict", base64.StdEncoding.Strict(), false, true, true},
{"url raw strict", base64.RawURLEncoding.Strict(), true, false, true},
{"std padded", base64.StdEncoding, false, true, false},
{"url raw", base64.RawURLEncoding, true, false, false},
}
inputs := []string{"", "A", "AA", "AB", "AAA", "AAB", "AAAA", "AA==", "AAA=", "A===", "AB==", "AAB=", "=", "==", "AA=", "AA=A", "AA==A", "AA\n", "A\nA", "AA\r\n", "\nAAAA", "Zm9v", "Zm9", "Zm8", "Zm", "Zg", "Zh", "Z", "Zm9v=", "Zm\x80v", " Zm9v", "Zm-v", "Zm_v", "Zm+v", "Zm/v", "Zm9vYmFy", "Zm9vYmFyZm9v!mFy", "Zm9vYmFyZm9vYmF", "Zm9vYmFyZm9vYmE", "Zm9vYmFyZm9vYmE=", "Zm9vYmFyZm9vYm==", "Zm9vYmFyZm9vYm", "Zm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFy*", "AAAA\x00AAA", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB"}
var b64Cases []obj
for _, e := range encodings {
for _, in := range inputs {
c := obj{"encoding": e.name, "url": e.url, "padded": e.padded, "strict": e.strict, "input": h([]byte(in))}
out, err := e.enc.DecodeString(in)
if err != nil {
c["error"] = err.Error()
} else {
c["output"] = h(out)
}
b64Cases = append(b64Cases, c)
}
}
doc["base64"] = b64Cases
// Bech32 as age's internal/bech32, copied by datekeys-go as codec/bech32.
var bechCases []obj
addDecode := func(s string) {
hrp, data, err := bech32.Decode(s)
c := obj{"op": "decode", "input": s}
if err != nil {
c["error"] = err.Error()
} else {
c["hrp"], c["data"] = hrp, h(data)
}
bechCases = append(bechCases, c)
}
addEncode := func(hrp string, data []byte) {
s, err := bech32.Encode(hrp, data)
c := obj{"op": "encode", "hrp": hrp, "data": h(data)}
if err != nil {
c["error"] = err.Error()
} else {
c["output"] = s
}
bechCases = append(bechCases, c)
}
key := randBytes(32)
id, _ := bech32.Encode("AGE-SECRET-KEY-", key)
rec, _ := bech32.Encode("age", randBytes(32))
for _, d := range [][]byte{nil, {0}, {0xff}, randBytes(5), key, randBytes(60)} {
addEncode("age", d)
addEncode("AGE-SECRET-KEY-", d)
}
addEncode("", key)
addEncode("Age", key)
addEncode("a b", key)
addEncode("é", key)
for _, s := range []string{id, strings.ToLower(id), rec, strings.ToUpper(rec), id[:len(id)-1] + "Q", id[:len(id)-1], id + "q", strings.Replace(id, "1", "", 1), "1" + id[16:], "A1QQQQQQ", "a1qqqqqq", "a1qqqqq", "a1qqqqqb", "a1qqqqqqqq", "\x7f1qqqqqq", "x1Ẁqqqqqq", "é1qqqqqq", "AGE-SECRET-KEY-1Qa", rec[:4] + "B" + rec[5:], "age1" + strings.Repeat("q", 100), "age1qyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgp"} {
addDecode(s)
}
// Non-zero padding and illegal zero padding, with a valid checksum.
for _, data := range [][]byte{{1}, {0, 1}, {0, 0, 1}, {31, 31}} {
values := data
s := "age1"
for _, v := range values {
s += string("qpzry9x8gf2tvdw0s3jn54khce6mua7l"[v])
}
s += checksum("age", values)
addDecode(s)
}
doc["bech32"] = bechCases
path := filepath.Join(*outDir, "primitives.json")
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
if err := enc.Encode(doc); err != nil {
log.Fatal(err)
}
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
log.Fatal(err)
}
fmt.Printf("wrote %s, %d bytes\n", path, buf.Len())
// The same JSON as a Dart constant, for the tests compiled to
// JavaScript, which cannot read files.
if bytes.Contains(buf.Bytes(), []byte("'''")) {
log.Fatal("the JSON holds three quotes")
}
dart := "// Generated by tool/gen_primitive_vectors.go from primitives.json, for the\n" +
"// tests that also run compiled to JavaScript, where no file can be read. Do\n" +
"// not edit.\n\n" +
"/// The text of test/vectors/primitives.json.\n" +
"const primitivesJson = r'''\n" + buf.String() + "''';\n"
dpath := filepath.Join(*outDir, "primitives.g.dart")
if err := os.WriteFile(dpath, []byte(dart), 0o644); err != nil {
log.Fatal(err)
}
fmt.Printf("wrote %s\n", dpath)
}
func mustBig(s string) *big.Int {
v, ok := new(big.Int).SetString(s, 10)
if !ok {
log.Fatal(s)
}
return v
}
func leBig(b []byte) *big.Int {
be := slices.Clone(b)
slices.Reverse(be)
return new(big.Int).SetBytes(be)
}
func leBytes(v *big.Int, n int) []byte {
b := make([]byte, n)
v.FillBytes(b)
slices.Reverse(b)
return b
}
// checksum is the Bech32 checksum of hrp and the 5-bit values.
func checksum(hrp string, values []byte) string {
gen := []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
var v []byte
for _, c := range []byte(hrp) {
v = append(v, c>>5)
}
v = append(v, 0)
for _, c := range []byte(hrp) {
v = append(v, c&31)
}
v = append(v, values...)
v = append(v, 0, 0, 0, 0, 0, 0)
chk := uint32(1)
for _, x := range v {
top := chk >> 25
chk = (chk&0x1ffffff)<<5 ^ uint32(x)
for i := range 5 {
if top>>i&1 == 1 {
chk ^= gen[i]
}
}
}
chk ^= 1
s := ""
for p := range 6 {
s += string("qpzry9x8gf2tvdw0s3jn54khce6mua7l"[chk>>(5*(5-p))&31])
}
return s
}
// ---------------------------------------------------------------------------
// A verbatim copy of the functions of internal/ed25519strict of datekeys-go
// (v0.12 branch), which a program outside that module cannot import.
var smallOrder = [8][32]byte{
{0x00},
{31: 0x80},
{0x01},
{0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05},
{0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85},
{0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a},
{0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa},
{0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
}
func strictVerify(pub, msg, sig []byte) bool {
if len(pub) != ed25519.PublicKeySize || len(sig) != ed25519.SignatureSize {
return false
}
if !canonical(pub) || isSmallOrder(pub) {
return false
}
return ed25519.Verify(ed25519.PublicKey(pub), msg, sig)
}
func canonical(a []byte) bool {
if len(a) != 32 {
return false
}
high := a[31] & 0x7f
ones := true
for _, b := range a[1:31] {
if b != 0xff {
ones = false
break
}
}
if high == 0x7f && ones && a[0] >= 0xed {
return false
}
if a[31]&0x80 == 0 {
return true
}
zeros := high == 0
for _, b := range a[1:31] {
if b != 0 {
zeros = false
break
}
}
isOne := zeros && a[0] == 0x01
isMinusOne := high == 0x7f && ones && a[0] == 0xec
return !isOne && !isMinusOne
}
var curveP, curveD, halfP = func() (p, d, h *big.Int) {
p = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 255), big.NewInt(19))
d = new(big.Int).ModInverse(big.NewInt(121666), p)
d.Mul(d, big.NewInt(-121665)).Mod(d, p)
h = new(big.Int).Rsh(new(big.Int).Sub(p, big.NewInt(1)), 1)
return p, d, h
}()
func onCurve(a []byte) bool {
if len(a) != 32 {
return false
}
be := slices.Clone(a)
be[31] &= 0x7f
slices.Reverse(be)
y := new(big.Int).SetBytes(be)
y2 := new(big.Int).Mul(y, y)
u := new(big.Int).Sub(y2, big.NewInt(1))
v := new(big.Int).Mul(curveD, y2)
v.Add(v, big.NewInt(1)).Mod(v, curveP)
x2 := u.Mul(u, v.ModInverse(v, curveP))
x2.Mod(x2, curveP)
return x2.Sign() == 0 || new(big.Int).Exp(x2, halfP, curveP).Cmp(big.NewInt(1)) == 0
}
func isSmallOrder(a []byte) bool {
if len(a) != 32 {
return false
}
for _, s := range smallOrder {
if [32]byte(a) == s {
return true
}
}
return false
}
Loading…
Cancel
Save

Powered by TurnKey Linux.