@hpke/chacha20poly1305
Version:
A Hybrid Public Key Encryption (HPKE) module extension for ChaCha20/Poly1305
203 lines (194 loc) • 8.81 kB
JavaScript
/**
* This file is based on noble-ciphers (https://github.com/paulmillr/noble-ciphers).
*
* noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com)
*
* The original file is located at:
* https://github.com/paulmillr/noble-ciphers/blob/749cdf9cd07ebdd19e9b957d0f172f1045179695/src/_arx.ts
*/
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./utils.js"], factory);
}
})(function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rotl = rotl;
exports.createCipher = createCipher;
/**
* Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers.
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:
- 32-bit counters are enough, no need for 64-bit: max ArrayBuffer size in JS is 4GB
- Original papers don't allow mutating counters
- Counter overflow is undefined [^1]
- Idea A: allow providing (nonce | counter) instead of just nonce, re-use it
- Caveat: Cannot be re-used through all cases:
- * chacha has (counter | nonce)
- * xchacha has (nonce16 | counter | nonce16)
- Idea B: separate nonce / counter and provide separate API for counter re-use
- Caveat: there are different counter sizes depending on an algorithm.
- salsa & chacha also differ in structures of key & sigma:
salsa20: s[0] | k(4) | s[1] | nonce(2) | cnt(2) | s[2] | k(4) | s[3]
chacha: s(4) | k(8) | cnt(1) | nonce(3)
chacha20orig: s(4) | k(8) | cnt(2) | nonce(2)
- Idea C: helper method such as `setSalsaState(key, nonce, sigma, data)`
- Caveat: we can't re-use counter array
xchacha [^2] uses the subkey and remaining 8 byte nonce with ChaCha20 as normal
(prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce).
[^1]: https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/
[^2]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2
* @module
*/
const utils_js_1 = require("./utils.js");
// Can't use similar utils.utf8ToBytes, because it uses `TextEncoder` - not available in all envs
const _utf8ToBytes = (str) => Uint8Array.from(str.split("").map((c) => c.charCodeAt(0)));
const sigma16 = _utf8ToBytes("expand 16-byte k");
const sigma32 = _utf8ToBytes("expand 32-byte k");
const sigma16_32 = (0, utils_js_1.u32)(sigma16);
const sigma32_32 = (0, utils_js_1.u32)(sigma32);
/** Rotate left. */
function rotl(a, b) {
return (a << b) | (a >>> (32 - b));
}
// Is byte array aligned to 4 byte offset (u32)?
function isAligned32(b) {
return b.byteOffset % 4 === 0;
}
// Salsa and Chacha block length is always 512-bit
const BLOCK_LEN = 64;
const BLOCK_LEN32 = 16;
// new Uint32Array([2**32]) // => Uint32Array(1) [ 0 ]
// new Uint32Array([2**32-1]) // => Uint32Array(1) [ 4294967295 ]
const MAX_COUNTER = 2 ** 32 - 1;
const U32_EMPTY = Uint32Array.of();
function runCipher(core, sigma, key, nonce, data, output, counter, rounds) {
const len = data.length;
const block = new Uint8Array(BLOCK_LEN);
const b32 = (0, utils_js_1.u32)(block);
// Make sure that buffers aligned to 4 bytes
const isAligned = isAligned32(data) && isAligned32(output);
const d32 = isAligned ? (0, utils_js_1.u32)(data) : U32_EMPTY;
const o32 = isAligned ? (0, utils_js_1.u32)(output) : U32_EMPTY;
for (let pos = 0; pos < len; counter++) {
core(sigma, key, nonce, b32, counter, rounds);
if (counter >= MAX_COUNTER)
throw new Error("arx: counter overflow");
const take = Math.min(BLOCK_LEN, len - pos);
// aligned to 4 bytes
if (isAligned && take === BLOCK_LEN) {
const pos32 = pos / 4;
if (pos % 4 !== 0)
throw new Error("arx: invalid block position");
for (let j = 0, posj; j < BLOCK_LEN32; j++) {
posj = pos32 + j;
o32[posj] = d32[posj] ^ b32[j];
}
pos += BLOCK_LEN;
continue;
}
for (let j = 0, posj; j < take; j++) {
posj = pos + j;
output[posj] = data[posj] ^ block[j];
}
pos += take;
}
}
/** Creates ARX-like (ChaCha, Salsa) cipher stream from core function. */
function createCipher(core, opts) {
const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = (0, utils_js_1.checkOpts)({
allowShortKeys: false,
counterLength: 8,
counterRight: false,
rounds: 20,
}, opts);
if (typeof core !== "function")
throw new Error("core must be a function");
(0, utils_js_1.anumber)(counterLength);
(0, utils_js_1.anumber)(rounds);
(0, utils_js_1.abool)(counterRight);
(0, utils_js_1.abool)(allowShortKeys);
return (key, nonce, data, output, counter = 0) => {
(0, utils_js_1.abytes)(key, undefined, "key");
(0, utils_js_1.abytes)(nonce, undefined, "nonce");
(0, utils_js_1.abytes)(data, undefined, "data");
const len = data.length;
if (output === undefined)
output = new Uint8Array(len);
(0, utils_js_1.abytes)(output, undefined, "output");
(0, utils_js_1.anumber)(counter);
if (counter < 0 || counter >= MAX_COUNTER) {
throw new Error("arx: counter overflow");
}
if (output.length < len) {
throw new Error(`arx: output (${output.length}) is shorter than data (${len})`);
}
const toClean = [];
// Key & sigma
// key=16 -> sigma16, k=key|key
// key=32 -> sigma32, k=key
const l = key.length;
let k;
let sigma;
if (l === 32) {
toClean.push(k = (0, utils_js_1.copyBytes)(key));
sigma = sigma32_32;
}
else if (l === 16 && allowShortKeys) {
k = new Uint8Array(32);
k.set(key);
k.set(key, 16);
sigma = sigma16_32;
toClean.push(k);
}
else {
(0, utils_js_1.abytes)(key, 32, "arx key");
throw new Error("invalid key size");
// throw new Error(`"arx key" expected Uint8Array of length 32, got length=${l}`);
}
// Nonce
// salsa20: 8 (8-byte counter)
// chacha20orig: 8 (8-byte counter)
// chacha20: 12 (4-byte counter)
// xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce)
// xchacha20: 24 (16 -> hchacha, 8 -> old nonce)
// Align nonce to 4 bytes
if (!isAligned32(nonce))
toClean.push(nonce = (0, utils_js_1.copyBytes)(nonce));
const k32 = (0, utils_js_1.u32)(k);
// hsalsa & hchacha: handle extended nonce
if (extendNonceFn) {
if (nonce.length !== 24) {
throw new Error(`arx: extended nonce must be 24 bytes`);
}
extendNonceFn(sigma, k32, (0, utils_js_1.u32)(nonce.subarray(0, 16)), k32);
nonce = nonce.subarray(16);
}
// Handle nonce counter
const nonceNcLen = 16 - counterLength;
if (nonceNcLen !== nonce.length) {
throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`);
}
// Pad counter when nonce is 64 bit
if (nonceNcLen !== 12) {
const nc = new Uint8Array(12);
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
nonce = nc;
toClean.push(nonce);
}
const n32 = (0, utils_js_1.u32)(nonce);
runCipher(core, sigma, k32, n32, data, output, counter, rounds);
(0, utils_js_1.clean)(...toClean);
return output;
};
}
});