pqc-migration-toolkit/lib/keccak.js
Aere Network de9a79298c Aere PQC migration toolkit: measure an address quantum exposure from its bytecode
Zero dependencies, no account, no API. Every command takes --rpc, so it runs
against a node you operate and never needs ours. That is the point: an exposure
measurement you cannot re-run is an opinion with a logo on it.

Ships with two self-checks meant to be run by you, not just by us. The second,
proba-vocabular.mjs, exists because of a real defect found on 2026-08-16: the
scanner emits RED / YELLOW / GREEN, two downstream files each wrote their own
copy of that list, and both wrote AMBER, so a hybrid contract, exactly a client
who has already started migrating, was reported as unmeasurable. Both sides were
self-consistent, so nothing we had could see it. The vocabulary now has one
exported source, and the test walks the whole path with a negative control that
makes it able to fail.

The same scanner returns RED about our own contracts. See VERIFY-US.md.
2026-08-17 10:31:31 +03:00

143 lines
4.8 KiB
JavaScript

// keccak.js, a compact, zero-dependency keccak256 (Ethereum's Keccak, 0x01
// domain padding, NOT FIPS-202 SHA3). Pure Node built-ins, BigInt lanes.
//
// This exists so the migration SDK can derive CREATE2 account addresses and
// EIP-55 checksums with no runtime dependencies, matching the create-aere-pqc
// tool's "zero runtime dependencies" style. Correctness is proven two ways in
// selftest.js: the empty-string vector, and a real on-chain CREATE2 address.
const MASK64 = (1n << 64n) - 1n;
const ROUND_CONSTANTS = [
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
];
// Rho rotation offsets, flat-indexed by (x + 5*y).
const ROT = [
0, 1, 62, 28, 27,
36, 44, 6, 55, 20,
3, 10, 43, 25, 39,
41, 45, 15, 21, 8,
18, 2, 61, 56, 14,
];
function rotl64(x, n) {
const b = BigInt(n) % 64n;
if (b === 0n) return x & MASK64;
return ((x << b) | (x >> (64n - b))) & MASK64;
}
function keccakF(state) {
for (let round = 0; round < 24; round++) {
// theta
const C = new Array(5);
for (let x = 0; x < 5; x++) {
C[x] = state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20];
}
const D = new Array(5);
for (let x = 0; x < 5; x++) {
D[x] = C[(x + 4) % 5] ^ rotl64(C[(x + 1) % 5], 1n);
}
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) state[x + 5 * y] ^= D[x];
}
// rho + pi
const B = new Array(25).fill(0n);
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
B[y + 5 * ((2 * x + 3 * y) % 5)] = rotl64(state[x + 5 * y], ROT[x + 5 * y]);
}
}
// chi
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
state[x + 5 * y] = B[x + 5 * y] ^ ((~B[((x + 1) % 5) + 5 * y] & MASK64) & B[((x + 2) % 5) + 5 * y]);
}
}
// iota
state[0] ^= ROUND_CONSTANTS[round];
}
}
/**
* keccak256 over a Uint8Array, returning a 32-byte Uint8Array.
* @param {Uint8Array} bytes
* @returns {Uint8Array}
*/
export function keccak256(bytes) {
const RATE = 136; // 1088-bit rate for keccak256
const state = new Array(25).fill(0n);
// pad10*1 with Ethereum/Keccak domain byte 0x01
const padLen = RATE - (bytes.length % RATE);
const padded = new Uint8Array(bytes.length + padLen);
padded.set(bytes, 0);
padded[bytes.length] |= 0x01;
padded[padded.length - 1] |= 0x80;
for (let off = 0; off < padded.length; off += RATE) {
for (let i = 0; i < RATE; i++) {
const lane = i >> 3;
const shift = BigInt((i & 7) * 8);
state[lane] ^= BigInt(padded[off + i]) << shift;
}
keccakF(state);
}
const out = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
const lane = i >> 3;
const shift = BigInt((i & 7) * 8);
out[i] = Number((state[lane] >> shift) & 0xffn);
}
return out;
}
// ── small hex helpers (no dependency on ethers) ──────────────────────────────
/** Convert a hex string (with or without 0x) to a Uint8Array. */
export function hexToBytes(hex) {
let h = hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
if (h.length % 2 !== 0) h = '0' + h;
const out = new Uint8Array(h.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(h.substr(i * 2, 2), 16);
return out;
}
/** Convert a Uint8Array to a 0x-prefixed hex string. */
export function bytesToHex(bytes) {
let s = '0x';
for (const b of bytes) s += b.toString(16).padStart(2, '0');
return s;
}
/** keccak256 of a hex string input, returning a 0x-prefixed 32-byte hex string. */
export function keccak256Hex(hexInput) {
return bytesToHex(keccak256(hexToBytes(hexInput)));
}
/**
* EIP-55 mixed-case checksum of a 20-byte address hex string.
* @param {string} address 0x-prefixed 20-byte address (any case)
* @returns {string} checksummed address
*/
export function toChecksumAddress(address) {
const addr = (address.startsWith('0x') ? address.slice(2) : address).toLowerCase();
const hashBytes = keccak256(new TextEncoder().encode(addr));
// hex of the hash, one nibble per address char
let hashHex = '';
for (const b of hashBytes) hashHex += b.toString(16).padStart(2, '0');
let out = '0x';
for (let i = 0; i < addr.length; i++) {
const c = addr[i];
if (c >= '0' && c <= '9') out += c;
else out += parseInt(hashHex[i], 16) >= 8 ? c.toUpperCase() : c;
}
return out;
}