Stage 4b: the frames of the .dkc and the .dkk, the padding and BODY

lib/src/framing.dart ports ParsePrelude, Prelude.Bytes, PayloadOffset and
HeaderBinding of package capsule and the frame of Decode of package
accesskey of datekeys-go at c531e93, with their checks in the order of
spec §23 and §40 and their texts, and splitCapsule and FramingException,
the steps 1 to 3 of capsule.Inspect, as framing.ts of datekeys-ts. The
format of a capsule is the enum CapsuleFormat.

lib/src/padding.dart is padding.go: PaddedLength and PayloadAgeLength for
the codes of PaddingRule, exact up to L_MAX on the web too, where an int is
a double: bitlen doubles a power of two and the roundings divide and
multiply by powers of two, with no shift or mask of more than 31 bits. And
PaddingCheck, the checkPadding of capsule.Open at step 17, fed the
plaintext piece by piece.

lib/src/body.dart is the frame of BODY of format3.go (ParseBodyFrame,
CheckArea, ContentLength), and lib/src/digest.dart the incremental SHA-256
of a capsule_digest, with the comparison of checkCapsuleDigest. The errors
that Go returns without a normative code are ArgumentErrors with its text.

The tests run on the VM and compiled to JavaScript: the order of the
checks on capsules built in memory, P against a statement of spec §29.1 in
BigInt, next to 2^53 and at the boundaries of 32 bits, and the digest
against package:crypto however the file is cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.11
dev 2 days ago
parent 1e7753d110
commit 627f6fc5a1

@ -0,0 +1,58 @@
/// Unsigned big-endian integers of four and eight bytes, as the frames of the
/// protocol write them: the lengths of the PRELUDE of a .dkc and of a .dkk
/// (spec §22, §40), the frame of BODY (spec §29.2) and payload_length of
/// CONTROL_CBOR (spec §31).
///
/// They are exact on the VM and compiled to JavaScript, where an int is a
/// double and the bit operators work on 32 bits: no shift or mask here
/// touches more than 31 bits, and a value of eight bytes is an int only up to
/// 2^53-1.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
// 2^32, the weight of the high half of an integer of eight bytes.
const _twoPow32 = 0x100000000;
// The largest high half of an integer of eight bytes that is at most 2^53-1.
const _maxSafeHigh = 0x1fffff;
/// The unsigned 32-bit big-endian integer at [offset] of [b].
int readUint32BE(List<int> b, int offset) =>
b[offset] * 0x1000000 +
(b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]);
/// Writes [v], which must be in 0..2^32-1, at [offset] of [b], big-endian.
void writeUint32BE(Uint8List b, int offset, int v) {
if (v < 0 || v >= _twoPow32) {
throw RangeError.range(v, 0, _twoPow32 - 1, 'v');
}
final top = v ~/ 0x1000000;
final rest = v - top * 0x1000000;
b[offset] = top;
b[offset + 1] = rest >> 16;
b[offset + 2] = rest >> 8 & 0xff;
b[offset + 3] = rest & 0xff;
}
/// The unsigned 64-bit big-endian integer at [offset] of [b]: an [int] up to
/// 2^53-1 and a [BigInt] above, so that it is exact on every platform.
Object readUint64BE(List<int> b, int offset) {
final hi = readUint32BE(b, offset);
final lo = readUint32BE(b, offset + 4);
if (hi <= _maxSafeHigh) return hi * _twoPow32 + lo;
return BigInt.from(hi) << 32 | BigInt.from(lo);
}
/// Writes [v], which must be in 0..2^53-1, at [offset] of [b] in eight
/// bytes, big-endian.
void writeUint64BE(Uint8List b, int offset, int v) {
if (v < 0 || v > 9007199254740991) {
throw RangeError.range(v, 0, 9007199254740991, 'v');
}
final hi = v ~/ _twoPow32;
writeUint32BE(b, offset, hi);
writeUint32BE(b, offset + 4, v - hi * _twoPow32);
}

@ -0,0 +1,145 @@
/// The frame of BODY in a capsule of format 3 (spec §29.2), as BodyFrame,
/// ParseBodyFrame and CheckArea of package capsule of datekeys-go
/// (format3.go) and body.ts of datekeys-ts. The plaintext of PAYLOAD_AGE is
/// BODY followed by its padding, and BODY is
///
/// AREA_LEN || SECURITY_LEN || HEAD_LEN || SECURITY_CBOR, then zeros up
/// to AREA_LEN bytes || HEAD_CBOR || CONTENT
///
/// with the three lengths as unsigned 32-bit big-endian integers. Every
/// violation of the frame is ERR_INTEGRITY at step 17 (spec §63).
///
/// Internal, as in datekeys-ts: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'big_endian.dart';
import 'errors.dart';
/// The size of the frame: AREA_LEN, SECURITY_LEN and HEAD_LEN.
const bodyFrameSize = 12;
/// The unit of AREA_LEN.
const areaUnit = 512;
/// The largest AREA_LEN, 64 KiB.
const maxAreaLen = 128 * areaUnit;
/// The size of the security area that writers of this version write,
/// always, whatever the capsule holds (spec v0.11, §29.2, §62.1 rule 13):
/// 32 KiB. A writer of v0.10 wrote [areaUnit], 512 bytes, which a reader
/// still accepts.
const areaLen = 64 * areaUnit;
/// The area of a capsule whose creator expressly asked for a larger one
/// because the signatures do not fit in [areaLen]: 64 KiB.
const largeAreaLen = maxAreaLen;
/// The largest HEAD_LEN, 16 MiB.
const maxHeadLen = 16 << 20;
/// The frame of BODY (spec §29.2).
final class BodyFrame {
/// A frame of the three lengths.
const BodyFrame(this.areaLen, this.securityLen, this.headLen);
/// AREA_LEN, the size of the security area.
final int areaLen;
/// SECURITY_LEN, the size of SECURITY_CBOR.
final int securityLen;
/// HEAD_LEN, the size of HEAD_CBOR.
final int headLen;
@override
bool operator ==(Object other) =>
other is BodyFrame &&
other.areaLen == areaLen &&
other.securityLen == securityLen &&
other.headLen == headLen;
@override
int get hashCode => Object.hash(areaLen, securityLen, headLen);
@override
String toString() =>
'BodyFrame(AREA_LEN $areaLen, SECURITY_LEN $securityLen, '
'HEAD_LEN $headLen)';
}
/// The 12 bytes of the frame [f].
Uint8List bodyFrameBytes(BodyFrame f) {
final b = Uint8List(bodyFrameSize);
writeUint32BE(b, 0, f.areaLen);
writeUint32BE(b, 4, f.securityLen);
writeUint32BE(b, 8, f.headLen);
return b;
}
/// C, the length of CONTENT in a BODY of length [l] whose frame is [f]:
/// [parseBodyFrame] has checked that it is not negative.
int contentLength(BodyFrame f, int l) =>
l - bodyFrameSize - f.areaLen - f.headLen;
DateKeysException _integrity(String detail) =>
DateKeysException(ErrorCode.integrity, 'capsule: BODY: $detail');
/// Decodes the frame of BODY from its first 12 bytes [b] and checks it
/// against [l], L, the length of BODY (spec §29.2, §63 step 17.2), with the
/// texts of ParseBodyFrame of Go. The plaintext of PAYLOAD_AGE is at least
/// 256 bytes, so the 12 bytes exist even when L is shorter than the frame.
/// [b] must be 12 bytes long: any other length is an error of the caller,
/// without a normative code, as in Go.
BodyFrame parseBodyFrame(List<int> b, int l) {
if (b.length != bodyFrameSize) {
throw ArgumentError(
'capsule: BODY: frame of ${b.length} bytes, want $bodyFrameSize',
);
}
if (l < bodyFrameSize) {
throw _integrity(
'L = $l is shorter than the frame of $bodyFrameSize bytes',
);
}
final f = BodyFrame(
readUint32BE(b, 0),
readUint32BE(b, 4),
readUint32BE(b, 8),
);
if (f.areaLen < areaUnit ||
f.areaLen > maxAreaLen ||
f.areaLen % areaUnit != 0) {
throw _integrity(
'AREA_LEN ${f.areaLen} is not a multiple of $areaUnit from $areaUnit '
'to $maxAreaLen',
);
}
if (f.securityLen < 1 || f.securityLen > f.areaLen) {
throw _integrity(
'SECURITY_LEN ${f.securityLen} is not from 1 to AREA_LEN = '
'${f.areaLen}',
);
}
if (f.headLen < 1 || f.headLen > maxHeadLen) {
throw _integrity('HEAD_LEN ${f.headLen} is not from 1 to $maxHeadLen');
}
if (bodyFrameSize + f.areaLen + f.headLen > l) {
throw _integrity(
'the frame, the area of ${f.areaLen} bytes and the head of '
'${f.headLen} bytes exceed L = $l',
);
}
return f;
}
/// Checks that the bytes of the security area [area] after SECURITY_CBOR,
/// its first [securityLen] bytes, are zero (spec §29.2), as CheckArea of Go.
void checkArea(List<int> area, int securityLen) {
for (var i = securityLen; i < area.length; i++) {
if (area[i] != 0) {
throw _integrity('byte $i of the security area is not zero');
}
}
}

@ -0,0 +1,63 @@
/// SHA-256 computed incrementally, for the capsule_digest of a .dkk over a
/// .dkc that is not held in memory (spec §43, §63 step 9.a), as digest.ts of
/// datekeys-ts and the io.Copy into sha256 of checkCapsuleDigest in Go. It
/// runs on the SHA-256 of sha256.dart.
///
/// Internal, as in datekeys-ts: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
import 'errors.dart';
import 'sha256.dart';
/// An incremental SHA-256: [add] each piece, then [digest] once.
final class Sha256Hasher {
final Sha256 _h = Sha256();
bool _done = false;
/// Hashes [piece].
void add(List<int> piece) {
if (_done) throw StateError('SHA-256: add after digest');
_h.add(piece);
}
/// The SHA-256 of every piece added: 32 bytes. Called once.
Uint8List digest() {
if (_done) throw StateError('SHA-256: digest called twice');
_done = true;
return _h.finish();
}
}
/// A new incremental SHA-256.
Sha256Hasher sha256Hasher() => Sha256Hasher();
/// The SHA-256 of every piece that [stream] yields.
Future<Uint8List> sha256Stream(Stream<List<int>> stream) async {
final h = Sha256Hasher();
await for (final piece in stream) {
h.add(piece);
}
return h.digest();
}
/// capsule_digest = SHA-256 of the exact bytes of a .dkc (spec §43).
Uint8List capsuleDigest(List<int> dkc) => sha256(dkc);
/// Compares [digest], the SHA-256 of a .dkc, with the capsule_digest [want]
/// of a .dkk, without an early exit, as checkCapsuleDigest of capsule.Open:
/// ERR_ACCESS_INVALID when they differ (spec §43, §63 step 9.a). The digest
/// is a fast failure for a wrong file, not a security property.
void checkCapsuleDigest(List<int> digest, List<int> want) {
var diff = digest.length ^ want.length;
for (var i = 0; i < digest.length && i < want.length; i++) {
diff |= digest[i] ^ want[i];
}
if (diff != 0) {
throw DateKeysException(
ErrorCode.accessInvalid,
'capsule: the .dkk capsule_digest does not match this .dkc',
);
}
}

@ -0,0 +1,364 @@
/// The frames of a .dkc and of a .dkk (spec §22, §23, §40, §57), as
/// framing.go and inspect.go of package capsule and accesskey.go of
/// datekeys-go, and framing.ts of datekeys-ts: the same checks in the same
/// order, with the same codes and texts.
///
/// A .dkc is PRELUDE || PUBLIC_HEADER || SEALED_CONTROL || PAYLOAD_AGE, and
/// its payload runs to the end of the file. A .dkk is a prelude of 12 bytes
/// and BODY_CBOR.
library;
import 'dart:typed_data';
import 'big_endian.dart';
import 'errors.dart';
import 'sha256.dart';
/// The magic of a .dkc (spec §22).
const dkcMagic = 'DKC1';
/// The size of the PRELUDE of a .dkc (spec §23).
const dkcPreludeSize = 16;
/// The limit of PUBLIC_HEADER, 1 MiB (spec §57).
const maxPublicHeaderLen = 1 << 20;
/// The limit of SEALED_CONTROL, 64 MiB (spec §57).
const maxSealedControlLen = 64 << 20;
/// The magic of a .dkk (spec §40).
const dkkMagic = 'DKK1';
/// The only framing version of a .dkk (spec §40).
const dkkFramingVersion = 1;
/// The size of the prelude of a .dkk (spec §40).
const dkkPreludeSize = 12;
/// The limit of the BODY_CBOR of a .dkk, 16 MiB (spec §57).
const maxDkkBodyLen = 16 << 20;
/// The format of a capsule: the VERSION byte of its PRELUDE. It also fixes
/// the schema version of its CONTROL_CBOR, the number of stanzas of its
/// INNER_ACCESS_AGE, whether its payload is padded and what its plaintext is
/// (spec §22). A reader opens all three; a writer writes format 3 only (spec
/// §62.1, §70).
enum CapsuleFormat {
/// The format of spec v0.8.2: one or more stanzas in INNER_ACCESS_AGE and a
/// payload without padding. Only a generator of test vectors may write it.
format1(1),
/// The format of spec v0.9: exactly 16 stanzas in INNER_ACCESS_AGE and a
/// padded payload (spec §29.1, §39). Only a generator of test vectors may
/// write it.
format2(2),
/// The format of spec v0.10 and later: the padded plaintext of PAYLOAD_AGE
/// is BODY, with the security area, the head and the files (spec §29.2).
format3(3);
const CapsuleFormat(this.version);
/// The VERSION byte of the PRELUDE, and the schema version of the
/// CONTROL_CBOR of the capsule (spec §22, §31).
final int version;
/// Whether the payload of this format is padded, with L and the padding
/// code in keys 6 and 7 of its control (spec §29.1, §31): formats 2 and 3.
bool get isPadded => this != format1;
/// The format whose VERSION byte is [version], or null when V1 defines
/// none.
static CapsuleFormat? fromVersion(int version) => switch (version) {
1 => format1,
2 => format2,
3 => format3,
_ => null,
};
}
/// The decoded fields of the PRELUDE of a .dkc (spec §22, §23).
final class Prelude {
/// A PRELUDE of [format] with the two lengths.
const Prelude(this.format, this.publicHeaderLen, this.sealedControlLen);
/// VERSION, the format of the capsule.
final CapsuleFormat format;
/// PUBLIC_HEADER_LEN.
final int publicHeaderLen;
/// SEALED_CONTROL_LEN.
final int sealedControlLen;
@override
bool operator ==(Object other) =>
other is Prelude &&
other.format == format &&
other.publicHeaderLen == publicHeaderLen &&
other.sealedControlLen == sealedControlLen;
@override
int get hashCode => Object.hash(format, publicHeaderLen, sealedControlLen);
@override
String toString() =>
'Prelude(format ${format.version}, PUBLIC_HEADER_LEN $publicHeaderLen, '
'SEALED_CONTROL_LEN $sealedControlLen)';
}
bool _hasMagic(List<int> b, String magic) {
if (b.length < 4) return false;
for (var i = 0; i < 4; i++) {
if (b[i] != magic.codeUnitAt(i)) return false;
}
return true;
}
// Go's %#x of a byte: 0x and its digits, without leading zeros.
String _hx(int v) => '0x${v.toRadixString(16)}';
// Go's %02x of a byte.
String _hx2(int v) => v.toRadixString(16).padLeft(2, '0');
/// Validates the PRELUDE of a .dkc (spec §22, §23, §63 steps 1 and 2), as
/// ParsePrelude of Go, in the order of spec §23: the magic, a complete
/// prelude, the version (the format, 1 to 3), FLAGS == 0 and RESERVED == 0,
/// and each length from 1 up to its limit of spec §57. [b] holds the bytes
/// read, at most 16 of them are looked at.
Prelude parsePrelude(List<int> b) {
if (!_hasMagic(b, dkcMagic)) {
throw DateKeysException(ErrorCode.invalidMagic, 'capsule');
}
if (b.length < dkcPreludeSize) {
throw DateKeysException(ErrorCode.integrity, 'capsule: truncated prelude');
}
final format = CapsuleFormat.fromVersion(b[4]);
if (format == null) {
throw DateKeysException(
ErrorCode.unsupportedVersion,
'capsule: framing version ${b[4]}',
);
}
if (b[5] != 0 || b[6] != 0 || b[7] != 0) {
// Go's "flags %#x, reserved %#02x%02x": the zero padding of %#02x
// counts digits only, so a zero byte is 0x00.
throw DateKeysException(
ErrorCode.invalidFlags,
'capsule: flags ${_hx(b[5])}, reserved 0x${_hx2(b[6])}${_hx2(b[7])}',
);
}
final p = Prelude(format, readUint32BE(b, 8), readUint32BE(b, 12));
if (p.publicHeaderLen == 0 || p.publicHeaderLen > maxPublicHeaderLen) {
throw DateKeysException(
ErrorCode.integrity,
'capsule: PUBLIC_HEADER_LEN ${p.publicHeaderLen} outside '
'1..$maxPublicHeaderLen',
);
}
if (p.sealedControlLen == 0 || p.sealedControlLen > maxSealedControlLen) {
throw DateKeysException(
ErrorCode.integrity,
'capsule: SEALED_CONTROL_LEN ${p.sealedControlLen} outside '
'1..$maxSealedControlLen',
);
}
return p;
}
/// The exact 16 bytes of the PRELUDE [p], the ones that header_binding
/// covers: FLAGS and RESERVED are zero in V1.
Uint8List preludeBytes(Prelude p) {
final b = Uint8List(dkcPreludeSize);
for (var i = 0; i < 4; i++) {
b[i] = dkcMagic.codeUnitAt(i);
}
b[4] = p.format.version;
writeUint32BE(b, 8, p.publicHeaderLen);
writeUint32BE(b, 12, p.sealedControlLen);
return b;
}
/// Where PAYLOAD_AGE starts: 16 + PUBLIC_HEADER_LEN + SEALED_CONTROL_LEN
/// (spec §63).
int payloadOffset(Prelude p) =>
dkcPreludeSize + p.publicHeaderLen + p.sealedControlLen;
/// header_binding = SHA-256(PRELUDE || PUBLIC_HEADER_BYTES), over the exact
/// stored bytes: the header is never encoded again for it (spec §26).
Uint8List headerBinding(List<int> prelude, List<int> publicHeader) =>
(Sha256()
..add(prelude)
..add(publicHeader))
.finish();
/// The sections of a .dkc held in memory, as views of its bytes.
final class CapsuleSections {
/// The sections of a .dkc.
const CapsuleSections({
required this.prelude,
required this.preludeBytes,
required this.publicHeader,
required this.sealedControl,
required this.payload,
});
/// The decoded PRELUDE.
final Prelude prelude;
/// The exact 16 bytes of the PRELUDE.
final Uint8List preludeBytes;
/// The exact bytes of PUBLIC_HEADER.
final Uint8List publicHeader;
/// SEALED_CONTROL, OUTER_TIME_AGE, or null when the file ends before its
/// declared end, which the reference reports at step 5, after the header
/// is validated at step 4.
final Uint8List? sealedControl;
/// PAYLOAD_AGE, to the end of the file; empty when [sealedControl] is
/// null.
final Uint8List payload;
}
/// A failure of the frame of a .dkc in [splitCapsule]: its step of spec §63,
/// 1 to 3, its error and, from step 3, the PRELUDE.
final class FramingException implements Exception {
/// The failure of [step] with [error].
const FramingException(this.step, this.error, [this.prelude]);
/// The step of spec §63 that fails: 1, 2 or 3.
final int step;
/// The normative error.
final DateKeysException error;
/// The PRELUDE, which step 2 validated, when [step] is 3.
final Prelude? prelude;
@override
String toString() => error.message;
}
/// Splits a whole .dkc held in memory, with the checks of the frame of steps
/// 1, 2 and 3 of the inspection (spec §63) in the order of the reference:
/// the magic and a truncated prelude (step 1), the fields of the prelude and
/// their limits (step 2) and the exact bytes of PUBLIC_HEADER (step 3). A
/// failure throws a [FramingException] with its step. A truncated
/// SEALED_CONTROL is reported by a null [CapsuleSections.sealedControl]: the
/// reference finds it at step 5, after the header is validated at step 4.
CapsuleSections splitCapsule(List<int> dkc) {
final bytes = dkc is Uint8List ? dkc : Uint8List.fromList(dkc);
final pre = Uint8List.sublistView(
bytes,
0,
bytes.length < dkcPreludeSize ? bytes.length : dkcPreludeSize,
);
if (pre.length < dkcPreludeSize && _hasMagic(pre, dkcMagic)) {
throw FramingException(
1,
DateKeysException(ErrorCode.integrity, 'capsule: truncated prelude'),
);
}
final Prelude prelude;
try {
prelude = parsePrelude(pre);
} on DateKeysException catch (e) {
throw FramingException(e.code == ErrorCode.invalidMagic ? 1 : 2, e);
}
final h1 = dkcPreludeSize + prelude.publicHeaderLen;
if (bytes.length < h1) {
throw FramingException(
3,
DateKeysException(
ErrorCode.integrity,
'capsule: truncated PUBLIC_HEADER',
),
prelude,
);
}
final s1 = h1 + prelude.sealedControlLen;
final complete = bytes.length >= s1;
return CapsuleSections(
prelude: prelude,
preludeBytes: pre,
publicHeader: Uint8List.sublistView(bytes, dkcPreludeSize, h1),
sealedControl: complete ? Uint8List.sublistView(bytes, h1, s1) : null,
payload: complete
? Uint8List.sublistView(bytes, s1)
: Uint8List.sublistView(bytes, 0, 0),
);
}
/// The error of step 5 for a .dkc that ends inside SEALED_CONTROL, as the
/// reference reports it.
DateKeysException truncatedSealedControl() =>
DateKeysException(ErrorCode.integrity, 'capsule: truncated SEALED_CONTROL');
// ---------------------------------------------------------------------------
// DKK1
/// The 12 bytes of the prelude of a .dkk whose BODY_CBOR is [bodyLen] bytes
/// long (spec §40).
Uint8List dkkPreludeBytes(int bodyLen) {
final b = Uint8List(dkkPreludeSize);
for (var i = 0; i < 4; i++) {
b[i] = dkkMagic.codeUnitAt(i);
}
b[4] = dkkFramingVersion;
writeUint32BE(b, 8, bodyLen);
return b;
}
/// Validates the frame of a whole .dkk and returns its BODY_CBOR, a view of
/// [dkk], in the order of spec §40 and of Decode of package accesskey: the
/// magic, a complete prelude, the version, FLAGS and RESERVED, BODY_LEN in
/// 1..16 MiB (spec §57), the declared length present and nothing after the
/// body.
Uint8List splitAccessKey(List<int> dkk) {
final bytes = dkk is Uint8List ? dkk : Uint8List.fromList(dkk);
if (!_hasMagic(bytes, dkkMagic)) {
throw DateKeysException(ErrorCode.invalidMagic, 'accesskey');
}
if (bytes.length < dkkPreludeSize) {
throw DateKeysException(
ErrorCode.integrity,
'accesskey: truncated prelude',
);
}
if (bytes[4] != dkkFramingVersion) {
throw DateKeysException(
ErrorCode.unsupportedVersion,
'accesskey: framing version ${bytes[4]}',
);
}
if (bytes[5] != 0 || bytes[6] != 0 || bytes[7] != 0) {
// Go's "flags %#x, reserved %#x%02x".
throw DateKeysException(
ErrorCode.invalidFlags,
'accesskey: flags ${_hx(bytes[5])}, reserved '
'${_hx(bytes[6])}${_hx2(bytes[7])}',
);
}
// Spec §40, §57: BODY_LEN in 1..16 MiB. No empty frame holds a valid body,
// so 0 is a framing error, like a PUBLIC_HEADER_LEN of 0 (spec §22).
final bodyLen = readUint32BE(bytes, 8);
if (bodyLen == 0 || bodyLen > maxDkkBodyLen) {
throw DateKeysException(
ErrorCode.integrity,
'accesskey: BODY_LEN $bodyLen outside 1..$maxDkkBodyLen',
);
}
final end = dkkPreludeSize + bodyLen;
if (bytes.length < end) {
throw DateKeysException(ErrorCode.integrity, 'accesskey: truncated body');
}
if (bytes.length > end) {
throw DateKeysException(
ErrorCode.integrity,
'accesskey: data after BODY_CBOR',
);
}
return Uint8List.sublistView(bytes, dkkPreludeSize, end);
}

@ -0,0 +1,181 @@
/// The padding of the payload of a capsule of format 2 or 3 (spec §29.1), as
/// padding.go of package capsule of datekeys-go and padding.ts of
/// datekeys-ts: the plaintext of PAYLOAD_AGE is the content, L bytes (in
/// format 3, BODY), followed by zeros up to P = rule(L).
///
/// The arithmetic is exact over the whole range, up to L_MAX = 2^53 - 2^46,
/// on the VM and compiled to JavaScript, where an int is a double and the bit
/// operators work on 32 bits: bitlen counts by doubling a power of two, and
/// the roundings divide and multiply by powers of two, which is exact in
/// doubles. Never a floating-point logarithm, nor a shift or a mask of more
/// than 31 bits, which spec §29.1 forbids: they give a wrong P from
/// L = 2 113 929 217 on.
library;
import 'dart:typed_data';
import 'errors.dart';
/// The padding rule of a capsule of format 2 or 3, sealed in key 7 of
/// CONTROL_CBOR (spec §29.1, §31). No code stands for no padding. Its [name]
/// is the one of Go's String: bloque256 or reforzado.
enum PaddingRule {
/// Code 1: the content padded to the next multiple of 256 bytes, and to at
/// least 256.
bloque256(1),
/// Code 2: the larger of bloque256 and Padmé. The rule a writer uses by
/// default, as spec §29.1 asks of the official SDK.
reforzado(2);
const PaddingRule(this.code);
/// The code sealed in key 7 of CONTROL_CBOR.
final int code;
/// The rule of [code], or null when spec §29.1 defines none.
static PaddingRule? fromCode(int code) => switch (code) {
1 => bloque256,
2 => reforzado,
_ => null,
};
}
/// L_MAX = 2^53 - 2^46, the largest content length a capsule can seal: the
/// largest L for which both rules give a P of at most 2^53 - 1 (spec §29.1).
const maxPayloadLength = 8936830510563328;
/// The number of bits of [n], a non-negative integer up to 2^53: bitlen(0)
/// is 0, bitlen(256) is 9. It doubles a power of two, exact in a double up
/// to 2^54, and never shifts.
int bitLength(int n) {
if (n < 0) throw RangeError.value(n, 'n', 'not a non-negative integer');
var bits = 0;
for (var p = 1; p <= n; p *= 2) {
bits++;
}
return bits;
}
// 2^k, exact in a double for every k of this file.
int _pow2(int k) {
var p = 1;
for (var i = 0; i < k; i++) {
p *= 2;
}
return p;
}
/// The intermediate values of Padmé for an L above 256 (spec §29.1): E =
/// bitlen(L) - 1, S = bitlen(E) and lastBits = E - S.
({int e, int s, int lastBits}) padmeParameters(int l) {
final e = bitLength(l) - 1;
final s = bitLength(e);
return (e: e, s: s, lastBits: e - s);
}
/// P = rule(L), the exact length of the plaintext of PAYLOAD_AGE in a
/// capsule of format 2 or 3 whose content is [l] bytes long (spec §29.1), as
/// PaddedLength of Go. [l] must be in 0..L_MAX: above, it is the error of Go,
/// which has no normative code.
int paddedLength(int l, PaddingRule rule) {
if (l > maxPayloadLength) {
throw ArgumentError(
'capsule: content of $l bytes exceeds L_MAX = $maxPayloadLength',
);
}
if (l < 0) {
throw ArgumentError(
'capsule: content of $l bytes, outside 0..L_MAX = $maxPayloadLength',
);
}
if (l <= 256) return 256;
final block = (l + 255) ~/ 256 * 256;
if (rule == PaddingRule.bloque256) return block;
// Padmé keeps the S + 1 most significant bits of L and rounds up the
// others, the lastBits = E - S lowest ones: 2^lastBits·ceil(L/2^lastBits).
final unit = _pow2(padmeParameters(l).lastBits);
final padme = (l + unit - 1) ~/ unit * unit;
return padme > block ? padme : block;
}
/// The length of a PAYLOAD_AGE whose plaintext is [n] bytes long: the 184
/// bytes of an age header with one X25519 stanza and the nonce, the
/// plaintext, and the 16-byte tag of each STREAM chunk of 64 KiB, at least
/// one (spec §62.1, informative note). Exact for every P up to L_MAX.
int payloadAgeLength(int n) {
final chunks = (n + 65535) ~/ 65536;
return 184 + n + 16 * (chunks < 1 ? 1 : chunks);
}
/// The check of the plaintext of PAYLOAD_AGE against L and P (spec §29.1,
/// §63 step 17), as checkPadding of capsule.Open in Go: the plaintext is
/// exactly P bytes, its first L bytes are the content and the bytes from L
/// to P - 1 are zero. It is fed the plaintext as age authenticates each
/// piece, from the offset [start]: 0 for a capsule of format 2, whose first
/// L bytes are the content, and L for one of format 3, whose BODY has been
/// read already. [add] fails as soon as the plaintext goes past P or a byte
/// of the padding is not zero; [close] fails when it ends short of P. Every
/// failure is ERR_INTEGRITY, whatever the moment it is found (spec §69.1),
/// with the texts of Go, which name [what], the age file.
final class PaddingCheck {
/// A check of a plaintext whose content is [l] bytes long and whose
/// padding goes up to [p], from the offset [start].
PaddingCheck(this.l, this.p, {int start = 0, this.what = 'PAYLOAD_AGE'})
: _at = start {
if (l < 0 || l > p || start < 0) {
throw ArgumentError('padding check of L = $l, P = $p from $start');
}
}
/// L, the length of the content.
final int l;
/// P, the length of the plaintext.
final int p;
/// The age file of the texts.
final String what;
int _at;
/// The offset of the next byte of the plaintext: the number of bytes
/// received, the [start] included.
int get received => _at;
/// Takes the next [piece] of the plaintext and returns the part of it that
/// is content, a view of [piece], empty past L. The rest is checked as
/// padding.
Uint8List add(List<int> piece) {
final bytes = piece is Uint8List ? piece : Uint8List.fromList(piece);
final left = l - _at;
final content = left <= 0 ? 0 : (left < bytes.length ? left : bytes.length);
for (var i = content; i < bytes.length; i++) {
if (_at + i >= p) {
throw DateKeysException(
ErrorCode.integrity,
'capsule: $what: the plaintext is longer than P = $p',
);
}
if (bytes[i] != 0) {
throw DateKeysException(
ErrorCode.integrity,
'capsule: $what: byte ${_at + i} of the plaintext is padding and '
'is not zero',
);
}
}
_at += bytes.length;
return Uint8List.sublistView(bytes, 0, content);
}
/// Checks, at the end of the plaintext, that it is P bytes long.
void close() {
if (_at < p) {
throw DateKeysException(
ErrorCode.integrity,
'capsule: $what: the plaintext is $_at bytes, shorter than P = $p',
);
}
}
}

@ -0,0 +1,321 @@
// The frames of a .dkc and of a .dkk (lib/src/framing.dart), the frame of
// BODY (lib/src/body.dart) and the incremental SHA-256 of the
// capsule_digest (lib/src/digest.dart), as framing.test.ts, body.test.ts
// and digest.test.ts of datekeys-ts, on capsules built here: the order of
// the checks, the steps of the inspection, the views and the limits. The
// texts of Go of every failure are in the differential
// (formats_framing.json and formats_body.json). It reads no file: it runs on
// the VM and compiled to JavaScript.
library;
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:datekeys/src/big_endian.dart';
import 'package:datekeys/src/body.dart';
import 'package:datekeys/src/bytes.dart';
import 'package:datekeys/src/digest.dart';
import 'package:datekeys/src/errors.dart';
import 'package:datekeys/src/framing.dart';
import 'package:test/test.dart';
String codeOf(void Function() body) {
try {
body();
return 'ok';
} on DateKeysException catch (e) {
return e.code.code;
}
}
/// A .dkc of [format] whose sections are [header], [control] and
/// [payload] bytes of 1, 2 and 3.
Uint8List capsule(int format, int header, int control, int payload) {
final out = Uint8List(16 + header + control + payload)
..setAll(0, 'DKC1'.codeUnits);
out[4] = format;
writeUint32BE(out, 8, header);
writeUint32BE(out, 12, control);
out
..fillRange(16, 16 + header, 1)
..fillRange(16 + header, 16 + header + control, 2)
..fillRange(16 + header + control, out.length, 3);
return out;
}
void main() {
group('PRELUDE', () {
final ok = capsule(1, 121, 446, 0);
Uint8List withByte(int i, int v) => Uint8List.fromList(ok)..[i] = v;
test('reads the format and the lengths, and writes them back', () {
for (final f in CapsuleFormat.values) {
final p = parsePrelude(withByte(4, f.version));
expect(p, Prelude(f, 121, 446));
expect(preludeBytes(p), withByte(4, f.version).sublist(0, 16));
expect(payloadOffset(p), 16 + 121 + 446);
expect(f.isPadded, f != CapsuleFormat.format1);
expect(CapsuleFormat.fromVersion(f.version), f);
}
expect([0, 4, 255].map(CapsuleFormat.fromVersion), everyElement(isNull));
});
test('checks the magic, a whole prelude, the version, FLAGS and '
'RESERVED, and then the lengths, in the order of spec §23', () {
expect(codeOf(() => parsePrelude(Uint8List(0))), 'ERR_INVALID_MAGIC');
expect(codeOf(() => parsePrelude(ok.sublist(0, 3))), 'ERR_INVALID_MAGIC');
expect(
codeOf(() => parsePrelude(withByte(3, 0x32))),
'ERR_INVALID_MAGIC',
);
expect(codeOf(() => parsePrelude(ok.sublist(0, 4))), 'ERR_INTEGRITY');
expect(codeOf(() => parsePrelude(ok.sublist(0, 15))), 'ERR_INTEGRITY');
for (final v in [0, 4, 0xff]) {
expect(
codeOf(() => parsePrelude(withByte(4, v))),
'ERR_UNSUPPORTED_VERSION',
);
}
for (final i in [5, 6, 7]) {
expect(codeOf(() => parsePrelude(withByte(i, 1))), 'ERR_INVALID_FLAGS');
}
Uint8List lengths(int h, int s) {
final b = Uint8List.fromList(ok.sublist(0, 16));
writeUint32BE(b, 8, h);
writeUint32BE(b, 12, s);
return b;
}
expect(
parsePrelude(lengths(maxPublicHeaderLen, maxSealedControlLen)),
Prelude(CapsuleFormat.format1, maxPublicHeaderLen, maxSealedControlLen),
);
expect(parsePrelude(lengths(1, 1)), Prelude(CapsuleFormat.format1, 1, 1));
for (final (h, s) in [
(0, 1),
(1, 0),
(maxPublicHeaderLen + 1, 1),
(1, maxSealedControlLen + 1),
(0xffffffff, 0xffffffff),
]) {
expect(codeOf(() => parsePrelude(lengths(h, s))), 'ERR_INTEGRITY');
}
// The version and FLAGS come before the lengths.
final both = lengths(0, 0)..[7] = 1;
expect(codeOf(() => parsePrelude(both)), 'ERR_INVALID_FLAGS');
both[4] = 9;
expect(codeOf(() => parsePrelude(both)), 'ERR_UNSUPPORTED_VERSION');
});
test('header_binding is the SHA-256 of the exact PRELUDE and header', () {
final s = splitCapsule(capsule(2, 20, 30, 40));
expect(
headerBinding(s.preludeBytes, s.publicHeader),
crypto.sha256.convert(capsule(2, 20, 30, 40).sublist(0, 36)).bytes,
);
});
});
group('splitCapsule', () {
final dkc = capsule(3, 20, 30, 40);
FramingException failure(List<int> b) {
try {
splitCapsule(b);
} on FramingException catch (e) {
return e;
}
fail('no failure');
}
test('gives views of the sections', () {
final s = splitCapsule(dkc);
expect(s.prelude, Prelude(CapsuleFormat.format3, 20, 30));
expect(s.preludeBytes, dkc.sublist(0, 16));
expect(s.publicHeader, Uint8List(20)..fillRange(0, 20, 1));
expect(s.sealedControl, Uint8List(30)..fillRange(0, 30, 2));
expect(s.payload, Uint8List(40)..fillRange(0, 40, 3));
// Views, not copies.
s.publicHeader[0] = 9;
expect(dkc[16], 9);
dkc[16] = 1;
});
test('reports the step of each failure of the frame', () {
for (final (cut, step, code) in [
(0, 1, 'ERR_INVALID_MAGIC'),
(3, 1, 'ERR_INVALID_MAGIC'),
(4, 1, 'ERR_INTEGRITY'),
(15, 1, 'ERR_INTEGRITY'),
(16, 3, 'ERR_INTEGRITY'),
(35, 3, 'ERR_INTEGRITY'),
]) {
final e = failure(dkc.sublist(0, cut));
expect([e.step, e.error.code.code], [step, code], reason: '$cut');
expect(e.prelude, step == 3 ? splitCapsule(dkc).prelude : isNull);
expect('$e', e.error.message);
}
expect(failure(Uint8List.fromList(dkc)..[4] = 9).step, 2);
expect(failure(Uint8List.fromList(dkc)..[5] = 1).step, 2);
});
test('leaves a short SEALED_CONTROL to step 5', () {
for (final cut in [36, 37, 65]) {
final s = splitCapsule(dkc.sublist(0, cut));
expect(s.publicHeader, hasLength(20));
expect(s.sealedControl, isNull);
expect(s.payload, isEmpty);
}
expect(truncatedSealedControl().code, ErrorCode.integrity);
expect(splitCapsule(dkc.sublist(0, 66)).payload, isEmpty);
expect(splitCapsule(dkc.sublist(0, 66)).sealedControl, hasLength(30));
});
});
group('DKK1', () {
test('frames and unframes a body', () {
final body = Uint8List.fromList([0xa0, 1, 2]);
final framed = concatBytes([dkkPreludeBytes(body.length), body]);
expect(toHex(framed.sublist(0, 12)), '444b4b310100000000000003');
expect(splitAccessKey(framed), body);
});
test('checks the frame in the order of spec §40', () {
final ok = concatBytes([dkkPreludeBytes(2), Uint8List(2)]);
Uint8List withByte(int i, int v) => Uint8List.fromList(ok)..[i] = v;
expect(codeOf(() => splitAccessKey(Uint8List(0))), 'ERR_INVALID_MAGIC');
expect(codeOf(() => splitAccessKey(withByte(0, 0))), 'ERR_INVALID_MAGIC');
expect(codeOf(() => splitAccessKey(ok.sublist(0, 11))), 'ERR_INTEGRITY');
expect(
codeOf(() => splitAccessKey(withByte(4, 2))),
'ERR_UNSUPPORTED_VERSION',
);
for (final i in [5, 6, 7]) {
expect(
codeOf(() => splitAccessKey(withByte(i, 1))),
'ERR_INVALID_FLAGS',
);
}
for (final n in [0, maxDkkBodyLen + 1]) {
expect(
codeOf(() => splitAccessKey(dkkPreludeBytes(n))),
'ERR_INTEGRITY',
);
}
// BODY_LEN 0 after FLAGS.
expect(
codeOf(() => splitAccessKey(dkkPreludeBytes(0)..[5] = 1)),
'ERR_INVALID_FLAGS',
);
expect(codeOf(() => splitAccessKey(ok.sublist(0, 13))), 'ERR_INTEGRITY');
expect(
codeOf(() => splitAccessKey(concatBytes([ok, Uint8List(1)]))),
'ERR_INTEGRITY',
);
});
});
group('BODY', () {
BodyFrame parse(int area, int security, int head, int l) =>
parseBodyFrame(bodyFrameBytes(BodyFrame(area, security, head)), l);
test('reads the frame at its limits', () {
for (final (area, security, head, l, c) in [
(512, 22, 53, 577, 0),
(512, 512, 1, 525, 0),
(65536, 1, 1 << 24, 12 + 65536 + (1 << 24), 0),
(1024, 1024, 100, 1099511627776, 1099511626640),
(32768, 22, 53, 8936830510563328, 8936830510563328 - 12 - 32768 - 53),
]) {
final f = parse(area, security, head, l);
expect(f, BodyFrame(area, security, head));
expect(contentLength(f, l), c);
}
expect(
[areaUnit, maxAreaLen, areaLen, largeAreaLen, maxHeadLen],
[512, 65536, 32768, 65536, 16777216],
);
});
test('rejects every violation of the frame with ERR_INTEGRITY', () {
for (final (area, security, head, l) in [
(512, 22, 53, 11),
(0, 22, 53, 1000),
(511, 22, 53, 1000),
(513, 22, 53, 1000),
(66048, 22, 53, 100000),
(512, 0, 53, 1000),
(512, 513, 53, 1000),
(512, 22, 0, 1000),
(512, 22, (1 << 24) + 1, 1 << 30),
(512, 22, 53, 576),
]) {
expect(
codeOf(() => parse(area, security, head, l)),
'ERR_INTEGRITY',
reason: '$area $security $head $l',
);
}
expect(() => parseBodyFrame(Uint8List(11), 100), throwsArgumentError);
});
test('requires zeros after SECURITY_CBOR up to AREA_LEN', () {
final area = Uint8List(512)..fillRange(0, 22, 9);
checkArea(area, 22);
area[511] = 1;
expect(codeOf(() => checkArea(area, 22)), 'ERR_INTEGRITY');
checkArea(area, 512);
});
});
group('the digest of a .dkc', () {
final dkc = Uint8List.fromList([
for (var i = 0; i < 200003; i++) i * 7 % 251,
]);
final want = crypto.sha256.convert(dkc).bytes;
test('is the SHA-256 of the whole file, however it is cut', () {
expect(capsuleDigest(dkc), want);
for (final cut in [1, 7, 63, 64, 65, 4096, 65536, dkc.length]) {
final h = sha256Hasher();
for (var at = 0; at < dkc.length; at += cut) {
h.add(dkc.sublist(at, at + cut < dkc.length ? at + cut : dkc.length));
}
expect(h.digest(), want, reason: '$cut');
expect(h.digest, throwsStateError);
expect(() => h.add([1]), throwsStateError);
}
final empty = Sha256Hasher()..add(Uint8List(0));
expect(empty.digest(), crypto.sha256.convert(const []).bytes);
});
test('of a stream', () async {
final stream = Stream.fromIterable([
dkc.sublist(0, 1),
dkc.sublist(1, 70000),
dkc.sublist(70000),
]);
expect(await sha256Stream(stream), want);
expect(
await sha256Stream(const Stream.empty()),
crypto.sha256.convert(const []).bytes,
);
});
test('a different one is ERR_ACCESS_INVALID', () {
checkCapsuleDigest(capsuleDigest(dkc), want);
final other = Uint8List.fromList(want)..[31] ^= 1;
expect(
codeOf(() => checkCapsuleDigest(capsuleDigest(dkc), other)),
'ERR_ACCESS_INVALID',
);
expect(
codeOf(
() => checkCapsuleDigest(capsuleDigest(dkc), want.sublist(0, 31)),
),
'ERR_ACCESS_INVALID',
);
});
});
}

@ -0,0 +1,203 @@
// 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;
}
Loading…
Cancel
Save

Powered by TurnKey Linux.