working rss feed to nostr publish
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
function number(n: number) {
|
||||
if (!Number.isSafeInteger(n) || n < 0) throw new Error(`Wrong positive integer: ${n}`);
|
||||
}
|
||||
|
||||
function bool(b: boolean) {
|
||||
if (typeof b !== 'boolean') throw new Error(`Expected boolean, not ${b}`);
|
||||
}
|
||||
|
||||
function bytes(b: Uint8Array | undefined, ...lengths: number[]) {
|
||||
if (!(b instanceof Uint8Array)) throw new Error('Expected Uint8Array');
|
||||
if (lengths.length > 0 && !lengths.includes(b.length))
|
||||
throw new Error(`Expected Uint8Array of length ${lengths}, not of length=${b.length}`);
|
||||
}
|
||||
|
||||
export type Hash = {
|
||||
(data: Uint8Array): Uint8Array;
|
||||
blockLen: number;
|
||||
outputLen: number;
|
||||
create: any;
|
||||
};
|
||||
function hash(hash: Hash) {
|
||||
if (typeof hash !== 'function' || typeof hash.create !== 'function')
|
||||
throw new Error('hash must be wrapped by utils.wrapConstructor');
|
||||
number(hash.outputLen);
|
||||
number(hash.blockLen);
|
||||
}
|
||||
|
||||
function exists(instance: any, checkFinished = true) {
|
||||
if (instance.destroyed) throw new Error('Hash instance has been destroyed');
|
||||
if (checkFinished && instance.finished) throw new Error('Hash#digest() has already been called');
|
||||
}
|
||||
function output(out: any, instance: any) {
|
||||
bytes(out);
|
||||
const min = instance.outputLen;
|
||||
if (out.length < min) {
|
||||
throw new Error(`digestInto() expects output buffer of length at least ${min}`);
|
||||
}
|
||||
}
|
||||
|
||||
export { number, bool, bytes, hash, exists, output };
|
||||
const assert = { number, bool, bytes, hash, exists, output };
|
||||
export default assert;
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
||||
|
||||
// micro-noble-ciphers: more auditable, but slower version of salsa20, chacha & poly1305.
|
||||
// Implements the same algorithms that are present in other files,
|
||||
// but without unrolled loops (https://en.wikipedia.org/wiki/Loop_unrolling).
|
||||
|
||||
import * as u from './utils.js';
|
||||
import { salsaBasic } from './_salsa.js';
|
||||
// Utils
|
||||
function hexToNumber(hex: string): bigint {
|
||||
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
|
||||
// Big Endian
|
||||
return BigInt(hex === '' ? '0' : `0x${hex}`);
|
||||
}
|
||||
function bytesToNumberLE(bytes: Uint8Array): bigint {
|
||||
return hexToNumber(u.bytesToHex(Uint8Array.from(bytes).reverse()));
|
||||
}
|
||||
function numberToBytesLE(n: number | bigint, len: number): Uint8Array {
|
||||
return u.hexToBytes(n.toString(16).padStart(len * 2, '0')).reverse();
|
||||
}
|
||||
|
||||
const rotl = (a: number, b: number) => (a << b) | (a >>> (32 - b));
|
||||
// /Utils
|
||||
|
||||
function salsaQR(x: Uint32Array, a: number, b: number, c: number, d: number) {
|
||||
x[b] ^= rotl((x[a] + x[d]) | 0, 7);
|
||||
x[c] ^= rotl((x[b] + x[a]) | 0, 9);
|
||||
x[d] ^= rotl((x[c] + x[b]) | 0, 13);
|
||||
x[a] ^= rotl((x[d] + x[c]) | 0, 18);
|
||||
}
|
||||
// prettier-ignore
|
||||
function chachaQR(x: Uint32Array, a: number, b: number, c: number, d: number) {
|
||||
x[a] = (x[a] + x[b]) | 0; x[d] = rotl(x[d] ^ x[a], 16);
|
||||
x[c] = (x[c] + x[d]) | 0; x[b] = rotl(x[b] ^ x[c], 12);
|
||||
x[a] = (x[a] + x[b]) | 0; x[d] = rotl(x[d] ^ x[a], 8);
|
||||
x[c] = (x[c] + x[d]) | 0; x[b] = rotl(x[b] ^ x[c], 7);
|
||||
}
|
||||
|
||||
function salsaRound(x: Uint32Array, rounds = 20) {
|
||||
for (let i = 0; i < rounds; i += 2) {
|
||||
salsaQR(x, 0, 4, 8, 12);
|
||||
salsaQR(x, 5, 9, 13, 1);
|
||||
salsaQR(x, 10, 14, 2, 6);
|
||||
salsaQR(x, 15, 3, 7, 11);
|
||||
salsaQR(x, 0, 1, 2, 3);
|
||||
salsaQR(x, 5, 6, 7, 4);
|
||||
salsaQR(x, 10, 11, 8, 9);
|
||||
salsaQR(x, 15, 12, 13, 14);
|
||||
}
|
||||
}
|
||||
|
||||
function chachaRound(x: Uint32Array, rounds = 20) {
|
||||
for (let i = 0; i < rounds; i += 2) {
|
||||
chachaQR(x, 0, 4, 8, 12);
|
||||
chachaQR(x, 1, 5, 9, 13);
|
||||
chachaQR(x, 2, 6, 10, 14);
|
||||
chachaQR(x, 3, 7, 11, 15);
|
||||
chachaQR(x, 0, 5, 10, 15);
|
||||
chachaQR(x, 1, 6, 11, 12);
|
||||
chachaQR(x, 2, 7, 8, 13);
|
||||
chachaQR(x, 3, 4, 9, 14);
|
||||
}
|
||||
}
|
||||
|
||||
function salsaCore(
|
||||
c: Uint32Array,
|
||||
k: Uint32Array,
|
||||
n: Uint32Array,
|
||||
out: Uint32Array,
|
||||
cnt: number,
|
||||
rounds = 20
|
||||
): void {
|
||||
// prettier-ignore
|
||||
const y = new Uint32Array([
|
||||
c[0], k[0], k[1], k[2], // "expa" Key Key Key
|
||||
k[3], c[1], n[0], n[1], // Key "nd 3" Nonce Nonce
|
||||
cnt, 0 , c[2], k[4], // Pos. Pos. "2-by" Key
|
||||
k[5], k[6], k[7], c[3], // Key Key Key "te k"
|
||||
]);
|
||||
const x = y.slice();
|
||||
salsaRound(x, rounds);
|
||||
for (let i = 0; i < 16; i++) out[i] = (y[i] + x[i]) | 0;
|
||||
}
|
||||
|
||||
export function hsalsa(c: Uint32Array, key: Uint8Array, nonce: Uint8Array): Uint8Array {
|
||||
const k = u.u32(key);
|
||||
const i = u.u32(nonce);
|
||||
// prettier-ignore
|
||||
const x = new Uint32Array([
|
||||
c[0], k[0], k[1], k[2],
|
||||
k[3], c[1], i[0], i[1],
|
||||
i[2], i[3], c[2], k[4],
|
||||
k[5], k[6], k[7], c[3]
|
||||
]);
|
||||
salsaRound(x);
|
||||
return u.u8(new Uint32Array([x[0], x[5], x[10], x[15], x[6], x[7], x[8], x[9]]));
|
||||
}
|
||||
|
||||
function chachaCore(
|
||||
c: Uint32Array,
|
||||
k: Uint32Array,
|
||||
n: Uint32Array,
|
||||
out: Uint32Array,
|
||||
cnt: number,
|
||||
rounds = 20
|
||||
): void {
|
||||
// prettier-ignore
|
||||
const y = new Uint32Array([
|
||||
c[0], c[1], c[2], c[3], // "expa" "nd 3" "2-by" "te k"
|
||||
k[0], k[1], k[2], k[3], // Key Key Key Key
|
||||
k[4], k[5], k[6], k[7], // Key Key Key Key
|
||||
cnt, n[0], n[1], n[2], // Counter Counter Nonce Nonce
|
||||
]);
|
||||
const x = y.slice();
|
||||
chachaRound(x, rounds);
|
||||
for (let i = 0; i < 16; i++) out[i] = (y[i] + x[i]) | 0;
|
||||
}
|
||||
|
||||
export function hchacha(c: Uint32Array, key: Uint8Array, nonce: Uint8Array): Uint8Array {
|
||||
const k = u.u32(key);
|
||||
const i = u.u32(nonce);
|
||||
// prettier-ignore
|
||||
const x = new Uint32Array([
|
||||
c[0], c[1], c[2], c[3],
|
||||
k[0], k[1], k[2], k[3],
|
||||
k[4], k[5], k[6], k[7],
|
||||
i[0], i[1], i[2], i[3],
|
||||
]);
|
||||
chachaRound(x);
|
||||
return u.u8(new Uint32Array([x[0], x[1], x[2], x[3], x[12], x[13], x[14], x[15]]));
|
||||
}
|
||||
|
||||
/**
|
||||
* salsa20, 12-byte nonce.
|
||||
*/
|
||||
export const salsa20 = salsaBasic({ core: salsaCore, counterRight: true });
|
||||
|
||||
/**
|
||||
* xsalsa20, 24-byte nonce.
|
||||
*/
|
||||
export const xsalsa20 = salsaBasic({
|
||||
core: salsaCore,
|
||||
counterRight: true,
|
||||
extendNonceFn: hsalsa,
|
||||
allow128bitKeys: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* chacha20 non-RFC, original version by djb. 8-byte nonce, 8-byte counter.
|
||||
*/
|
||||
export const chacha20orig = salsaBasic({ core: chachaCore, counterRight: false, counterLen: 8 });
|
||||
/**
|
||||
* chacha20 RFC 8439 (IETF / TLS). 12-byte nonce, 4-byte counter.
|
||||
*/
|
||||
export const chacha20 = salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 4,
|
||||
allow128bitKeys: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* xchacha20 eXtended-nonce. https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||
*/
|
||||
export const xchacha20 = salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 8,
|
||||
extendNonceFn: hchacha,
|
||||
allow128bitKeys: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 8-round chacha from the original paper.
|
||||
*/
|
||||
export const chacha8 = salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 4,
|
||||
rounds: 8,
|
||||
});
|
||||
|
||||
/**
|
||||
* 12-round chacha from the original paper.
|
||||
*/
|
||||
export const chacha12 = salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 4,
|
||||
rounds: 12,
|
||||
});
|
||||
|
||||
const POW_2_130_5 = 2n ** 130n - 5n;
|
||||
const POW_2_128_1 = 2n ** (16n * 8n) - 1n;
|
||||
// Can be speed-up using BigUint64Array, but would be more complicated
|
||||
export function poly1305(msg: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
u.ensureBytes(msg);
|
||||
u.ensureBytes(key);
|
||||
let acc = 0n;
|
||||
const r = bytesToNumberLE(key.subarray(0, 16)) & 0x0ffffffc0ffffffc0ffffffc0fffffffn;
|
||||
const s = bytesToNumberLE(key.subarray(16));
|
||||
// Process by 16 byte chunks
|
||||
for (let i = 0; i < msg.length; i += 16) {
|
||||
const m = msg.subarray(i, i + 16);
|
||||
const n = bytesToNumberLE(m) | (1n << BigInt(8 * m.length));
|
||||
acc = ((acc + n) * r) % POW_2_130_5;
|
||||
}
|
||||
const res = (acc + s) & POW_2_128_1;
|
||||
return numberToBytesLE(res, 16);
|
||||
}
|
||||
|
||||
function computeTag(
|
||||
fn: typeof chacha20,
|
||||
key: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
ciphertext: Uint8Array,
|
||||
AAD?: Uint8Array
|
||||
): Uint8Array {
|
||||
const res = [];
|
||||
if (AAD) {
|
||||
res.push(AAD);
|
||||
const leftover = AAD.length % 16;
|
||||
if (leftover > 0) res.push(new Uint8Array(16 - leftover));
|
||||
}
|
||||
res.push(ciphertext);
|
||||
const leftover = ciphertext.length % 16;
|
||||
if (leftover > 0) res.push(new Uint8Array(16 - leftover));
|
||||
// Lengths
|
||||
const num = new Uint8Array(16);
|
||||
const view = u.createView(num);
|
||||
u.setBigUint64(view, 0, BigInt(AAD ? AAD.length : 0), true);
|
||||
u.setBigUint64(view, 8, BigInt(ciphertext.length), true);
|
||||
res.push(num);
|
||||
const authKey = fn(key, nonce, new Uint8Array(32));
|
||||
return poly1305(u.concatBytes(...res), authKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* xsalsa20-poly1305 eXtended-nonce (24 bytes) salsa.
|
||||
*/
|
||||
export function xsalsa20poly1305(key: Uint8Array, nonce: Uint8Array) {
|
||||
u.ensureBytes(key);
|
||||
u.ensureBytes(nonce);
|
||||
return {
|
||||
encrypt: (plaintext: Uint8Array) => {
|
||||
u.ensureBytes(plaintext);
|
||||
const m = u.concatBytes(new Uint8Array(32), plaintext);
|
||||
const c = xsalsa20(key, nonce, m);
|
||||
const authKey = c.subarray(0, 32);
|
||||
const data = c.subarray(32);
|
||||
const tag = poly1305(data, authKey);
|
||||
return u.concatBytes(tag, data);
|
||||
},
|
||||
decrypt: (ciphertext: Uint8Array) => {
|
||||
u.ensureBytes(ciphertext);
|
||||
if (ciphertext.length < 16) throw new Error('encrypted data must be at least 16 bytes');
|
||||
const c = u.concatBytes(new Uint8Array(16), ciphertext);
|
||||
const authKey = xsalsa20(key, nonce, new Uint8Array(32));
|
||||
const tag = poly1305(c.subarray(32), authKey);
|
||||
if (!u.equalBytes(c.subarray(16, 32), tag)) throw new Error('invalid poly1305 tag');
|
||||
return xsalsa20(key, nonce, c).subarray(32);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias to xsalsa20-poly1305
|
||||
*/
|
||||
export function secretbox(key: Uint8Array, nonce: Uint8Array) {
|
||||
u.ensureBytes(key);
|
||||
u.ensureBytes(nonce);
|
||||
const xs = xsalsa20poly1305(key, nonce);
|
||||
return { seal: xs.encrypt, open: xs.decrypt };
|
||||
}
|
||||
|
||||
export const _poly1305_aead =
|
||||
(fn: typeof chacha20) =>
|
||||
(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): u.Cipher => {
|
||||
const tagLength = 16;
|
||||
const keyLength = 32;
|
||||
u.ensureBytes(key, keyLength);
|
||||
u.ensureBytes(nonce);
|
||||
return {
|
||||
tagLength,
|
||||
encrypt: (plaintext: Uint8Array) => {
|
||||
u.ensureBytes(plaintext);
|
||||
const res = fn(key, nonce, plaintext, undefined, 1);
|
||||
const tag = computeTag(fn, key, nonce, res, AAD);
|
||||
return u.concatBytes(res, tag);
|
||||
},
|
||||
decrypt: (ciphertext: Uint8Array) => {
|
||||
u.ensureBytes(ciphertext);
|
||||
if (ciphertext.length < tagLength)
|
||||
throw new Error(`encrypted data must be at least ${tagLength} bytes`);
|
||||
const passedTag = ciphertext.subarray(-tagLength);
|
||||
const data = ciphertext.subarray(0, -tagLength);
|
||||
const tag = computeTag(fn, key, nonce, data, AAD);
|
||||
if (!u.equalBytes(passedTag, tag)) throw new Error('invalid poly1305 tag');
|
||||
return fn(key, nonce, data, undefined, 1);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* chacha20-poly1305 12-byte-nonce chacha.
|
||||
*/
|
||||
export const chacha20poly1305 = _poly1305_aead(chacha20);
|
||||
|
||||
/**
|
||||
* xchacha20-poly1305 eXtended-nonce (24 bytes) chacha.
|
||||
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||
*/
|
||||
export const xchacha20poly1305 = _poly1305_aead(xchacha20);
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
import { toBytes, Input, ensureBytes, Hash } from './utils.js';
|
||||
import assert from './_assert.js';
|
||||
|
||||
// Poly1305 is a fast and parallel secret-key message-authentication code.
|
||||
// https://cr.yp.to/mac.html, https://cr.yp.to/mac/poly1305-20050329.pdf
|
||||
// https://datatracker.ietf.org/doc/html/rfc8439
|
||||
|
||||
// Based on Public Domain poly1305-donna https://github.com/floodyberry/poly1305-donna
|
||||
const u8to16 = (a: Uint8Array, i: number) => (a[i++] & 0xff) | ((a[i++] & 0xff) << 8);
|
||||
class Poly1305 implements Hash<Poly1305> {
|
||||
readonly blockLen = 16;
|
||||
readonly outputLen = 16;
|
||||
private buffer = new Uint8Array(16);
|
||||
private r = new Uint16Array(10);
|
||||
private h = new Uint16Array(10);
|
||||
private pad = new Uint16Array(8);
|
||||
private pos = 0;
|
||||
protected finished = false;
|
||||
|
||||
constructor(key: Input) {
|
||||
key = toBytes(key);
|
||||
ensureBytes(key, 32);
|
||||
const t0 = u8to16(key, 0);
|
||||
const t1 = u8to16(key, 2);
|
||||
const t2 = u8to16(key, 4);
|
||||
const t3 = u8to16(key, 6);
|
||||
const t4 = u8to16(key, 8);
|
||||
const t5 = u8to16(key, 10);
|
||||
const t6 = u8to16(key, 12);
|
||||
const t7 = u8to16(key, 14);
|
||||
|
||||
// https://github.com/floodyberry/poly1305-donna/blob/e6ad6e091d30d7f4ec2d4f978be1fcfcbce72781/poly1305-donna-16.h#L47
|
||||
this.r[0] = t0 & 0x1fff;
|
||||
this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff;
|
||||
this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03;
|
||||
this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff;
|
||||
this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff;
|
||||
this.r[5] = (t4 >>> 1) & 0x1ffe;
|
||||
this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff;
|
||||
this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81;
|
||||
this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff;
|
||||
this.r[9] = (t7 >>> 5) & 0x007f;
|
||||
for (let i = 0; i < 8; i++) this.pad[i] = u8to16(key, 16 + 2 * i);
|
||||
}
|
||||
|
||||
private process(data: Uint8Array, offset: number, isLast = false) {
|
||||
const hibit = isLast ? 0 : 1 << 11;
|
||||
const { h, r } = this;
|
||||
const r0 = r[0];
|
||||
const r1 = r[1];
|
||||
const r2 = r[2];
|
||||
const r3 = r[3];
|
||||
const r4 = r[4];
|
||||
const r5 = r[5];
|
||||
const r6 = r[6];
|
||||
const r7 = r[7];
|
||||
const r8 = r[8];
|
||||
const r9 = r[9];
|
||||
|
||||
const t0 = u8to16(data, offset + 0);
|
||||
const t1 = u8to16(data, offset + 2);
|
||||
const t2 = u8to16(data, offset + 4);
|
||||
const t3 = u8to16(data, offset + 6);
|
||||
const t4 = u8to16(data, offset + 8);
|
||||
const t5 = u8to16(data, offset + 10);
|
||||
const t6 = u8to16(data, offset + 12);
|
||||
const t7 = u8to16(data, offset + 14);
|
||||
|
||||
let h0 = h[0] + (t0 & 0x1fff);
|
||||
let h1 = h[1] + (((t0 >>> 13) | (t1 << 3)) & 0x1fff);
|
||||
let h2 = h[2] + (((t1 >>> 10) | (t2 << 6)) & 0x1fff);
|
||||
let h3 = h[3] + (((t2 >>> 7) | (t3 << 9)) & 0x1fff);
|
||||
let h4 = h[4] + (((t3 >>> 4) | (t4 << 12)) & 0x1fff);
|
||||
let h5 = h[5] + ((t4 >>> 1) & 0x1fff);
|
||||
let h6 = h[6] + (((t4 >>> 14) | (t5 << 2)) & 0x1fff);
|
||||
let h7 = h[7] + (((t5 >>> 11) | (t6 << 5)) & 0x1fff);
|
||||
let h8 = h[8] + (((t6 >>> 8) | (t7 << 8)) & 0x1fff);
|
||||
let h9 = h[9] + ((t7 >>> 5) | hibit);
|
||||
|
||||
let c = 0;
|
||||
|
||||
let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6);
|
||||
c = d0 >>> 13;
|
||||
d0 &= 0x1fff;
|
||||
d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1);
|
||||
c += d0 >>> 13;
|
||||
d0 &= 0x1fff;
|
||||
|
||||
let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7);
|
||||
c = d1 >>> 13;
|
||||
d1 &= 0x1fff;
|
||||
d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2);
|
||||
c += d1 >>> 13;
|
||||
d1 &= 0x1fff;
|
||||
|
||||
let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8);
|
||||
c = d2 >>> 13;
|
||||
d2 &= 0x1fff;
|
||||
d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3);
|
||||
c += d2 >>> 13;
|
||||
d2 &= 0x1fff;
|
||||
|
||||
let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9);
|
||||
c = d3 >>> 13;
|
||||
d3 &= 0x1fff;
|
||||
d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4);
|
||||
c += d3 >>> 13;
|
||||
d3 &= 0x1fff;
|
||||
|
||||
let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
|
||||
c = d4 >>> 13;
|
||||
d4 &= 0x1fff;
|
||||
d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5);
|
||||
c += d4 >>> 13;
|
||||
d4 &= 0x1fff;
|
||||
|
||||
let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1;
|
||||
c = d5 >>> 13;
|
||||
d5 &= 0x1fff;
|
||||
d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6);
|
||||
c += d5 >>> 13;
|
||||
d5 &= 0x1fff;
|
||||
|
||||
let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2;
|
||||
c = d6 >>> 13;
|
||||
d6 &= 0x1fff;
|
||||
d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7);
|
||||
c += d6 >>> 13;
|
||||
d6 &= 0x1fff;
|
||||
|
||||
let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3;
|
||||
c = d7 >>> 13;
|
||||
d7 &= 0x1fff;
|
||||
d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8);
|
||||
c += d7 >>> 13;
|
||||
d7 &= 0x1fff;
|
||||
|
||||
let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4;
|
||||
c = d8 >>> 13;
|
||||
d8 &= 0x1fff;
|
||||
d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9);
|
||||
c += d8 >>> 13;
|
||||
d8 &= 0x1fff;
|
||||
|
||||
let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5;
|
||||
c = d9 >>> 13;
|
||||
d9 &= 0x1fff;
|
||||
d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0;
|
||||
c += d9 >>> 13;
|
||||
d9 &= 0x1fff;
|
||||
|
||||
c = ((c << 2) + c) | 0;
|
||||
c = (c + d0) | 0;
|
||||
d0 = c & 0x1fff;
|
||||
c = c >>> 13;
|
||||
d1 += c;
|
||||
|
||||
h[0] = d0;
|
||||
h[1] = d1;
|
||||
h[2] = d2;
|
||||
h[3] = d3;
|
||||
h[4] = d4;
|
||||
h[5] = d5;
|
||||
h[6] = d6;
|
||||
h[7] = d7;
|
||||
h[8] = d8;
|
||||
h[9] = d9;
|
||||
}
|
||||
|
||||
private finalize() {
|
||||
const { h, pad } = this;
|
||||
const g = new Uint16Array(10);
|
||||
let c = h[1] >>> 13;
|
||||
h[1] &= 0x1fff;
|
||||
for (let i = 2; i < 10; i++) {
|
||||
h[i] += c;
|
||||
c = h[i] >>> 13;
|
||||
h[i] &= 0x1fff;
|
||||
}
|
||||
h[0] += c * 5;
|
||||
c = h[0] >>> 13;
|
||||
h[0] &= 0x1fff;
|
||||
h[1] += c;
|
||||
c = h[1] >>> 13;
|
||||
h[1] &= 0x1fff;
|
||||
h[2] += c;
|
||||
|
||||
g[0] = h[0] + 5;
|
||||
c = g[0] >>> 13;
|
||||
g[0] &= 0x1fff;
|
||||
for (let i = 1; i < 10; i++) {
|
||||
g[i] = h[i] + c;
|
||||
c = g[i] >>> 13;
|
||||
g[i] &= 0x1fff;
|
||||
}
|
||||
g[9] -= 1 << 13;
|
||||
|
||||
let mask = (c ^ 1) - 1;
|
||||
for (let i = 0; i < 10; i++) g[i] &= mask;
|
||||
mask = ~mask;
|
||||
for (let i = 0; i < 10; i++) h[i] = (h[i] & mask) | g[i];
|
||||
h[0] = (h[0] | (h[1] << 13)) & 0xffff;
|
||||
h[1] = ((h[1] >>> 3) | (h[2] << 10)) & 0xffff;
|
||||
h[2] = ((h[2] >>> 6) | (h[3] << 7)) & 0xffff;
|
||||
h[3] = ((h[3] >>> 9) | (h[4] << 4)) & 0xffff;
|
||||
h[4] = ((h[4] >>> 12) | (h[5] << 1) | (h[6] << 14)) & 0xffff;
|
||||
h[5] = ((h[6] >>> 2) | (h[7] << 11)) & 0xffff;
|
||||
h[6] = ((h[7] >>> 5) | (h[8] << 8)) & 0xffff;
|
||||
h[7] = ((h[8] >>> 8) | (h[9] << 5)) & 0xffff;
|
||||
|
||||
let f = h[0] + pad[0];
|
||||
h[0] = f & 0xffff;
|
||||
for (let i = 1; i < 8; i++) {
|
||||
f = (((h[i] + pad[i]) | 0) + (f >>> 16)) | 0;
|
||||
h[i] = f & 0xffff;
|
||||
}
|
||||
}
|
||||
update(data: Input): this {
|
||||
assert.exists(this);
|
||||
const { buffer, blockLen } = this;
|
||||
data = toBytes(data);
|
||||
const len = data.length;
|
||||
|
||||
for (let pos = 0; pos < len; ) {
|
||||
const take = Math.min(blockLen - this.pos, len - pos);
|
||||
// Fast path: we have at least one block in input
|
||||
if (take === blockLen) {
|
||||
for (; blockLen <= len - pos; pos += blockLen) this.process(data, pos);
|
||||
continue;
|
||||
}
|
||||
buffer.set(data.subarray(pos, pos + take), this.pos);
|
||||
this.pos += take;
|
||||
pos += take;
|
||||
if (this.pos === blockLen) {
|
||||
this.process(buffer, 0, false);
|
||||
this.pos = 0;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
destroy() {
|
||||
this.h.fill(0);
|
||||
this.r.fill(0);
|
||||
this.buffer.fill(0);
|
||||
this.pad.fill(0);
|
||||
}
|
||||
digestInto(out: Uint8Array) {
|
||||
assert.exists(this);
|
||||
assert.output(out, this);
|
||||
this.finished = true;
|
||||
const { buffer, h } = this;
|
||||
let { pos } = this;
|
||||
if (pos) {
|
||||
buffer[pos++] = 1;
|
||||
// buffer.subarray(pos).fill(0);
|
||||
for (; pos < 16; pos++) buffer[pos] = 0;
|
||||
this.process(buffer, 0, true);
|
||||
}
|
||||
this.finalize();
|
||||
let opos = 0;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
out[opos++] = h[i] >>> 0;
|
||||
out[opos++] = h[i] >>> 8;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
digest(): Uint8Array {
|
||||
const { buffer, outputLen } = this;
|
||||
this.digestInto(buffer);
|
||||
const res = buffer.slice(0, outputLen);
|
||||
this.destroy();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export type CHash = ReturnType<typeof wrapConstructorWithKey>;
|
||||
export function wrapConstructorWithKey<H extends Hash<H>>(hashCons: (key: Input) => Hash<H>) {
|
||||
const hashC = (msg: Input, key: Input): Uint8Array => hashCons(key).update(toBytes(msg)).digest();
|
||||
const tmp = hashCons(new Uint8Array(32));
|
||||
hashC.outputLen = tmp.outputLen;
|
||||
hashC.blockLen = tmp.blockLen;
|
||||
hashC.create = (key: Input) => hashCons(key);
|
||||
return hashC;
|
||||
}
|
||||
|
||||
export const poly1305 = wrapConstructorWithKey((key) => new Poly1305(key));
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { u8, u32, ensureBytes } from './utils.js';
|
||||
|
||||
// AES-SIV polyval, little-endian "mirror image" of AES-GCM GHash
|
||||
// polynomial hash function. Defined in RFC 8452.
|
||||
|
||||
// Reverse bits in u32, constant-time, precompute will be faster, but non-constant time
|
||||
function rev32(x: number) {
|
||||
x = ((x & 0x5555_5555) << 1) | ((x >>> 1) & 0x5555_5555);
|
||||
x = ((x & 0x3333_3333) << 2) | ((x >>> 2) & 0x3333_3333);
|
||||
x = ((x & 0x0f0f_0f0f) << 4) | ((x >>> 4) & 0x0f0f_0f0f);
|
||||
x = ((x & 0x00ff_00ff) << 8) | ((x >>> 8) & 0x00ff_00ff);
|
||||
return (x << 16) | (x >>> 16);
|
||||
}
|
||||
|
||||
// wrapped 32 bit multiplication
|
||||
const wrapMul = (a: number, b: number) => Math.imul(a, b) >>> 0;
|
||||
|
||||
// https://timtaubert.de/blog/2017/06/verified-binary-multiplication-for-ghash/
|
||||
function bmul32(x: number, y: number) {
|
||||
const x0 = x & 0x1111_1111;
|
||||
const x1 = x & 0x2222_2222;
|
||||
const x2 = x & 0x4444_4444;
|
||||
const x3 = x & 0x8888_8888;
|
||||
const y0 = y & 0x1111_1111;
|
||||
const y1 = y & 0x2222_2222;
|
||||
const y2 = y & 0x4444_4444;
|
||||
const y3 = y & 0x8888_8888;
|
||||
let res = (wrapMul(x0, y0) ^ wrapMul(x1, y3) ^ wrapMul(x2, y2) ^ wrapMul(x3, y1)) & 0x1111_1111;
|
||||
res |= (wrapMul(x0, y1) ^ wrapMul(x1, y0) ^ wrapMul(x2, y3) ^ wrapMul(x3, y2)) & 0x2222_2222;
|
||||
res |= (wrapMul(x0, y2) ^ wrapMul(x1, y1) ^ wrapMul(x2, y0) ^ wrapMul(x3, y3)) & 0x4444_4444;
|
||||
res |= (wrapMul(x0, y3) ^ wrapMul(x1, y2) ^ wrapMul(x2, y1) ^ wrapMul(x3, y0)) & 0x8888_8888;
|
||||
return res >>> 0;
|
||||
}
|
||||
|
||||
function mulPart(arr: Uint32Array) {
|
||||
const a = new Uint32Array(18);
|
||||
a[0] = arr[0];
|
||||
a[1] = arr[1];
|
||||
a[2] = arr[2];
|
||||
a[3] = arr[3];
|
||||
a[4] = a[0] ^ a[1];
|
||||
a[5] = a[2] ^ a[3];
|
||||
a[6] = a[0] ^ a[2];
|
||||
a[7] = a[1] ^ a[3];
|
||||
a[8] = a[6] ^ a[7];
|
||||
a[9] = rev32(arr[0]);
|
||||
a[10] = rev32(arr[1]);
|
||||
a[11] = rev32(arr[2]);
|
||||
a[12] = rev32(arr[3]);
|
||||
a[13] = a[9] ^ a[10];
|
||||
a[14] = a[11] ^ a[12];
|
||||
a[15] = a[9] ^ a[11];
|
||||
a[16] = a[10] ^ a[12];
|
||||
a[17] = a[15] ^ a[16];
|
||||
return a;
|
||||
}
|
||||
|
||||
export function polyval(h: Uint8Array, data: Uint8Array) {
|
||||
ensureBytes(h);
|
||||
ensureBytes(data);
|
||||
const s = new Uint32Array(4);
|
||||
// Precompute for multiplication
|
||||
const a = mulPart(u32(h));
|
||||
if (data.length % 16) throw new Error('polyval: data must be padded to 16 bytes');
|
||||
const data32 = u32(data);
|
||||
for (let i = 0; i < data32.length; i += 4) {
|
||||
// Xor
|
||||
s[0] ^= data32[i + 0];
|
||||
s[1] ^= data32[i + 1];
|
||||
s[2] ^= data32[i + 2];
|
||||
s[3] ^= data32[i + 3];
|
||||
|
||||
// Dot via Karatsuba multiplication, based on MIT-licensed
|
||||
// https://bearssl.org/gitweb/?p=BearSSL;a=blob;f=src/hash/ghash_ctmul32.c;hb=4b6046412
|
||||
const b = mulPart(s);
|
||||
|
||||
const c = new Uint32Array(18);
|
||||
for (let i = 0; i < 18; i++) c[i] = bmul32(a[i], b[i]);
|
||||
c[4] ^= c[0] ^ c[1];
|
||||
c[5] ^= c[2] ^ c[3];
|
||||
c[8] ^= c[6] ^ c[7];
|
||||
c[13] ^= c[9] ^ c[10];
|
||||
c[14] ^= c[11] ^ c[12];
|
||||
c[17] ^= c[15] ^ c[16];
|
||||
|
||||
const zw = new Uint32Array(8);
|
||||
zw[0] = c[0];
|
||||
zw[1] = c[4] ^ (rev32(c[9]) >>> 1);
|
||||
zw[2] = c[1] ^ c[0] ^ c[2] ^ c[6] ^ (rev32(c[13]) >>> 1);
|
||||
zw[3] = c[4] ^ c[5] ^ c[8] ^ (rev32(c[10] ^ c[9] ^ c[11] ^ c[15]) >>> 1);
|
||||
zw[4] = c[2] ^ c[1] ^ c[3] ^ c[7] ^ (rev32(c[13] ^ c[14] ^ c[17]) >>> 1);
|
||||
zw[5] = c[5] ^ (rev32(c[11] ^ c[10] ^ c[12] ^ c[16]) >>> 1);
|
||||
zw[6] = c[3] ^ (rev32(c[14]) >>> 1);
|
||||
zw[7] = rev32(c[12]) >>> 1;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const lw = zw[i];
|
||||
zw[i + 4] ^= lw ^ (lw >>> 1) ^ (lw >>> 2) ^ (lw >>> 7);
|
||||
zw[i + 3] ^= (lw << 31) ^ (lw << 30) ^ (lw << 25);
|
||||
}
|
||||
|
||||
s[0] = zw[4];
|
||||
s[1] = zw[5];
|
||||
s[2] = zw[6];
|
||||
s[3] = zw[7];
|
||||
}
|
||||
return u8(s);
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
// Basic utils for salsa-like ciphers
|
||||
// Check out _micro.ts for descriptive documentation.
|
||||
import assert from './_assert.js';
|
||||
import { u32, utf8ToBytes, checkOpts } from './utils.js';
|
||||
|
||||
/*
|
||||
RFC8439 requires multi-step cipher stream, where
|
||||
authKey starts with counter: 0, actual msg with counter: 1.
|
||||
|
||||
For this, we need a way to re-use nonce / counter:
|
||||
|
||||
const counter = new Uint8Array(4);
|
||||
chacha(..., counter, ...); // counter is now 1
|
||||
chacha(..., counter, ...); // counter is now 2
|
||||
|
||||
This is complicated:
|
||||
|
||||
- Original papers don't allow mutating counters
|
||||
- Counter overflow is undefined: https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/
|
||||
- 3rd-party library stablelib implementation uses an approach where you can provide
|
||||
nonce and counter instead of just nonce - and it will re-use it
|
||||
- We could have did something similar, but ChaCha has different counter position
|
||||
(counter | nonce), which is not composable with XChaCha, because full counter
|
||||
is (nonce16 | counter | nonce16). Stablelib doesn't support in-place counter for XChaCha.
|
||||
- We could separate nonce & counter and provide separate API for counter re-use, but
|
||||
there are different counter sizes depending on an algorithm.
|
||||
- Salsa & ChaCha also differ in structures of key / sigma:
|
||||
|
||||
salsa: c0 | k(4) | c1 | nonce(2) | ctr(2) | c2 | k(4) | c4
|
||||
chacha: c(4) | k(8) | ctr(1) | nonce(3)
|
||||
chachaDJB: c(4) | k(8) | ctr(2) | nonce(2)
|
||||
- Creating function such as `setSalsaState(key, nonce, sigma, data)` won't work,
|
||||
because we can't re-use counter array
|
||||
- 32-bit nonce is `2 ** 32 * 64` = 256GB with 32-bit counter
|
||||
- JS does not allow UintArrays bigger than 4GB, so supporting 64-bit counters doesn't matter
|
||||
|
||||
Structure is as following:
|
||||
|
||||
key=16 -> sigma16, k=key|key
|
||||
key=32 -> sigma32, k=key
|
||||
|
||||
nonces:
|
||||
salsa20: 8 (8-byte counter)
|
||||
chacha20djb: 8 (8-byte counter)
|
||||
chacha20tls: 12 (4-byte counter)
|
||||
xsalsa: 24 (16 -> hsalsa, 8 -> old nonce)
|
||||
xchacha: 24 (16 -> hchacha, 8 -> old nonce)
|
||||
|
||||
https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2
|
||||
Use the subkey and remaining 8 byte nonce with ChaCha20 as normal
|
||||
(prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce).
|
||||
*/
|
||||
|
||||
const sigma16 = utf8ToBytes('expand 16-byte k');
|
||||
const sigma32 = utf8ToBytes('expand 32-byte k');
|
||||
const sigma16_32 = u32(sigma16);
|
||||
const sigma32_32 = u32(sigma32);
|
||||
|
||||
export type SalsaOpts = {
|
||||
core: (
|
||||
c: Uint32Array,
|
||||
key: Uint32Array,
|
||||
nonce: Uint32Array,
|
||||
out: Uint32Array,
|
||||
counter: number,
|
||||
rounds?: number
|
||||
) => void;
|
||||
rounds?: number;
|
||||
counterRight?: boolean; // counterRight ? nonce | counter : counter | nonce;
|
||||
counterLen?: number;
|
||||
blockLen?: number; // NOTE: not tested with different blockLens!
|
||||
allow128bitKeys?: boolean; // Original salsa/chacha allows these, but not tested!
|
||||
extendNonceFn?: (c: Uint32Array, key: Uint8Array, src: Uint8Array, dst: Uint8Array) => Uint8Array;
|
||||
};
|
||||
|
||||
// Is byte array aligned to 4 byte offset (u32)?
|
||||
const isAligned32 = (b: Uint8Array) => !(b.byteOffset % 4);
|
||||
|
||||
export const salsaBasic = (opts: SalsaOpts) => {
|
||||
const { core, rounds, counterRight, counterLen, allow128bitKeys, extendNonceFn, blockLen } =
|
||||
checkOpts(
|
||||
{ rounds: 20, counterRight: false, counterLen: 8, allow128bitKeys: true, blockLen: 64 },
|
||||
opts
|
||||
);
|
||||
assert.number(counterLen);
|
||||
assert.number(rounds);
|
||||
assert.number(blockLen);
|
||||
assert.bool(counterRight);
|
||||
assert.bool(allow128bitKeys);
|
||||
const blockLen32 = blockLen / 4;
|
||||
if (blockLen % 4 !== 0) throw new Error('Salsa/ChaCha: blockLen must be aligned to 4 bytes');
|
||||
return (
|
||||
key: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
data: Uint8Array,
|
||||
output?: Uint8Array,
|
||||
counter = 0
|
||||
): Uint8Array => {
|
||||
assert.bytes(key);
|
||||
assert.bytes(nonce);
|
||||
assert.bytes(data);
|
||||
if (!output) output = new Uint8Array(data.length);
|
||||
assert.bytes(output);
|
||||
assert.number(counter);
|
||||
// > new Uint32Array([2**32])
|
||||
// Uint32Array(1) [ 0 ]
|
||||
// > new Uint32Array([2**32-1])
|
||||
// Uint32Array(1) [ 4294967295 ]
|
||||
if (counter < 0 || counter >= 2 ** 32 - 1) throw new Error('Salsa/ChaCha: counter overflow');
|
||||
if (output.length < data.length) {
|
||||
throw new Error(
|
||||
`Salsa/ChaCha: output (${output.length}) is shorter than data (${data.length})`
|
||||
);
|
||||
}
|
||||
const toClean = [];
|
||||
let k, sigma;
|
||||
// Handle 128 byte keys
|
||||
if (key.length === 32) {
|
||||
k = key;
|
||||
sigma = sigma32_32;
|
||||
} else if (key.length === 16 && allow128bitKeys) {
|
||||
k = new Uint8Array(32);
|
||||
k.set(key);
|
||||
k.set(key, 16);
|
||||
sigma = sigma16_32;
|
||||
toClean.push(k);
|
||||
} else throw new Error(`Salsa/ChaCha: invalid 32-byte key, got length=${key.length}`);
|
||||
// Handle extended nonce (HChaCha/HSalsa)
|
||||
if (extendNonceFn) {
|
||||
if (nonce.length <= 16)
|
||||
throw new Error(`Salsa/ChaCha: extended nonce must be bigger than 16 bytes`);
|
||||
k = extendNonceFn(sigma, k, nonce.subarray(0, 16), new Uint8Array(32));
|
||||
toClean.push(k);
|
||||
nonce = nonce.subarray(16);
|
||||
}
|
||||
// Handle nonce counter
|
||||
const nonceLen = 16 - counterLen;
|
||||
if (nonce.length !== nonceLen)
|
||||
throw new Error(`Salsa/ChaCha: nonce must be ${nonceLen} or 16 bytes`);
|
||||
// Pad counter when nonce is 64 bit
|
||||
if (nonceLen !== 12) {
|
||||
const nc = new Uint8Array(12);
|
||||
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
|
||||
toClean.push((nonce = nc));
|
||||
}
|
||||
// Counter positions
|
||||
const block = new Uint8Array(blockLen);
|
||||
// Cast to Uint32Array for speed
|
||||
const b32 = u32(block);
|
||||
const k32 = u32(k);
|
||||
const n32 = u32(nonce);
|
||||
// Make sure that buffers aligned to 4 bytes
|
||||
const d32 = isAligned32(data) && u32(data);
|
||||
const o32 = isAligned32(output) && u32(output);
|
||||
toClean.push(b32);
|
||||
const len = data.length;
|
||||
for (let pos = 0, ctr = counter; pos < len; ctr++) {
|
||||
core(sigma, k32, n32, b32, ctr, rounds);
|
||||
if (ctr >= 2 ** 32 - 1) throw new Error('Salsa/ChaCha: counter overflow');
|
||||
const take = Math.min(blockLen, len - pos);
|
||||
// full block && aligned to 4 bytes
|
||||
if (take === blockLen && o32 && d32) {
|
||||
const pos32 = pos / 4;
|
||||
if (pos % 4 !== 0) throw new Error('Salsa/ChaCha: invalid block position');
|
||||
for (let j = 0; j < blockLen32; j++) o32[pos32 + j] = d32[pos32 + j] ^ b32[j];
|
||||
pos += blockLen;
|
||||
continue;
|
||||
}
|
||||
for (let j = 0; j < take; j++) output[pos + j] = data[pos + j] ^ block[j];
|
||||
pos += take;
|
||||
}
|
||||
for (let i = 0; i < toClean.length; i++) toClean[i].fill(0);
|
||||
return output;
|
||||
};
|
||||
};
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
import { Cipher, createView, ensureBytes, equalBytes, setBigUint64, u32 } from './utils.js';
|
||||
import { poly1305 } from './_poly1305.js';
|
||||
import { salsaBasic } from './_salsa.js';
|
||||
|
||||
// ChaCha20 stream cipher was released in 2008. ChaCha aims to increase
|
||||
// the diffusion per round, but had slightly less cryptanalysis.
|
||||
// https://cr.yp.to/chacha.html, http://cr.yp.to/chacha/chacha-20080128.pdf
|
||||
|
||||
// Left rotate for uint32
|
||||
const rotl = (a: number, b: number) => (a << b) | (a >>> (32 - b));
|
||||
|
||||
/**
|
||||
* ChaCha core function.
|
||||
*/
|
||||
// prettier-ignore
|
||||
function chachaCore(
|
||||
c: Uint32Array, k: Uint32Array, n: Uint32Array, out: Uint32Array, cnt: number, rounds = 20
|
||||
): void {
|
||||
let y00 = c[0], y01 = c[1], y02 = c[2], y03 = c[3]; // "expa" "nd 3" "2-by" "te k"
|
||||
let y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3]; // Key Key Key Key
|
||||
let y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7]; // Key Key Key Key
|
||||
let y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce
|
||||
// Save state to temporary variables
|
||||
let x00 = y00, x01 = y01, x02 = y02, x03 = y03,
|
||||
x04 = y04, x05 = y05, x06 = y06, x07 = y07,
|
||||
x08 = y08, x09 = y09, x10 = y10, x11 = y11,
|
||||
x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
||||
// Main loop
|
||||
for (let i = 0; i < rounds; i += 2) {
|
||||
x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 16);
|
||||
x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 12);
|
||||
x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 8);
|
||||
x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 7);
|
||||
|
||||
x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 16);
|
||||
x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 12);
|
||||
x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 8);
|
||||
x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 7);
|
||||
|
||||
x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 16);
|
||||
x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 12);
|
||||
x02 = (x02 + x06) | 0; x14 = rotl(x14 ^x02, 8);
|
||||
x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 7);
|
||||
|
||||
x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 16);
|
||||
x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 12);
|
||||
x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 8)
|
||||
x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 7);
|
||||
|
||||
x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 16);
|
||||
x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 12);
|
||||
x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 8);
|
||||
x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 7);
|
||||
|
||||
x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 16);
|
||||
x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 12);
|
||||
x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 8);
|
||||
x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 7);
|
||||
|
||||
x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 16);
|
||||
x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 12);
|
||||
x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 8);
|
||||
x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 7);
|
||||
|
||||
x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 16)
|
||||
x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 12);
|
||||
x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 8);
|
||||
x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 7);
|
||||
}
|
||||
// Write output
|
||||
let oi = 0;
|
||||
out[oi++] = (y00 + x00) | 0; out[oi++] = (y01 + x01) | 0;
|
||||
out[oi++] = (y02 + x02) | 0; out[oi++] = (y03 + x03) | 0;
|
||||
out[oi++] = (y04 + x04) | 0; out[oi++] = (y05 + x05) | 0;
|
||||
out[oi++] = (y06 + x06) | 0; out[oi++] = (y07 + x07) | 0;
|
||||
out[oi++] = (y08 + x08) | 0; out[oi++] = (y09 + x09) | 0;
|
||||
out[oi++] = (y10 + x10) | 0; out[oi++] = (y11 + x11) | 0;
|
||||
out[oi++] = (y12 + x12) | 0; out[oi++] = (y13 + x13) | 0;
|
||||
out[oi++] = (y14 + x14) | 0; out[oi++] = (y15 + x15) | 0;
|
||||
}
|
||||
/**
|
||||
* hchacha helper method, used primarily in xchacha, to hash
|
||||
* key and nonce into key' and nonce'.
|
||||
* Same as chachaCore, but there doesn't seem to be a way to move the block
|
||||
* out without 25% performance hit.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export function hchacha(
|
||||
c: Uint32Array, key: Uint8Array, src: Uint8Array, out: Uint8Array
|
||||
): Uint8Array {
|
||||
const k32 = u32(key);
|
||||
const i32 = u32(src);
|
||||
const o32 = u32(out);
|
||||
let x00 = c[0], x01 = c[1], x02 = c[2], x03 = c[3];
|
||||
let x04 = k32[0], x05 = k32[1], x06 = k32[2], x07 = k32[3];
|
||||
let x08 = k32[4], x09 = k32[5], x10 = k32[6], x11 = k32[7]
|
||||
let x12 = i32[0], x13 = i32[1], x14 = i32[2], x15 = i32[3];
|
||||
for (let i = 0; i < 20; i += 2) {
|
||||
x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 16);
|
||||
x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 12);
|
||||
x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 8);
|
||||
x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 7);
|
||||
|
||||
x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 16);
|
||||
x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 12);
|
||||
x01 = (x01 + x05) | 0; x13 = rotl(x13 ^ x01, 8);
|
||||
x09 = (x09 + x13) | 0; x05 = rotl(x05 ^ x09, 7);
|
||||
|
||||
x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 16);
|
||||
x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 12);
|
||||
x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 8);
|
||||
x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 7);
|
||||
|
||||
x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 16);
|
||||
x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 12);
|
||||
x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 8)
|
||||
x11 = (x11 + x15) | 0; x07 = rotl(x07 ^ x11, 7);
|
||||
|
||||
x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 16);
|
||||
x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 12);
|
||||
x00 = (x00 + x05) | 0; x15 = rotl(x15 ^ x00, 8);
|
||||
x10 = (x10 + x15) | 0; x05 = rotl(x05 ^ x10, 7);
|
||||
|
||||
x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 16);
|
||||
x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 12);
|
||||
x01 = (x01 + x06) | 0; x12 = rotl(x12 ^ x01, 8);
|
||||
x11 = (x11 + x12) | 0; x06 = rotl(x06 ^ x11, 7);
|
||||
|
||||
x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 16);
|
||||
x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 12);
|
||||
x02 = (x02 + x07) | 0; x13 = rotl(x13 ^ x02, 8);
|
||||
x08 = (x08 + x13) | 0; x07 = rotl(x07 ^ x08, 7);
|
||||
|
||||
x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 16)
|
||||
x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 12);
|
||||
x03 = (x03 + x04) | 0; x14 = rotl(x14 ^ x03, 8);
|
||||
x09 = (x09 + x14) | 0; x04 = rotl(x04 ^ x09, 7);
|
||||
}
|
||||
o32[0] = x00;
|
||||
o32[1] = x01;
|
||||
o32[2] = x02;
|
||||
o32[3] = x03;
|
||||
o32[4] = x12;
|
||||
o32[5] = x13;
|
||||
o32[6] = x14;
|
||||
o32[7] = x15;
|
||||
return out;
|
||||
}
|
||||
/**
|
||||
* Original, non-RFC chacha20 from DJB. 8-byte nonce, 8-byte counter.
|
||||
*/
|
||||
export const chacha20orig = /* @__PURE__ */ salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 8,
|
||||
});
|
||||
/**
|
||||
* ChaCha stream cipher. Conforms to RFC 8439 (IETF, TLS). 12-byte nonce, 4-byte counter.
|
||||
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||
*/
|
||||
export const chacha20 = /* @__PURE__ */ salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 4,
|
||||
allow128bitKeys: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* XChaCha eXtended-nonce ChaCha. 24-byte nonce.
|
||||
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||
*/
|
||||
export const xchacha20 = /* @__PURE__ */ salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 8,
|
||||
extendNonceFn: hchacha,
|
||||
allow128bitKeys: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* Reduced 8-round chacha, described in original paper.
|
||||
*/
|
||||
export const chacha8 = /* @__PURE__ */ salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 4,
|
||||
rounds: 8,
|
||||
});
|
||||
|
||||
/**
|
||||
* Reduced 12-round chacha, described in original paper.
|
||||
*/
|
||||
export const chacha12 = /* @__PURE__ */ salsaBasic({
|
||||
core: chachaCore,
|
||||
counterRight: false,
|
||||
counterLen: 4,
|
||||
rounds: 12,
|
||||
});
|
||||
|
||||
const ZERO = /* @__PURE__ */ new Uint8Array(16);
|
||||
// Pad to digest size with zeros
|
||||
const updatePadded = (h: ReturnType<typeof poly1305.create>, msg: Uint8Array) => {
|
||||
h.update(msg);
|
||||
const left = msg.length % 16;
|
||||
if (left) h.update(ZERO.subarray(left));
|
||||
};
|
||||
|
||||
const computeTag = (
|
||||
fn: typeof chacha20,
|
||||
key: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
data: Uint8Array,
|
||||
AAD?: Uint8Array
|
||||
) => {
|
||||
const authKey = fn(key, nonce, new Uint8Array(32));
|
||||
const h = poly1305.create(authKey);
|
||||
if (AAD) updatePadded(h, AAD);
|
||||
updatePadded(h, data);
|
||||
const num = new Uint8Array(16);
|
||||
const view = createView(num);
|
||||
setBigUint64(view, 0, BigInt(AAD ? AAD.length : 0), true);
|
||||
setBigUint64(view, 8, BigInt(data.length), true);
|
||||
h.update(num);
|
||||
const res = h.digest();
|
||||
authKey.fill(0);
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* AEAD algorithm from RFC 8439.
|
||||
* Salsa20 and chacha (RFC 8439) use poly1305 differently.
|
||||
* We could have composed them similar to:
|
||||
* https://github.com/paulmillr/scure-base/blob/b266c73dde977b1dd7ef40ef7a23cc15aab526b3/index.ts#L250
|
||||
* But it's hard because of authKey:
|
||||
* In salsa20, authKey changes position in salsa stream.
|
||||
* In chacha, authKey can't be computed inside computeTag, it modifies the counter.
|
||||
*/
|
||||
export const _poly1305_aead =
|
||||
(xorStream: typeof chacha20) =>
|
||||
(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => {
|
||||
const tagLength = 16;
|
||||
ensureBytes(key, 32);
|
||||
ensureBytes(nonce);
|
||||
return {
|
||||
tagLength,
|
||||
encrypt: (plaintext: Uint8Array, output?: Uint8Array) => {
|
||||
const plength = plaintext.length;
|
||||
const clength = plength + tagLength;
|
||||
if (output) {
|
||||
ensureBytes(output, clength);
|
||||
} else {
|
||||
output = new Uint8Array(clength);
|
||||
}
|
||||
xorStream(key, nonce, plaintext, output, 1);
|
||||
const tag = computeTag(xorStream, key, nonce, output.subarray(0, -tagLength), AAD);
|
||||
output.set(tag, plength); // append tag
|
||||
return output;
|
||||
},
|
||||
decrypt: (ciphertext: Uint8Array, output?: Uint8Array) => {
|
||||
const clength = ciphertext.length;
|
||||
const plength = clength - tagLength;
|
||||
if (clength < tagLength)
|
||||
throw new Error(`encrypted data must be at least ${tagLength} bytes`);
|
||||
if (output) {
|
||||
ensureBytes(output, plength);
|
||||
} else {
|
||||
output = new Uint8Array(plength);
|
||||
}
|
||||
const data = ciphertext.subarray(0, -tagLength);
|
||||
const passedTag = ciphertext.subarray(-tagLength);
|
||||
const tag = computeTag(xorStream, key, nonce, data, AAD);
|
||||
if (!equalBytes(passedTag, tag)) throw new Error('invalid tag');
|
||||
xorStream(key, nonce, data, output, 1);
|
||||
return output;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ChaCha20-Poly1305 from RFC 8439.
|
||||
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||
*/
|
||||
export const chacha20poly1305 = /* @__PURE__ */ _poly1305_aead(chacha20);
|
||||
/**
|
||||
* XChaCha20-Poly1305 extended-nonce chacha.
|
||||
* https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha
|
||||
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||
*/
|
||||
export const xchacha20poly1305 = /* @__PURE__ */ _poly1305_aead(xchacha20);
|
||||
+1
@@ -0,0 +1 @@
|
||||
throw new Error('noble-ciphers have no entry-point: consult README for usage');
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { ensureBytes, u32, equalBytes, Cipher } from './utils.js';
|
||||
import { salsaBasic } from './_salsa.js';
|
||||
import { poly1305 } from './_poly1305.js';
|
||||
|
||||
// Salsa20 stream cipher was released in 2005.
|
||||
// Salsa's goal was to implement AES replacement that does not rely on S-Boxes,
|
||||
// which are hard to implement in a constant-time manner.
|
||||
// https://cr.yp.to/snuffle.html, https://cr.yp.to/snuffle/salsafamily-20071225.pdf
|
||||
|
||||
// Left rotate for uint32
|
||||
const rotl = (a: number, b: number) => (a << b) | (a >>> (32 - b));
|
||||
|
||||
/**
|
||||
* Salsa20 core function.
|
||||
*/
|
||||
// prettier-ignore
|
||||
function salsaCore(
|
||||
c: Uint32Array, k: Uint32Array, i: Uint32Array, out: Uint32Array, cnt: number, rounds = 20
|
||||
): void {
|
||||
// Based on https://cr.yp.to/salsa20.html
|
||||
let y00 = c[0], y01 = k[0], y02 = k[1], y03 = k[2]; // "expa" Key Key Key
|
||||
let y04 = k[3], y05 = c[1], y06 = i[0], y07 = i[1]; // Key "nd 3" Nonce Nonce
|
||||
let y08 = cnt, y09 = 0 , y10 = c[2], y11 = k[4]; // Pos. Pos. "2-by" Key
|
||||
let y12 = k[5], y13 = k[6], y14 = k[7], y15 = c[3]; // Key Key Key "te k"
|
||||
// Save state to temporary variables
|
||||
let x00 = y00, x01 = y01, x02 = y02, x03 = y03,
|
||||
x04 = y04, x05 = y05, x06 = y06, x07 = y07,
|
||||
x08 = y08, x09 = y09, x10 = y10, x11 = y11,
|
||||
x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
||||
// Main loop
|
||||
for (let i = 0; i < rounds; i += 2) {
|
||||
x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9);
|
||||
x12 ^= rotl(x08 + x04 | 0, 13); x00 ^= rotl(x12 + x08 | 0, 18);
|
||||
x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9);
|
||||
x01 ^= rotl(x13 + x09 | 0, 13); x05 ^= rotl(x01 + x13 | 0, 18);
|
||||
x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9);
|
||||
x06 ^= rotl(x02 + x14 | 0, 13); x10 ^= rotl(x06 + x02 | 0, 18);
|
||||
x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9);
|
||||
x11 ^= rotl(x07 + x03 | 0, 13); x15 ^= rotl(x11 + x07 | 0, 18);
|
||||
x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9);
|
||||
x03 ^= rotl(x02 + x01 | 0, 13); x00 ^= rotl(x03 + x02 | 0, 18);
|
||||
x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9);
|
||||
x04 ^= rotl(x07 + x06 | 0, 13); x05 ^= rotl(x04 + x07 | 0, 18);
|
||||
x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9);
|
||||
x09 ^= rotl(x08 + x11 | 0, 13); x10 ^= rotl(x09 + x08 | 0, 18);
|
||||
x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9);
|
||||
x14 ^= rotl(x13 + x12 | 0, 13); x15 ^= rotl(x14 + x13 | 0, 18);
|
||||
}
|
||||
// Write output
|
||||
let oi = 0;
|
||||
out[oi++] = (y00 + x00) | 0; out[oi++] = (y01 + x01) | 0;
|
||||
out[oi++] = (y02 + x02) | 0; out[oi++] = (y03 + x03) | 0;
|
||||
out[oi++] = (y04 + x04) | 0; out[oi++] = (y05 + x05) | 0;
|
||||
out[oi++] = (y06 + x06) | 0; out[oi++] = (y07 + x07) | 0;
|
||||
out[oi++] = (y08 + x08) | 0; out[oi++] = (y09 + x09) | 0;
|
||||
out[oi++] = (y10 + x10) | 0; out[oi++] = (y11 + x11) | 0;
|
||||
out[oi++] = (y12 + x12) | 0; out[oi++] = (y13 + x13) | 0;
|
||||
out[oi++] = (y14 + x14) | 0; out[oi++] = (y15 + x15) | 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* hsalsa hashing function, used primarily in xsalsa, to hash
|
||||
* key and nonce into key' and nonce'.
|
||||
* Same as salsaCore, but there doesn't seem to be a way to move the block
|
||||
* out without 25% performance hit.
|
||||
*/
|
||||
// prettier-ignore
|
||||
export function hsalsa(c: Uint32Array, key: Uint8Array, nonce: Uint8Array, out: Uint8Array): Uint8Array {
|
||||
const k32 = u32(key);
|
||||
const i32 = u32(nonce);
|
||||
const o32 = u32(out);
|
||||
let x00 = c[0], x01 = k32[0], x02 = k32[1], x03 = k32[2], x04 = k32[3];
|
||||
let x05 = c[1], x06 = i32[0], x07 = i32[1], x08 = i32[2], x09 = i32[3];
|
||||
let x10 = c[2], x11 = k32[4], x12 = k32[5], x13 = k32[6], x14 = k32[7];
|
||||
let x15 = c[3];
|
||||
// Main loop
|
||||
for (let i = 0; i < 20; i += 2) {
|
||||
x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9);
|
||||
x12 ^= rotl(x08 + x04 | 0, 13); x00 ^= rotl(x12 + x08 | 0, 18);
|
||||
x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9);
|
||||
x01 ^= rotl(x13 + x09 | 0, 13); x05 ^= rotl(x01 + x13 | 0, 18);
|
||||
x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9);
|
||||
x06 ^= rotl(x02 + x14 | 0, 13); x10 ^= rotl(x06 + x02 | 0, 18);
|
||||
x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9);
|
||||
x11 ^= rotl(x07 + x03 | 0, 13); x15 ^= rotl(x11 + x07 | 0, 18);
|
||||
x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9);
|
||||
x03 ^= rotl(x02 + x01 | 0, 13); x00 ^= rotl(x03 + x02 | 0, 18);
|
||||
x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9);
|
||||
x04 ^= rotl(x07 + x06 | 0, 13); x05 ^= rotl(x04 + x07 | 0, 18);
|
||||
x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9);
|
||||
x09 ^= rotl(x08 + x11 | 0, 13); x10 ^= rotl(x09 + x08 | 0, 18);
|
||||
x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9);
|
||||
x14 ^= rotl(x13 + x12 | 0, 13); x15 ^= rotl(x14 + x13 | 0, 18);
|
||||
}
|
||||
o32[0] = x00;
|
||||
o32[1] = x05;
|
||||
o32[2] = x10;
|
||||
o32[3] = x15;
|
||||
o32[4] = x06;
|
||||
o32[5] = x07;
|
||||
o32[6] = x08;
|
||||
o32[7] = x09;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Salsa20 from original paper.
|
||||
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
||||
*/
|
||||
export const salsa20 = /* @__PURE__ */ salsaBasic({ core: salsaCore, counterRight: true });
|
||||
|
||||
/**
|
||||
* xsalsa20 eXtended-nonce salsa.
|
||||
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||
*/
|
||||
export const xsalsa20 = /* @__PURE__ */ salsaBasic({
|
||||
core: salsaCore,
|
||||
counterRight: true,
|
||||
extendNonceFn: hsalsa,
|
||||
allow128bitKeys: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* xsalsa20-poly1305 eXtended-nonce salsa.
|
||||
* With 24-byte nonce, it's safe to use fill it with random (CSPRNG).
|
||||
* Also known as secretbox from libsodium / nacl.
|
||||
*/
|
||||
export const xsalsa20poly1305 = (key: Uint8Array, nonce: Uint8Array): Cipher => {
|
||||
const tagLength = 16;
|
||||
ensureBytes(key, 32);
|
||||
ensureBytes(nonce, 24);
|
||||
return {
|
||||
tagLength,
|
||||
encrypt: (plaintext: Uint8Array, output?: Uint8Array) => {
|
||||
ensureBytes(plaintext);
|
||||
// This is small optimization (calculate auth key with same call as encryption itself) makes it hard
|
||||
// to separate tag calculation and encryption itself, since 32 byte is half-block of salsa (64 byte)
|
||||
const clength = plaintext.length + 32;
|
||||
if (output) {
|
||||
ensureBytes(output, clength);
|
||||
} else {
|
||||
output = new Uint8Array(clength);
|
||||
}
|
||||
output.set(plaintext, 32);
|
||||
xsalsa20(key, nonce, output, output);
|
||||
const authKey = output.subarray(0, 32);
|
||||
const tag = poly1305(output.subarray(32), authKey);
|
||||
// Clean auth key, even though JS provides no guarantees about memory cleaning
|
||||
output.set(tag, tagLength);
|
||||
output.subarray(0, tagLength).fill(0);
|
||||
return output.subarray(tagLength);
|
||||
},
|
||||
decrypt: (ciphertext: Uint8Array) => {
|
||||
ensureBytes(ciphertext);
|
||||
const clength = ciphertext.length;
|
||||
if (clength < tagLength) throw new Error('encrypted data should be at least 16 bytes');
|
||||
// Create new ciphertext array:
|
||||
// auth tag auth tag from ciphertext ciphertext
|
||||
// [bytes 0..16] [bytes 16..32] [bytes 32..]
|
||||
// 16 instead of 32, because we already have 16 byte tag
|
||||
const ciphertext_ = new Uint8Array(clength + tagLength); // alloc
|
||||
ciphertext_.set(ciphertext, tagLength);
|
||||
// Each xsalsa20 calls to hsalsa to calculate key, but seems not much perf difference
|
||||
// Separate call to calculate authkey, since first bytes contains tag
|
||||
const authKey = xsalsa20(key, nonce, new Uint8Array(32)); // alloc(32)
|
||||
const tag = poly1305(ciphertext_.subarray(32), authKey);
|
||||
if (!equalBytes(ciphertext_.subarray(16, 32), tag)) throw new Error('invalid tag');
|
||||
|
||||
const plaintext = xsalsa20(key, nonce, ciphertext_); // alloc
|
||||
// Clean auth key, even though JS provides no guarantees about memory cleaning
|
||||
plaintext.subarray(0, 32).fill(0);
|
||||
authKey.fill(0);
|
||||
return plaintext.subarray(32);
|
||||
},
|
||||
};
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { xchacha20poly1305 } from './chacha.js';
|
||||
import { xsalsa20poly1305 } from './salsa.js';
|
||||
import { concatBytes, ensureBytes, utf8ToBytes } from './utils.js';
|
||||
import { aes_256_gcm } from './webcrypto/aes.js';
|
||||
import { randomBytes } from './webcrypto/utils.js';
|
||||
|
||||
export { utf8ToBytes };
|
||||
|
||||
/**
|
||||
* Alias to xsalsa20poly1305, for compatibility with libsodium / nacl
|
||||
*/
|
||||
export function secretbox(key: Uint8Array, nonce: Uint8Array) {
|
||||
ensureBytes(key);
|
||||
ensureBytes(nonce);
|
||||
const xs = xsalsa20poly1305(key, nonce);
|
||||
return { seal: xs.encrypt, open: xs.decrypt };
|
||||
}
|
||||
|
||||
export function randomKey(): Uint8Array {
|
||||
return randomBytes(32);
|
||||
}
|
||||
/**
|
||||
* Encrypt plaintext under key with random nonce, using xchacha20poly1305.
|
||||
* User never touches nonce: it is prepended to ciphertext.
|
||||
*/
|
||||
export function encrypt(key: Uint8Array, plaintext: Uint8Array): Uint8Array {
|
||||
ensureBytes(key);
|
||||
const nonce = randomBytes(24);
|
||||
const ciphertext = xchacha20poly1305(key, nonce).encrypt(plaintext);
|
||||
return concatBytes(nonce, ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt plaintext under key with random nonce, using xchacha20poly1305.
|
||||
* User never touches nonce: it is prepended to ciphertext.
|
||||
*/
|
||||
export function decrypt(key: Uint8Array, ciphertext: Uint8Array): Uint8Array {
|
||||
const nonceLength = 24;
|
||||
ensureBytes(ciphertext);
|
||||
if (ciphertext.length <= nonceLength) throw new Error('invalid ciphertext length');
|
||||
const nonce = ciphertext.subarray(0, nonceLength);
|
||||
const ciphertextWithoutNonce = ciphertext.subarray(nonceLength);
|
||||
return xchacha20poly1305(key, nonce).decrypt(ciphertextWithoutNonce);
|
||||
}
|
||||
|
||||
export async function aes_encrypt(key: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array> {
|
||||
const nonceLength = 12;
|
||||
ensureBytes(key);
|
||||
const nonce = randomBytes(nonceLength);
|
||||
const ciphertext = await aes_256_gcm(key, nonce).encrypt(plaintext);
|
||||
return concatBytes(nonce, ciphertext);
|
||||
}
|
||||
|
||||
export async function aes_decrypt(key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
|
||||
const nonceLength = 12;
|
||||
ensureBytes(ciphertext);
|
||||
if (ciphertext.length <= nonceLength) throw new Error('invalid ciphertext length');
|
||||
const nonce = ciphertext.subarray(0, nonceLength);
|
||||
const ciphertextWithoutNonce = ciphertext.subarray(nonceLength);
|
||||
return aes_256_gcm(key, nonce).decrypt(ciphertextWithoutNonce);
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
||||
|
||||
// prettier-ignore
|
||||
export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array |
|
||||
Uint16Array | Int16Array | Uint32Array | Int32Array;
|
||||
|
||||
const u8a = (a: any): a is Uint8Array => a instanceof Uint8Array;
|
||||
// Cast array to different type
|
||||
export const u8 = (arr: TypedArray) => new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||
export const u16 = (arr: TypedArray) =>
|
||||
new Uint16Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 2));
|
||||
export const u32 = (arr: TypedArray) =>
|
||||
new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
||||
|
||||
// Cast array to view
|
||||
export const createView = (arr: TypedArray) =>
|
||||
new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
||||
|
||||
// big-endian hardware is rare. Just in case someone still decides to run ciphers:
|
||||
// early-throw an error because we don't support BE yet.
|
||||
export const isLE = new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44;
|
||||
if (!isLE) throw new Error('Non little-endian hardware is not supported');
|
||||
|
||||
const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) =>
|
||||
i.toString(16).padStart(2, '0')
|
||||
);
|
||||
/**
|
||||
* @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123'
|
||||
*/
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
if (!u8a(bytes)) throw new Error('Uint8Array expected');
|
||||
// pre-caching improves the speed 6x
|
||||
let hex = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
hex += hexes[bytes[i]];
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @example hexToBytes('cafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23])
|
||||
*/
|
||||
export function hexToBytes(hex: string): Uint8Array {
|
||||
if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex);
|
||||
const len = hex.length;
|
||||
if (len % 2) throw new Error('padded hex string expected, got unpadded hex of length ' + len);
|
||||
const array = new Uint8Array(len / 2);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const j = i * 2;
|
||||
const hexByte = hex.slice(j, j + 2);
|
||||
const byte = Number.parseInt(hexByte, 16);
|
||||
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence');
|
||||
array[i] = byte;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
// There is no setImmediate in browser and setTimeout is slow.
|
||||
// call of async fn will return Promise, which will be fullfiled only on
|
||||
// next scheduler queue processing step and this is exactly what we need.
|
||||
export const nextTick = async () => {};
|
||||
|
||||
// Returns control to thread each 'tick' ms to avoid blocking
|
||||
export async function asyncLoop(iters: number, tick: number, cb: (i: number) => void) {
|
||||
let ts = Date.now();
|
||||
for (let i = 0; i < iters; i++) {
|
||||
cb(i);
|
||||
// Date.now() is not monotonic, so in case if clock goes backwards we return return control too
|
||||
const diff = Date.now() - ts;
|
||||
if (diff >= 0 && diff < tick) continue;
|
||||
await nextTick();
|
||||
ts += diff;
|
||||
}
|
||||
}
|
||||
|
||||
// Global symbols in both browsers and Node.js since v11
|
||||
// See https://github.com/microsoft/TypeScript/issues/31535
|
||||
declare const TextEncoder: any;
|
||||
declare const TextDecoder: any;
|
||||
|
||||
/**
|
||||
* @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99])
|
||||
*/
|
||||
export function utf8ToBytes(str: string): Uint8Array {
|
||||
if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
|
||||
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
||||
}
|
||||
|
||||
export function bytesToUtf8(bytes: Uint8Array): string {
|
||||
return new TextDecoder().decode(bytes);
|
||||
}
|
||||
|
||||
export type Input = Uint8Array | string;
|
||||
/**
|
||||
* Normalizes (non-hex) string or Uint8Array to Uint8Array.
|
||||
* Warning: when Uint8Array is passed, it would NOT get copied.
|
||||
* Keep in mind for future mutable operations.
|
||||
*/
|
||||
export function toBytes(data: Input): Uint8Array {
|
||||
if (typeof data === 'string') data = utf8ToBytes(data);
|
||||
if (!u8a(data)) throw new Error(`expected Uint8Array, got ${typeof data}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies several Uint8Arrays into one.
|
||||
*/
|
||||
export function concatBytes(...arrays: Uint8Array[]): Uint8Array {
|
||||
const r = new Uint8Array(arrays.reduce((sum, a) => sum + a.length, 0));
|
||||
let pad = 0; // walk through each item, ensure they have proper type
|
||||
arrays.forEach((a) => {
|
||||
if (!u8a(a)) throw new Error('Uint8Array expected');
|
||||
r.set(a, pad);
|
||||
pad += a.length;
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
// Check if object doens't have custom constructor (like Uint8Array/Array)
|
||||
const isPlainObject = (obj: any) =>
|
||||
Object.prototype.toString.call(obj) === '[object Object]' && obj.constructor === Object;
|
||||
|
||||
type EmptyObj = {};
|
||||
export function checkOpts<T1 extends EmptyObj, T2 extends EmptyObj>(
|
||||
defaults: T1,
|
||||
opts?: T2
|
||||
): T1 & T2 {
|
||||
if (opts !== undefined && (typeof opts !== 'object' || !isPlainObject(opts)))
|
||||
throw new Error('options must be object or undefined');
|
||||
const merged = Object.assign(defaults, opts);
|
||||
return merged as T1 & T2;
|
||||
}
|
||||
|
||||
export function ensureBytes(b: any, len?: number) {
|
||||
if (!(b instanceof Uint8Array)) throw new Error('Uint8Array expected');
|
||||
if (typeof len === 'number')
|
||||
if (b.length !== len) throw new Error(`Uint8Array length ${len} expected`);
|
||||
}
|
||||
|
||||
// Constant-time equality
|
||||
export function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
|
||||
// Should not happen
|
||||
if (a.length !== b.length) throw new Error('equalBytes: Different size of Uint8Arrays');
|
||||
let isSame = true;
|
||||
for (let i = 0; i < a.length; i++) isSame &&= a[i] === b[i]; // Lets hope JIT won't optimize away.
|
||||
return isSame;
|
||||
}
|
||||
|
||||
// For runtime check if class implements interface
|
||||
export abstract class Hash<T extends Hash<T>> {
|
||||
abstract blockLen: number; // Bytes per block
|
||||
abstract outputLen: number; // Bytes in output
|
||||
abstract update(buf: Input): this;
|
||||
// Writes digest into buf
|
||||
abstract digestInto(buf: Uint8Array): void;
|
||||
abstract digest(): Uint8Array;
|
||||
/**
|
||||
* Resets internal state. Makes Hash instance unusable.
|
||||
* Reset is impossible for keyed hashes if key is consumed into state. If digest is not consumed
|
||||
* by user, they will need to manually call `destroy()` when zeroing is necessary.
|
||||
*/
|
||||
abstract destroy(): void;
|
||||
}
|
||||
|
||||
// This will allow to re-use with composable things like packed & base encoders
|
||||
// Also, we probably can make tags composable
|
||||
export type Cipher = {
|
||||
tagLength?: number;
|
||||
encrypt(plaintext: Uint8Array): Uint8Array;
|
||||
decrypt(ciphertext: Uint8Array): Uint8Array;
|
||||
};
|
||||
|
||||
export type AsyncCipher = {
|
||||
tagLength?: number;
|
||||
encrypt(plaintext: Uint8Array): Promise<Uint8Array>;
|
||||
decrypt(ciphertext: Uint8Array): Promise<Uint8Array>;
|
||||
};
|
||||
|
||||
// Polyfill for Safari 14
|
||||
export function setBigUint64(
|
||||
view: DataView,
|
||||
byteOffset: number,
|
||||
value: bigint,
|
||||
isLE: boolean
|
||||
): void {
|
||||
if (typeof view.setBigUint64 === 'function') return view.setBigUint64(byteOffset, value, isLE);
|
||||
const _32n = BigInt(32);
|
||||
const _u32_max = BigInt(0xffffffff);
|
||||
const wh = Number((value >> _32n) & _u32_max);
|
||||
const wl = Number(value & _u32_max);
|
||||
const h = isLE ? 4 : 0;
|
||||
const l = isLE ? 0 : 4;
|
||||
view.setUint32(byteOffset + h, wh, isLE);
|
||||
view.setUint32(byteOffset + l, wl, isLE);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { ensureBytes } from '../utils.js';
|
||||
import { getWebcryptoSubtle } from './utils.js';
|
||||
|
||||
function generate(algo: string, length: number) {
|
||||
const keyLength = length / 8;
|
||||
const keyParams = { name: algo, length };
|
||||
const cryptParams: Record<string, any> = { name: algo };
|
||||
// const params: Record<string, any> = ({ e: algo, i: { name: algo, length } });
|
||||
|
||||
return (key: Uint8Array, nonce: Uint8Array) => {
|
||||
ensureBytes(key, keyLength);
|
||||
if (algo === 'AES-CTR') {
|
||||
cryptParams.counter = nonce;
|
||||
cryptParams.length = 64;
|
||||
} else {
|
||||
cryptParams.iv = nonce;
|
||||
}
|
||||
|
||||
return {
|
||||
keyLength,
|
||||
|
||||
async encrypt(plaintext: Uint8Array): Promise<Uint8Array> {
|
||||
ensureBytes(plaintext);
|
||||
const cr = getWebcryptoSubtle();
|
||||
const iKey = await cr.importKey('raw', key, keyParams, true, ['encrypt']);
|
||||
const cipher = await cr.encrypt(cryptParams, iKey, plaintext);
|
||||
return new Uint8Array(cipher);
|
||||
},
|
||||
|
||||
async decrypt(ciphertext: Uint8Array): Promise<Uint8Array> {
|
||||
ensureBytes(ciphertext);
|
||||
const cr = getWebcryptoSubtle();
|
||||
const iKey = await cr.importKey('raw', key, keyParams, true, ['decrypt']);
|
||||
const plaintext = await cr.decrypt(cryptParams, iKey, ciphertext);
|
||||
return new Uint8Array(plaintext);
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const aes_128_ctr = generate('AES-CTR', 128);
|
||||
export const aes_256_ctr = generate('AES-CTR', 256);
|
||||
|
||||
export const aes_128_cbc = generate('AES-CBC', 128);
|
||||
export const aes_256_cbc = generate('AES-CBC', 256);
|
||||
|
||||
export const aes_128_gcm = generate('AES-GCM', 128);
|
||||
export const aes_256_gcm = generate('AES-GCM', 256);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
|
||||
// See utils.ts for details.
|
||||
declare const globalThis: Record<string, any> | undefined;
|
||||
export const crypto =
|
||||
typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
|
||||
// See utils.ts for details.
|
||||
// The file will throw on node.js 14 and earlier.
|
||||
// @ts-ignore
|
||||
import * as nc from 'node:crypto';
|
||||
export const crypto =
|
||||
nc && typeof nc === 'object' && 'webcrypto' in nc ? (nc.webcrypto as any) : undefined;
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
import type { AsyncCipher } from '../utils.js';
|
||||
import { getWebcryptoSubtle } from './utils.js';
|
||||
|
||||
// Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G.
|
||||
// https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf
|
||||
|
||||
// Utils
|
||||
function toBytesBE(num: bigint, length?: number): Uint8Array {
|
||||
let hex = num.toString(16);
|
||||
hex = hex.length & 1 ? `0${hex}` : hex;
|
||||
if (length) hex = hex.padStart(length * 2, '00');
|
||||
const len = hex.length / 2;
|
||||
const u8 = new Uint8Array(len);
|
||||
for (let j = 0, i = 0; i < hex.length && i < len * 2; i += 2, j++)
|
||||
u8[j] = parseInt(hex[i] + hex[i + 1], 16);
|
||||
return u8;
|
||||
}
|
||||
|
||||
function fromBytesBE(bytes: Uint8Array): bigint {
|
||||
let value = 0n;
|
||||
for (let i = bytes.length - 1, j = 0; i >= 0; i--, j++)
|
||||
value += (BigInt(bytes[i]) & 255n) << (8n * BigInt(j));
|
||||
return value;
|
||||
}
|
||||
|
||||
// Calculates a modulo b
|
||||
function mod(a: number, b: number): number;
|
||||
function mod(a: bigint, b: bigint): bigint;
|
||||
function mod(a: any, b: any): number | bigint {
|
||||
const result = a % b;
|
||||
return result >= 0 ? result : b + result;
|
||||
}
|
||||
// AES stuff
|
||||
const BLOCK_LEN = 16;
|
||||
const IV = new Uint8Array(BLOCK_LEN);
|
||||
export async function encryptBlock(msg: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||
if (key.length !== 16 && key.length !== 32) throw new Error('Invalid key length');
|
||||
const cr = getWebcryptoSubtle();
|
||||
const mode = { name: `AES-CBC`, length: key.length * 8 };
|
||||
const wKey = await cr.importKey('raw', key, mode, true, ['encrypt']);
|
||||
const cipher = await cr.encrypt({ name: `aes-cbc`, iv: IV, counter: IV, length: 64 }, wKey, msg);
|
||||
return new Uint8Array(cipher).subarray(0, 16);
|
||||
}
|
||||
|
||||
function NUMradix(radix: number, data: number[]): bigint {
|
||||
let res = 0n;
|
||||
for (let i of data) res = res * BigInt(radix) + BigInt(i);
|
||||
return res;
|
||||
}
|
||||
|
||||
async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: number[]) {
|
||||
if (radix > 2 ** 16 - 1) throw new Error(`Invalid radix: ${radix}`);
|
||||
// radix**minlen ≥ 100
|
||||
const minLen = Math.ceil(Math.log(100) / Math.log(radix));
|
||||
const maxLen = 2 ** 32 - 1;
|
||||
// 2 ≤ minlen ≤ maxlen < 2**32
|
||||
if (2 > minLen || minLen > maxLen || maxLen >= 2 ** 32)
|
||||
throw new Error('Invalid radix: 2 ≤ minlen ≤ maxlen < 2**32');
|
||||
if (x.length < minLen || x.length > maxLen) throw new Error('X is outside minLen..maxLen bounds');
|
||||
const u = Math.floor(x.length / 2);
|
||||
const v = x.length - u;
|
||||
const b = Math.ceil(Math.ceil(v * Math.log2(radix)) / 8);
|
||||
const d = 4 * Math.ceil(b / 4) + 4;
|
||||
const padding = mod(-tweak.length - b - 1, 16);
|
||||
// P = [1]1 || [2]1 || [1]1 || [radix]3 || [10]1 || [u mod 256]1 || [n]4 || [t]4.
|
||||
const P = new Uint8Array([1, 2, 1, 0, 0, 0, 10, u, 0, 0, 0, 0, 0, 0, 0, 0]);
|
||||
const view = new DataView(P.buffer);
|
||||
view.setUint16(4, radix, false);
|
||||
view.setUint32(8, x.length, false);
|
||||
view.setUint32(12, tweak.length, false);
|
||||
// Q = T || [0](−t−b−1) mod 16 || [i]1 || [NUMradix(B)]b.
|
||||
const PQ = new Uint8Array(P.length + tweak.length + padding + 1 + b);
|
||||
PQ.set(P);
|
||||
P.fill(0);
|
||||
PQ.set(tweak, P.length);
|
||||
const round = async (A: number[], B: number[], i: number, decrypt = false) => {
|
||||
// Q = ... || [i]1 || [NUMradix(B)]b.
|
||||
PQ[PQ.length - b - 1] = i;
|
||||
if (b) PQ.set(toBytesBE(NUMradix(radix, B), b), PQ.length - b);
|
||||
// PRF
|
||||
let r = new Uint8Array(16);
|
||||
for (let j = 0; j < PQ.length / BLOCK_LEN; j++) {
|
||||
for (let i = 0; i < BLOCK_LEN; i++) r[i] ^= PQ[j * BLOCK_LEN + i];
|
||||
r.set(await encryptBlock(r, key));
|
||||
}
|
||||
// Let S be the first d bytes of the following string of ⎡d/16⎤ blocks:
|
||||
// R || CIPHK(R ⊕[1]16) || CIPHK(R ⊕[2]16) ...CIPHK(R ⊕[⎡d / 16⎤ – 1]16).
|
||||
let s = Array.from(r);
|
||||
for (let j = 1; s.length < d; j++) {
|
||||
const block = toBytesBE(BigInt(j), 16);
|
||||
for (let k = 0; k < BLOCK_LEN; k++) block[k] ^= r[k];
|
||||
s.push(...Array.from(await encryptBlock(block, key)));
|
||||
}
|
||||
let y = fromBytesBE(Uint8Array.from(s.slice(0, d)));
|
||||
s.fill(0);
|
||||
if (decrypt) y = -y;
|
||||
const m = i % 2 === 0 ? u : v;
|
||||
let c = mod(NUMradix(radix, A) + y, BigInt(radix) ** BigInt(m));
|
||||
// STR(radix, m, c)
|
||||
const C = Array(m).fill(0);
|
||||
for (let i = 0; i < m; i++, c /= BigInt(radix)) C[m - 1 - i] = Number(c % BigInt(radix));
|
||||
A.fill(0);
|
||||
A = B;
|
||||
B = C;
|
||||
return [A, B];
|
||||
};
|
||||
const destroy = () => PQ.fill(0);
|
||||
return { u, round, destroy };
|
||||
}
|
||||
|
||||
const EMPTY_BUF = new Uint8Array([]);
|
||||
|
||||
export function FF1(radix: number, key: Uint8Array, tweak: Uint8Array = EMPTY_BUF) {
|
||||
const PQ = getRound.bind(null, radix, key, tweak);
|
||||
return {
|
||||
async encrypt(x: number[]) {
|
||||
const { u, round, destroy } = await PQ(x);
|
||||
let [A, B] = [x.slice(0, u), x.slice(u)];
|
||||
for (let i = 0; i < 10; i++) [A, B] = await round(A, B, i);
|
||||
destroy();
|
||||
const res = A.concat(B);
|
||||
A.fill(0);
|
||||
B.fill(0);
|
||||
return res;
|
||||
},
|
||||
async decrypt(x: number[]) {
|
||||
const { u, round, destroy } = await PQ(x);
|
||||
// The FF1.Decrypt algorithm is similar to the FF1.Encrypt algorithm;
|
||||
// the differences are in Step 6, where:
|
||||
// 1) the order of the indices is reversed,
|
||||
// 2) the roles of A and B are swapped
|
||||
// 3) modular addition is replaced by modular subtraction, in Step 6vi.
|
||||
let [B, A] = [x.slice(0, u), x.slice(u)];
|
||||
for (let i = 9; i >= 0; i--) [A, B] = await round(A, B, i, true);
|
||||
destroy();
|
||||
const res = B.concat(A);
|
||||
A.fill(0);
|
||||
B.fill(0);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
}
|
||||
// Binary string which encodes each byte in little-endian byte order
|
||||
const binLE = {
|
||||
encode(bytes: Uint8Array): number[] {
|
||||
const x = [];
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
for (let j = 0, tmp = bytes[i]; j < 8; j++, tmp >>= 1) x.push(tmp & 1);
|
||||
}
|
||||
return x;
|
||||
},
|
||||
decode(b: number[]): Uint8Array {
|
||||
if (b.length % 8) throw new Error('Invalid binary string');
|
||||
const res = new Uint8Array(b.length / 8);
|
||||
for (let i = 0, j = 0; i < res.length; i++) {
|
||||
res[i] = b[j++] | (b[j++] << 1) | (b[j++] << 2) | (b[j++] << 3);
|
||||
res[i] |= (b[j++] << 4) | (b[j++] << 5) | (b[j++] << 6) | (b[j++] << 7);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
export function BinaryFF1(key: Uint8Array, tweak: Uint8Array = EMPTY_BUF): AsyncCipher {
|
||||
const ff1 = FF1(2, key, tweak);
|
||||
return {
|
||||
encrypt: async (x: Uint8Array) => binLE.decode(await ff1.encrypt(binLE.encode(x))),
|
||||
decrypt: async (x: Uint8Array) => binLE.decode(await ff1.decrypt(binLE.encode(x))),
|
||||
};
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { AsyncCipher, createView, setBigUint64 } from '../utils.js';
|
||||
import { polyval } from '../_polyval.js';
|
||||
import { getWebcryptoSubtle } from './utils.js';
|
||||
/**
|
||||
* AES-GCM-SIV: classic AES-GCM with nonce-misuse resistance.
|
||||
* RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452
|
||||
*/
|
||||
|
||||
// AES stuff (same as ff1)
|
||||
const BLOCK_LEN = 16;
|
||||
const IV = new Uint8Array(BLOCK_LEN);
|
||||
async function encryptBlock(msg: Uint8Array, key: Uint8Array): Promise<Uint8Array> {
|
||||
if (key.length !== 16 && key.length !== 32) throw new Error('Invalid key length');
|
||||
const mode = { name: `AES-CBC`, length: key.length * 8 };
|
||||
const cr = getWebcryptoSubtle();
|
||||
const wKey = await cr.importKey('raw', key, mode, true, ['encrypt']);
|
||||
const cipher = await cr.encrypt({ name: `aes-cbc`, iv: IV, counter: IV, length: 64 }, wKey, msg);
|
||||
return new Uint8Array(cipher).subarray(0, 16);
|
||||
}
|
||||
|
||||
// Kinda constant-time equality
|
||||
function equalBytes(a: Uint8Array, b: Uint8Array): boolean {
|
||||
// Should not happen
|
||||
if (a.length !== b.length) throw new Error('equalBytes: Different size of Uint8Arrays');
|
||||
let flag = true;
|
||||
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) flag &&= false;
|
||||
return flag;
|
||||
}
|
||||
// Wrap position so it will be in padded to blockSize
|
||||
const wrapPos = (pos: number, blockSize: number) => Math.ceil(pos / blockSize) * blockSize;
|
||||
|
||||
const limit = (name: string, min: number, max: number) => (value: number) => {
|
||||
if (!Number.isSafeInteger(value) || min > value || value > max)
|
||||
throw new Error(`${name}: invalid value=${value}, must be [${min}..${max}]`);
|
||||
};
|
||||
|
||||
// From RFC 8452: Section 6
|
||||
const AAD_LIMIT = limit('AAD', 0, 2 ** 36);
|
||||
const PLAIN_LIMIT = limit('Plaintext', 0, 2 ** 36);
|
||||
const NONCE_LIMIT = limit('Nonce', 12, 12);
|
||||
const CIPHER_LIMIT = limit('Ciphertext', 16, 2 ** 36 + 16);
|
||||
|
||||
// nodejs api doesn't support 32bit counters, browser does
|
||||
async function ctr(key: Uint8Array, tag: Uint8Array, input: Uint8Array) {
|
||||
// The initial counter block is the tag with the most significant bit of the last byte set to one.
|
||||
let block = tag.slice();
|
||||
block[15] |= 0x80;
|
||||
let view = createView(block);
|
||||
let output = new Uint8Array(input.length);
|
||||
for (let pos = 0; pos < input.length; ) {
|
||||
const encryptedBlock = await encryptBlock(block, key);
|
||||
view.setUint32(0, view.getUint32(0, true) + 1, true);
|
||||
const take = Math.min(input.length, encryptedBlock.length);
|
||||
for (let j = 0; j < take; j++, pos++) output[pos] = encryptedBlock[j] ^ input[pos];
|
||||
}
|
||||
return new Uint8Array(output);
|
||||
}
|
||||
|
||||
export async function deriveKeys(key: Uint8Array, nonce: Uint8Array) {
|
||||
NONCE_LIMIT(nonce.length);
|
||||
const len = key.length;
|
||||
if (len !== 16 && len !== 32)
|
||||
throw new Error(`key length must be 16 or 32 bytes, got: ${len} bytes`);
|
||||
const encKey = new Uint8Array(len);
|
||||
const authKey = new Uint8Array(16);
|
||||
let counter = 0;
|
||||
const deriveBlock = new Uint8Array(nonce.length + 4);
|
||||
deriveBlock.set(nonce, 4);
|
||||
const view = createView(deriveBlock);
|
||||
for (const derivedKey of [authKey, encKey]) {
|
||||
for (let i = 0; i < derivedKey.length; i += 8) {
|
||||
view.setUint32(0, counter++, true);
|
||||
const block = await encryptBlock(deriveBlock, key);
|
||||
derivedKey.set(block.subarray(0, 8), i);
|
||||
}
|
||||
}
|
||||
return { authKey, encKey };
|
||||
}
|
||||
|
||||
export async function aes_256_gcm_siv(
|
||||
key: Uint8Array,
|
||||
nonce: Uint8Array,
|
||||
AAD: Uint8Array
|
||||
): Promise<AsyncCipher> {
|
||||
const { encKey, authKey } = await deriveKeys(key, nonce);
|
||||
const computeTag = async (data: Uint8Array, AAD: Uint8Array) => {
|
||||
const dataPos = wrapPos(AAD.length, 16);
|
||||
const lenPos = wrapPos(dataPos + data.length, 16);
|
||||
const block = new Uint8Array(lenPos + 16);
|
||||
const view = createView(block);
|
||||
block.set(AAD);
|
||||
block.set(data, dataPos);
|
||||
setBigUint64(view, lenPos, BigInt(AAD.length * 8), true);
|
||||
setBigUint64(view, lenPos + 8, BigInt(data.length * 8), true);
|
||||
// Compute the expected tag by XORing S_s and the nonce, clearing the
|
||||
// most significant bit of the last byte and encrypting with the
|
||||
// message-encryption key.
|
||||
const tag = polyval(authKey, block);
|
||||
for (let i = 0; i < 12; i++) tag[i] ^= nonce[i];
|
||||
// Clear the highest bit
|
||||
tag[15] &= 0x7f;
|
||||
return await encryptBlock(tag, encKey);
|
||||
};
|
||||
return {
|
||||
// computeTag,
|
||||
encrypt: async (plaintext: Uint8Array) => {
|
||||
AAD_LIMIT(AAD.length);
|
||||
PLAIN_LIMIT(plaintext.length);
|
||||
const tag = await computeTag(plaintext, AAD);
|
||||
const out = new Uint8Array(plaintext.length + 16);
|
||||
out.set(tag, plaintext.length);
|
||||
out.set(await ctr(encKey, tag, plaintext));
|
||||
return out;
|
||||
},
|
||||
decrypt: async (ciphertext: Uint8Array) => {
|
||||
CIPHER_LIMIT(ciphertext.length);
|
||||
AAD_LIMIT(AAD.length);
|
||||
const tag = ciphertext.subarray(-16);
|
||||
const plaintext = await ctr(encKey, tag, ciphertext.subarray(0, -16));
|
||||
const expectedTag = await computeTag(plaintext, AAD);
|
||||
if (!equalBytes(tag, expectedTag)) throw new Error('invalid poly1305 tag');
|
||||
return plaintext;
|
||||
},
|
||||
};
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+.
|
||||
// node.js versions earlier than v19 don't declare it in global scope.
|
||||
// For node.js, package.js on#exports field mapping rewrites import
|
||||
// from `crypto` to `cryptoNode`, which imports native module.
|
||||
// Makes the utils un-importable in browsers without a bundler.
|
||||
// Once node.js 18 is deprecated, we can just drop the import.
|
||||
import { crypto } from '@noble/ciphers/webcrypto/crypto';
|
||||
|
||||
/**
|
||||
* Secure PRNG. Uses `crypto.getRandomValues`, which defers to OS.
|
||||
*/
|
||||
export function randomBytes(bytesLength = 32): Uint8Array {
|
||||
if (crypto && typeof crypto.getRandomValues === 'function') {
|
||||
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
||||
}
|
||||
throw new Error('crypto.getRandomValues must be defined');
|
||||
}
|
||||
|
||||
export function getWebcryptoSubtle() {
|
||||
if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) {
|
||||
return crypto.subtle;
|
||||
}
|
||||
throw new Error('crypto.subtle must be defined');
|
||||
}
|
||||
Reference in New Issue
Block a user