You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dateKeys-dart/test/primitives_test.dart

401 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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

Powered by TurnKey Linux.