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/lib/src/chacha20poly1305.dart

538 lines
16 KiB

/// ChaCha20, Poly1305 and the AEAD ChaCha20-Poly1305 of RFC 8439, which age
/// uses to wrap file keys and for its STREAM.
///
/// The arithmetic is exact on the VM and when compiled to JavaScript:
/// - ChaCha20 adds two 32-bit words at a time and masks the sum, and its
/// rotations are of 32-bit values, so that the 32-bit bit operators of the
/// web give what the 64-bit ones of the VM give;
/// - Poly1305 keeps its accumulator and r in ten limbs of 13 bits. A product
/// of a limb of h (below 2^15) and one of 5·r (below 2^16) is below 2^31,
/// and a sum of ten of them and a carry is below 2^35: far from 2^53, where
/// a double stops being exact. The carries of those sums, which may pass
/// 2^32, are taken with `~/` and the low bits with `&`, never with a shift
/// of a value above 2^32, which the web would truncate.
///
/// The tag is compared in constant time. The rest is written without
/// branches or indexes that depend on secrets, but neither the VM nor a
/// JavaScript engine promises constant time.
///
/// Internal: lib/datekeys.dart does not export it.
library;
import 'dart:typed_data';
const _mask32 = 0xffffffff;
/// The size of a ChaCha20 key.
const chachaKeySize = 32;
/// The size of the nonce of ChaCha20-Poly1305 (RFC 8439).
const chachaNonceSize = 12;
/// The size of a Poly1305 tag, the overhead of ChaCha20-Poly1305.
const poly1305TagSize = 16;
// ---------------------------------------------------------------------------
// ChaCha20
int _le32(List<int> b, int o) =>
b[o] | b[o + 1] << 8 | b[o + 2] << 16 | (b[o + 3] << 24 & _mask32);
/// The ChaCha20 keystream of RFC 8439, 2.4: [key] (32 bytes), [nonce] (12
/// bytes) and the block counter from [counter].
final class ChaCha20 {
/// The cipher of [key] and [nonce], from block [counter].
ChaCha20(List<int> key, List<int> nonce, [int counter = 0]) {
if (key.length != chachaKeySize) {
throw ArgumentError.value(key.length, 'key', 'not 32 bytes');
}
if (nonce.length != chachaNonceSize) {
throw ArgumentError.value(nonce.length, 'nonce', 'not 12 bytes');
}
if (counter < 0 || counter > _mask32) {
throw RangeError.range(counter, 0, _mask32, 'counter');
}
_input[0] = 0x61707865;
_input[1] = 0x3320646e;
_input[2] = 0x79622d32;
_input[3] = 0x6b206574;
for (var i = 0; i < 8; i++) {
_input[4 + i] = _le32(key, 4 * i);
}
_input[12] = counter;
for (var i = 0; i < 3; i++) {
_input[13 + i] = _le32(nonce, 4 * i);
}
}
final Uint32List _input = Uint32List(16);
final Uint32List _x = Uint32List(16);
final Uint8List _stream = Uint8List(64);
int _used = 64;
bool _overflow = false;
// The next 64 bytes of keystream into _stream, and the counter advanced.
void _block() {
// Go's chacha20 refuses a block past counter 2^32 - 1, which RFC 8439
// leaves undefined.
if (_overflow) throw StateError('chacha20: counter overflow');
final s = _input;
var x0 = s[0], x1 = s[1], x2 = s[2], x3 = s[3];
var x4 = s[4], x5 = s[5], x6 = s[6], x7 = s[7];
var x8 = s[8], x9 = s[9], x10 = s[10], x11 = s[11];
var x12 = s[12], x13 = s[13], x14 = s[14], x15 = s[15];
for (var i = 0; i < 10; i++) {
// Column rounds.
x0 = (x0 + x4) & _mask32;
x12 ^= x0;
x12 = (x12 << 16 & _mask32) | x12 >>> 16;
x8 = (x8 + x12) & _mask32;
x4 ^= x8;
x4 = (x4 << 12 & _mask32) | x4 >>> 20;
x0 = (x0 + x4) & _mask32;
x12 ^= x0;
x12 = (x12 << 8 & _mask32) | x12 >>> 24;
x8 = (x8 + x12) & _mask32;
x4 ^= x8;
x4 = (x4 << 7 & _mask32) | x4 >>> 25;
x1 = (x1 + x5) & _mask32;
x13 ^= x1;
x13 = (x13 << 16 & _mask32) | x13 >>> 16;
x9 = (x9 + x13) & _mask32;
x5 ^= x9;
x5 = (x5 << 12 & _mask32) | x5 >>> 20;
x1 = (x1 + x5) & _mask32;
x13 ^= x1;
x13 = (x13 << 8 & _mask32) | x13 >>> 24;
x9 = (x9 + x13) & _mask32;
x5 ^= x9;
x5 = (x5 << 7 & _mask32) | x5 >>> 25;
x2 = (x2 + x6) & _mask32;
x14 ^= x2;
x14 = (x14 << 16 & _mask32) | x14 >>> 16;
x10 = (x10 + x14) & _mask32;
x6 ^= x10;
x6 = (x6 << 12 & _mask32) | x6 >>> 20;
x2 = (x2 + x6) & _mask32;
x14 ^= x2;
x14 = (x14 << 8 & _mask32) | x14 >>> 24;
x10 = (x10 + x14) & _mask32;
x6 ^= x10;
x6 = (x6 << 7 & _mask32) | x6 >>> 25;
x3 = (x3 + x7) & _mask32;
x15 ^= x3;
x15 = (x15 << 16 & _mask32) | x15 >>> 16;
x11 = (x11 + x15) & _mask32;
x7 ^= x11;
x7 = (x7 << 12 & _mask32) | x7 >>> 20;
x3 = (x3 + x7) & _mask32;
x15 ^= x3;
x15 = (x15 << 8 & _mask32) | x15 >>> 24;
x11 = (x11 + x15) & _mask32;
x7 ^= x11;
x7 = (x7 << 7 & _mask32) | x7 >>> 25;
// Diagonal rounds.
x0 = (x0 + x5) & _mask32;
x15 ^= x0;
x15 = (x15 << 16 & _mask32) | x15 >>> 16;
x10 = (x10 + x15) & _mask32;
x5 ^= x10;
x5 = (x5 << 12 & _mask32) | x5 >>> 20;
x0 = (x0 + x5) & _mask32;
x15 ^= x0;
x15 = (x15 << 8 & _mask32) | x15 >>> 24;
x10 = (x10 + x15) & _mask32;
x5 ^= x10;
x5 = (x5 << 7 & _mask32) | x5 >>> 25;
x1 = (x1 + x6) & _mask32;
x12 ^= x1;
x12 = (x12 << 16 & _mask32) | x12 >>> 16;
x11 = (x11 + x12) & _mask32;
x6 ^= x11;
x6 = (x6 << 12 & _mask32) | x6 >>> 20;
x1 = (x1 + x6) & _mask32;
x12 ^= x1;
x12 = (x12 << 8 & _mask32) | x12 >>> 24;
x11 = (x11 + x12) & _mask32;
x6 ^= x11;
x6 = (x6 << 7 & _mask32) | x6 >>> 25;
x2 = (x2 + x7) & _mask32;
x13 ^= x2;
x13 = (x13 << 16 & _mask32) | x13 >>> 16;
x8 = (x8 + x13) & _mask32;
x7 ^= x8;
x7 = (x7 << 12 & _mask32) | x7 >>> 20;
x2 = (x2 + x7) & _mask32;
x13 ^= x2;
x13 = (x13 << 8 & _mask32) | x13 >>> 24;
x8 = (x8 + x13) & _mask32;
x7 ^= x8;
x7 = (x7 << 7 & _mask32) | x7 >>> 25;
x3 = (x3 + x4) & _mask32;
x14 ^= x3;
x14 = (x14 << 16 & _mask32) | x14 >>> 16;
x9 = (x9 + x14) & _mask32;
x4 ^= x9;
x4 = (x4 << 12 & _mask32) | x4 >>> 20;
x3 = (x3 + x4) & _mask32;
x14 ^= x3;
x14 = (x14 << 8 & _mask32) | x14 >>> 24;
x9 = (x9 + x14) & _mask32;
x4 ^= x9;
x4 = (x4 << 7 & _mask32) | x4 >>> 25;
}
final x = _x;
x[0] = x0 + s[0];
x[1] = x1 + s[1];
x[2] = x2 + s[2];
x[3] = x3 + s[3];
x[4] = x4 + s[4];
x[5] = x5 + s[5];
x[6] = x6 + s[6];
x[7] = x7 + s[7];
x[8] = x8 + s[8];
x[9] = x9 + s[9];
x[10] = x10 + s[10];
x[11] = x11 + s[11];
x[12] = x12 + s[12];
x[13] = x13 + s[13];
x[14] = x14 + s[14];
x[15] = x15 + s[15];
// A Uint32List keeps the low 32 bits of each sum, on the VM and on the
// web alike.
final out = _stream;
for (var i = 0; i < 16; i++) {
final v = x[i];
out[4 * i] = v & 0xff;
out[4 * i + 1] = v >>> 8 & 0xff;
out[4 * i + 2] = v >>> 16 & 0xff;
out[4 * i + 3] = v >>> 24;
}
if (s[12] == _mask32) {
_overflow = true;
} else {
s[12] = s[12] + 1;
}
_used = 0;
}
/// XORs the keystream into [data] from [start] to [end], in place.
void xorInPlace(Uint8List data, [int start = 0, int? end]) {
final stop = end ?? data.length;
RangeError.checkValidRange(start, stop, data.length);
for (var i = start; i < stop; i++) {
if (_used == 64) _block();
data[i] ^= _stream[_used++];
}
}
/// The next [n] bytes of keystream.
Uint8List keystream(int n) {
final out = Uint8List(n);
xorInPlace(out);
return out;
}
/// Clears the key and the keystream.
void wipe() {
_input.fillRange(0, 16, 0);
_x.fillRange(0, 16, 0);
_stream.fillRange(0, 64, 0);
_used = 64;
}
}
// ---------------------------------------------------------------------------
// Poly1305
const _limbBits = 13;
const _limbMask = 0x1fff;
const _limbRadix = 0x2000;
const _limbs = 10;
// The ten 13-bit limbs of the little-endian integer in pad[0..16]; pad has
// at least 19 bytes, those above the integer zero.
void _limbsOf(Uint8List pad, Int32List out) {
for (var i = 0; i < _limbs; i++) {
final bit = _limbBits * i;
final byte = bit >>> 3;
final v = pad[byte] | pad[byte + 1] << 8 | pad[byte + 2] << 16;
out[i] = v >>> (bit & 7) & _limbMask;
}
}
/// Poly1305 (RFC 8439, 2.5) with a one-time key of 32 bytes: r and s.
final class Poly1305 {
/// The MAC of the one-time [key], 32 bytes.
Poly1305(List<int> key) {
if (key.length != 32) {
throw ArgumentError.value(key.length, 'key', 'not 32 bytes');
}
final pad = _pad;
for (var i = 0; i < 16; i++) {
pad[i] = key[i];
}
// Clamping, RFC 8439 2.5.1.
pad[3] &= 15;
pad[7] &= 15;
pad[11] &= 15;
pad[15] &= 15;
pad[4] &= 252;
pad[8] &= 252;
pad[12] &= 252;
_limbsOf(pad, _r);
for (var i = 0; i < 16; i++) {
_s[i] = key[16 + i];
}
pad.fillRange(0, pad.length, 0);
}
final Int32List _r = Int32List(_limbs);
final Int32List _h = Int32List(_limbs);
final Int32List _m = Int32List(_limbs);
final Uint8List _s = Uint8List(16);
final Uint8List _pad = Uint8List(20);
int _buffered = 0;
/// Adds [data] to the message.
void add(List<int> data, [int start = 0, int? end]) {
final stop = end ?? data.length;
RangeError.checkValidRange(start, stop, data.length);
var i = start;
final pad = _pad;
if (_buffered > 0) {
while (_buffered < 16 && i < stop) {
pad[_buffered++] = data[i++];
}
if (_buffered < 16) return;
_block(16);
}
for (; i + 16 <= stop; i += 16) {
for (var k = 0; k < 16; k++) {
pad[k] = data[i + k];
}
_block(16);
}
while (i < stop) {
pad[_buffered++] = data[i++];
}
}
// h = (h + the block of the first n bytes of _pad, with the byte 1 after
// them) · r mod 2^130 - 5.
void _block(int n) {
final pad = _pad;
pad[n] = 1;
pad.fillRange(n + 1, pad.length, 0);
final h = _h;
final r = _r;
final m = _m;
_limbsOf(pad, m);
pad.fillRange(0, pad.length, 0);
_buffered = 0;
for (var i = 0; i < _limbs; i++) {
h[i] += m[i];
}
// d_i = the sum over j of h_j · r_(i-j), where a limb past 2^130 comes
// back multiplied by 5, as 2^130 = 5 mod p. Each d_i is below 2^35.
var carry = 0;
for (var i = 0; i < _limbs; i++) {
var d = carry;
for (var j = 0; j <= i; j++) {
d += h[j] * r[i - j];
}
for (var j = i + 1; j < _limbs; j++) {
d += h[j] * 5 * r[i - j + _limbs];
}
carry = d ~/ _limbRadix;
m[i] = d & _limbMask;
}
// The carry out of limb 9 is a multiple of 2^130: it comes back · 5.
final v = m[0] + carry * 5;
h[0] = v & _limbMask;
h[1] = m[1] + v ~/ _limbRadix;
for (var i = 2; i < _limbs; i++) {
h[i] = m[i];
}
}
/// The 16-byte tag of the message. The object is wiped and cannot be used
/// again.
Uint8List finish() {
if (_buffered > 0) _block(_buffered);
final h = _h;
// Two full carries: every limb below 2^13, but for h_1, which may reach
// 2^13 by one, and h below 2^130 + 2^26.
for (var round = 0; round < 2; round++) {
var c = 0;
for (var i = 0; i < _limbs; i++) {
final v = h[i] + c;
c = v ~/ _limbRadix;
h[i] = v & _limbMask;
}
final v = h[0] + c * 5;
h[0] = v & _limbMask;
h[1] += v ~/ _limbRadix;
}
// g = h + 5 - 2^130. The carry out of limb 9 is 1 exactly when
// h + 5 >= 2^130, that is h >= p, and then h mod p = g.
final g = _m;
var c = 5;
for (var i = 0; i < _limbs; i++) {
final v = h[i] + c;
c = v ~/ _limbRadix;
g[i] = v & _limbMask;
}
final keep = 1 - c;
for (var i = 0; i < _limbs; i++) {
h[i] = keep * h[i] + c * g[i];
}
// (h + s) mod 2^128, little-endian. The limbs are added, not ORed, so
// that h_1 = 2^13 carries into the next one.
final tag = Uint8List(16);
var acc = 0;
var bits = 0;
var limb = 0;
var carry = 0;
for (var i = 0; i < 16; i++) {
while (bits < 8) {
acc += h[limb++] << bits;
bits += _limbBits;
}
final v = (acc & 0xff) + _s[i] + carry;
tag[i] = v & 0xff;
carry = v >>> 8;
acc >>>= 8;
bits -= 8;
}
wipe();
return tag;
}
/// Clears the key and the state.
void wipe() {
_r.fillRange(0, _limbs, 0);
_h.fillRange(0, _limbs, 0);
_m.fillRange(0, _limbs, 0);
_s.fillRange(0, 16, 0);
_pad.fillRange(0, _pad.length, 0);
_buffered = 0;
}
}
/// The Poly1305 tag of [message] under the one-time [key].
Uint8List poly1305(List<int> key, List<int> message) =>
(Poly1305(key)..add(message)).finish();
/// Whether [a] and [b] are equal, in a time that depends only on their
/// lengths.
bool constantTimeEquals(List<int> a, List<int> b) {
if (a.length != b.length) return false;
var d = 0;
for (var i = 0; i < a.length; i++) {
d |= a[i] ^ b[i];
}
return d == 0;
}
// ---------------------------------------------------------------------------
// ChaCha20-Poly1305
final _zeros = Uint8List(16);
Uint8List _tag(
ChaCha20 cipher,
List<int> aad,
Uint8List ciphertext,
int start,
int end,
) =>
// The one-time key is the first 32 bytes of block 0 (RFC 8439, 2.6); the
// rest of that block is discarded, and the message starts at block 1.
_tagWith(cipher.keystream(64), aad, ciphertext, start, end);
// An int below 2^53 as 8 little-endian bytes, without a 64-bit shift.
Uint8List _le64(int v) {
final out = Uint8List(8);
var x = v;
for (var i = 0; i < 8; i++) {
out[i] = x & 0xff;
x = x ~/ 256;
}
return out;
}
/// The AEAD ChaCha20-Poly1305 of RFC 8439, 2.8, as Go's chacha20poly1305:
/// the ciphertext followed by the 16-byte tag.
Uint8List chacha20Poly1305Seal(
List<int> key,
List<int> nonce,
List<int> plaintext, [
List<int> aad = const [],
]) {
final cipher = ChaCha20(key, nonce);
final out = Uint8List(plaintext.length + poly1305TagSize);
out.setRange(0, plaintext.length, plaintext);
try {
// Block 0 gives the one-time key; the message starts at block 1.
final otk = cipher.keystream(64);
cipher.xorInPlace(out, 0, plaintext.length);
final tag = _tagWith(otk, aad, out, 0, plaintext.length);
out.setRange(plaintext.length, out.length, tag);
return out;
} finally {
cipher.wipe();
}
}
/// Opens the [ciphertext] (with its tag) of ChaCha20-Poly1305: the plaintext,
/// or null when the tag does not verify. The tag is compared in constant
/// time, and nothing is decrypted before it verifies.
Uint8List? chacha20Poly1305Open(
List<int> key,
List<int> nonce,
Uint8List ciphertext, [
List<int> aad = const [],
]) {
if (ciphertext.length < poly1305TagSize) return null;
final n = ciphertext.length - poly1305TagSize;
final cipher = ChaCha20(key, nonce);
try {
final want = _tag(cipher, aad, ciphertext, 0, n);
final got = Uint8List.sublistView(ciphertext, n);
if (!constantTimeEquals(want, got)) return null;
final out = Uint8List.fromList(Uint8List.sublistView(ciphertext, 0, n));
cipher.xorInPlace(out);
return out;
} finally {
cipher.wipe();
}
}
Uint8List _tagWith(
Uint8List otk,
List<int> aad,
Uint8List ciphertext,
int start,
int end,
) {
final mac = Poly1305(otk.sublist(0, 32));
otk.fillRange(0, otk.length, 0);
final n = end - start;
mac
..add(aad)
..add(_zeros, 0, (16 - aad.length % 16) % 16)
..add(ciphertext, start, end)
..add(_zeros, 0, (16 - n % 16) % 16)
..add(_le64(aad.length))
..add(_le64(n));
return mac.finish();
}

Powered by TurnKey Linux.