|
|
// 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']}');
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|