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.
204 lines
7.3 KiB
204 lines
7.3 KiB
|
2 days ago
|
// The padding of spec §29.1 (lib/src/padding.dart), as padding.test.ts of
|
||
|
|
// datekeys-ts: P of both rules against a statement of spec §29.1 in BigInt
|
||
|
|
// over the whole range up to L_MAX, at the lengths where 32-bit operations
|
||
|
|
// or a floating-point logarithm fail and next to 2^53, and the check of the
|
||
|
|
// plaintext, cut in pieces of every size. It reads no file: it runs on the
|
||
|
|
// VM and compiled to JavaScript, where an int is a double. The vectors of
|
||
|
|
// Go run in formats_vectors_test.dart (testdata/vectors/padding.json) and
|
||
|
|
// in the differential (formats_padding.json).
|
||
|
|
library;
|
||
|
|
|
||
|
|
import 'dart:typed_data';
|
||
|
|
|
||
|
|
import 'package:datekeys/src/errors.dart';
|
||
|
|
import 'package:datekeys/src/padding.dart';
|
||
|
|
import 'package:test/test.dart';
|
||
|
|
|
||
|
|
// Spec §29.1 in BigInt: bloque256 = 256·ceil(L/256), at least 256;
|
||
|
|
// reforzado = max(bloque256, Padmé), Padmé keeping the S + 1 most
|
||
|
|
// significant bits of L, E = bitlen(L) - 1 and S = bitlen(E).
|
||
|
|
BigInt reference(BigInt l, PaddingRule rule) {
|
||
|
|
if (l <= BigInt.from(256)) return BigInt.from(256);
|
||
|
|
final b256 = BigInt.from(256);
|
||
|
|
final block = (l + BigInt.from(255)) ~/ b256 * b256;
|
||
|
|
if (rule == PaddingRule.bloque256) return block;
|
||
|
|
final e = l.bitLength - 1;
|
||
|
|
final s = BigInt.from(e).bitLength;
|
||
|
|
final unit = BigInt.one << (e - s);
|
||
|
|
final padme = (l + unit - BigInt.one) ~/ unit * unit;
|
||
|
|
return padme > block ? padme : block;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lengths spread over every bit length, from a fixed xorshift generator in
|
||
|
|
// BigInt, so that the same lengths run on the VM and on the web.
|
||
|
|
Iterable<int> lengths(int count) sync* {
|
||
|
|
final mask64 = (BigInt.one << 64) - BigInt.one;
|
||
|
|
var x = BigInt.parse('9e3779b97f4a7c15', radix: 16);
|
||
|
|
final lMax = BigInt.from(maxPayloadLength + 1);
|
||
|
|
for (var i = 0; i < count; i++) {
|
||
|
|
x ^= x << 13 & mask64;
|
||
|
|
x ^= x >> 7;
|
||
|
|
x ^= x << 17 & mask64;
|
||
|
|
final bits = 1 + i % 53;
|
||
|
|
yield ((x & ((BigInt.one << bits) - BigInt.one)) % lMax).toInt();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void main() {
|
||
|
|
test('names the codes of spec §29.1, and no code for no padding', () {
|
||
|
|
expect([for (final r in PaddingRule.values) r.code], [1, 2]);
|
||
|
|
expect(
|
||
|
|
[for (final r in PaddingRule.values) r.name],
|
||
|
|
['bloque256', 'reforzado'],
|
||
|
|
);
|
||
|
|
expect([0, 1, 2, 3, 257].map(PaddingRule.fromCode).toList(), [
|
||
|
|
null,
|
||
|
|
PaddingRule.bloque256,
|
||
|
|
PaddingRule.reforzado,
|
||
|
|
null,
|
||
|
|
null,
|
||
|
|
]);
|
||
|
|
expect(maxPayloadLength, 9007199254740992 - 70368744177664);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('counts bits exactly, up to 2^53, where a logarithm does not', () {
|
||
|
|
final values = [0, 1, 2, 255, 256, 4294967295, 4294967296];
|
||
|
|
values.addAll([562949953421311, 9007199254740991, 9007199254740992]);
|
||
|
|
for (final n in values) {
|
||
|
|
expect(bitLength(n), BigInt.from(n).bitLength, reason: '$n');
|
||
|
|
}
|
||
|
|
// Spec §29.1: log2(2^49 - 1) is 49 in IEEE 754, and E is not.
|
||
|
|
for (final l in [257, 78000, 562949953421311, maxPayloadLength]) {
|
||
|
|
final e = BigInt.from(l).bitLength - 1;
|
||
|
|
final s = BigInt.from(e).bitLength;
|
||
|
|
expect(padmeParameters(l), (e: e, s: s, lastBits: e - s));
|
||
|
|
}
|
||
|
|
expect(() => bitLength(-1), throwsRangeError);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('agrees with spec §29.1 in BigInt over the whole range', () {
|
||
|
|
var n = 0;
|
||
|
|
for (final l in lengths(3000)) {
|
||
|
|
for (final rule in PaddingRule.values) {
|
||
|
|
final want = reference(BigInt.from(l), rule);
|
||
|
|
expect(BigInt.from(paddedLength(l, rule)), want, reason: '$l $rule');
|
||
|
|
}
|
||
|
|
n++;
|
||
|
|
}
|
||
|
|
expect(n, 3000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('is exact next to 2^53 and at the boundaries of 32 bits', () {
|
||
|
|
final ls = <int>[
|
||
|
|
for (var k = 0; k < 300; k++) maxPayloadLength - k,
|
||
|
|
for (var e = 8; e <= 52; e++) ...[
|
||
|
|
for (final d in [-257, -1, 0, 1, 255, 257])
|
||
|
|
if (_pow2(e) + d >= 0) _pow2(e) + d,
|
||
|
|
],
|
||
|
|
2113929216,
|
||
|
|
2113929217,
|
||
|
|
4227858432,
|
||
|
|
4227858433,
|
||
|
|
];
|
||
|
|
for (final l in ls) {
|
||
|
|
for (final rule in PaddingRule.values) {
|
||
|
|
final p = paddedLength(l, rule);
|
||
|
|
expect(BigInt.from(p), reference(BigInt.from(l), rule), reason: '$l');
|
||
|
|
expect(p % 256 == 0 && p >= l && p >= 256, isTrue, reason: '$l');
|
||
|
|
expect(p <= 9007199254740991, isTrue, reason: '$l');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('rejects an L outside 0..L_MAX as an error without a code', () {
|
||
|
|
// Its text, that of Go, is in the differential (formats_padding.json).
|
||
|
|
for (final l in [-1, maxPayloadLength + 1, 9007199254740992]) {
|
||
|
|
for (final rule in PaddingRule.values) {
|
||
|
|
expect(() => paddedLength(l, rule), throwsArgumentError);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('gives the length of PAYLOAD_AGE: one stanza, the nonce, a tag per '
|
||
|
|
'chunk of 64 KiB', () {
|
||
|
|
for (final n in [0, 256, 65536, 65537, 79872, maxPayloadLength]) {
|
||
|
|
final b = BigInt.from(n);
|
||
|
|
var chunks = (b + BigInt.from(65535)) ~/ BigInt.from(65536);
|
||
|
|
if (chunks < BigInt.one) chunks = BigInt.one;
|
||
|
|
final want = BigInt.from(184) + b + BigInt.from(16) * chunks;
|
||
|
|
expect(BigInt.from(payloadAgeLength(n)), want, reason: '$n');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
group('PaddingCheck', () {
|
||
|
|
// The plaintext of a format 2 capsule: content of l bytes, zeros to p.
|
||
|
|
Uint8List plaintext(int l, int p) => Uint8List(p)..fillRange(0, l, 0x61);
|
||
|
|
|
||
|
|
String failure(void Function() body) {
|
||
|
|
try {
|
||
|
|
body();
|
||
|
|
} on DateKeysException catch (e) {
|
||
|
|
expect(e.code, ErrorCode.integrity);
|
||
|
|
return e.message;
|
||
|
|
}
|
||
|
|
return 'ok';
|
||
|
|
}
|
||
|
|
|
||
|
|
test('delivers the content and never the padding, in pieces of any '
|
||
|
|
'size', () {
|
||
|
|
for (final (l, p) in [(0, 256), (34, 256), (256, 256), (300, 512)]) {
|
||
|
|
for (final size in [1, 3, 64, 256, 1000]) {
|
||
|
|
final plain = plaintext(l, p);
|
||
|
|
final check = PaddingCheck(l, p);
|
||
|
|
final out = BytesBuilder();
|
||
|
|
for (var at = 0; at < p; at += size) {
|
||
|
|
out.add(
|
||
|
|
check.add(plain.sublist(at, at + size < p ? at + size : p)),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
check.close();
|
||
|
|
expect(out.takeBytes(), plain.sublist(0, l));
|
||
|
|
expect(check.received, p);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// The texts of Go are in the differential (formats_padding.json): here,
|
||
|
|
// the offsets that each failure names.
|
||
|
|
test('fails on the first byte of padding that is not zero, or past P, '
|
||
|
|
'or short of P', () {
|
||
|
|
final plain = plaintext(10, 256)..[200] = 1;
|
||
|
|
final check = PaddingCheck(10, 256);
|
||
|
|
expect(check.add(plain.sublist(0, 100)), hasLength(10));
|
||
|
|
expect(
|
||
|
|
failure(() => check.add(plain.sublist(100))),
|
||
|
|
allOf(startsWith('capsule: PAYLOAD_AGE: byte 200 '), contains('zero')),
|
||
|
|
);
|
||
|
|
// The first byte past P fails, before the zero bytes after it.
|
||
|
|
final long = PaddingCheck(10, 256)..add(Uint8List(256));
|
||
|
|
expect(failure(long.close), 'ok');
|
||
|
|
expect(failure(() => long.add(Uint8List(1))), contains('P = 256'));
|
||
|
|
final short = PaddingCheck(10, 256)..add(Uint8List(255));
|
||
|
|
expect(
|
||
|
|
failure(short.close),
|
||
|
|
allOf(contains(' 255 bytes'), contains('P = 256')),
|
||
|
|
);
|
||
|
|
final none = PaddingCheck(10, 256);
|
||
|
|
expect(failure(none.close), contains(' 0 bytes'));
|
||
|
|
// Format 3: from L, after BODY, and with the name of another file.
|
||
|
|
final body = PaddingCheck(659, 768, start: 659, what: 'X');
|
||
|
|
expect(body.add(Uint8List(109)), isEmpty);
|
||
|
|
body.close();
|
||
|
|
expect(() => PaddingCheck(10, 9), throwsArgumentError);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
int _pow2(int e) {
|
||
|
|
var p = 1;
|
||
|
|
for (var i = 0; i < e; i++) {
|
||
|
|
p *= 2;
|
||
|
|
}
|
||
|
|
return p;
|
||
|
|
}
|