// 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 vectors = jsonDecode(primitivesJson) as Map; List> cases(String name) => (vectors[name]! as List).cast>(); Uint8List hx(Object? v) => fromHex(v! as String); /// Whether a case is cheap enough for the platform. bool affordable(Map 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().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().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; 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']}'); } } }); }