Pithos: share links work inside Theseus (ChaCha20-Poly1305 in plain JS)

Creating a public link in the Theseus add-on failed with 'Unknown cipher':
Electron's BoringSSL has neither chacha20 nor chacha20-poly1305 in
createCipheriv, and the tests only ran under Node's OpenSSL. Opening Sia's
32-byte data key now uses RFC 8439 ChaCha20 and Poly1305 written out in
JS; the Go x/crypto vectors pass under Node and inside Electron 44.
This commit is contained in:
Local Dev 2026-10-07 19:34:58 +02:00
parent f9eccdc4b5
commit e4e4b3b587

View file

@ -58,13 +58,63 @@ export function hchacha20(key, nonce16) {
[0, 1, 2, 3, 12, 13, 14, 15].forEach((w, i) => out.writeUInt32LE(s[w], i * 4));
return out;
}
// ChaCha20 and Poly1305 in plain JS (RFC 8439): Electron's BoringSSL has no
// chacha20 or chacha20-poly1305 in createCipheriv ("Unknown cipher" in the
// Theseus add-on), and all this ever opens is a 32-byte key.
function chachaBlock(key, counter, nonce12) {
const s = new Uint32Array(16);
s.set([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]);
for (let i = 0; i < 8; i++) s[4 + i] = key.readUInt32LE(i * 4);
s[12] = counter >>> 0;
for (let i = 0; i < 3; i++) s[13 + i] = nonce12.readUInt32LE(i * 4);
const w = Uint32Array.from(s);
for (let r = 0; r < 10; r++) {
quarter(w, 0, 4, 8, 12); quarter(w, 1, 5, 9, 13); quarter(w, 2, 6, 10, 14); quarter(w, 3, 7, 11, 15);
quarter(w, 0, 5, 10, 15); quarter(w, 1, 6, 11, 12); quarter(w, 2, 7, 8, 13); quarter(w, 3, 4, 9, 14);
}
const out = Buffer.alloc(64);
for (let i = 0; i < 16; i++) out.writeUInt32LE((w[i] + s[i]) >>> 0, i * 4);
return out;
}
function chachaXor(key, counter, nonce12, data) {
const out = Buffer.alloc(data.length);
for (let off = 0, c = counter; off < data.length; off += 64, c++) {
const ks = chachaBlock(key, c, nonce12);
for (let i = 0; i < 64 && off + i < data.length; i++) out[off + i] = data[off + i] ^ ks[i];
}
return out;
}
function poly1305(key32, msg) {
const le = (b) => b.reduceRight((acc, x) => (acc << 8n) | BigInt(x), 0n);
const r = le(key32.subarray(0, 16)) & 0x0ffffffc0ffffffc0ffffffc0fffffffn;
const s = le(key32.subarray(16, 32));
const p = (1n << 130n) - 5n;
let acc = 0n;
for (let off = 0; off < msg.length; off += 16) {
const block = msg.subarray(off, off + 16);
acc = ((acc + le(block) + (1n << BigInt(8 * block.length))) * r) % p;
}
acc = (acc + s) & ((1n << 128n) - 1n);
const tag = Buffer.alloc(16);
for (let i = 0; i < 16; i++) { tag[i] = Number(acc & 0xffn); acc >>= 8n; }
return tag;
}
const pad16 = (n) => Buffer.alloc((16 - (n % 16)) % 16);
const le64 = (n) => { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(n)); return b; };
export function xchachaOpen(key, nonce24, sealed) {
const sub = hchacha20(Buffer.from(key), Buffer.from(nonce24).subarray(0, 16));
const nonce12 = Buffer.concat([Buffer.alloc(4), Buffer.from(nonce24).subarray(16, 24)]);
const ct = Buffer.from(sealed);
const d = crypto.createDecipheriv('chacha20-poly1305', sub, nonce12, { authTagLength: 16 });
d.setAuthTag(ct.subarray(ct.length - 16));
return Buffer.concat([d.update(ct.subarray(0, ct.length - 16)), d.final()]);
if (ct.length < 16) throw new Error('sealed data is too short');
const body = ct.subarray(0, ct.length - 16);
const polyKey = chachaBlock(sub, 0, nonce12).subarray(0, 32);
const want = poly1305(polyKey, Buffer.concat([body, pad16(body.length), le64(0), le64(body.length)]));
if (!crypto.timingSafeEqual(want, ct.subarray(ct.length - 16))) throw new Error('unable to authenticate data');
return chachaXor(sub, 1, nonce12, body);
}
// ---- s3d's database: <drive>/<name> -> Sia object id -------------------------------