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.
538 lines
16 KiB
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();
|
|
}
|