UNPKG

@arcium-hq/client

Version:

Client SDK for interacting with encrypted Solana programs

1,252 lines (1,238 loc) 165 kB
import { randomBytes, createHash, hkdfSync, createCipheriv, createDecipheriv } from 'crypto'; import { ed25519 } from '@noble/curves/ed25519'; export { x25519 } from '@noble/curves/ed25519'; import { shake256, sha3_512 } from '@noble/hashes/sha3'; import { invert } from '@noble/curves/abstract/modular'; import * as anchor from '@coral-xyz/anchor'; import { Program, EventManager } from '@coral-xyz/anchor'; import { randomBytes as randomBytes$1 } from '@noble/hashes/utils'; import { twistedEdwards } from '@noble/curves/abstract/edwards'; import { PublicKey } from '@solana/web3.js'; /** * Scalar field prime modulus for Curve25519: 2^252 + 27742317777372353535851937790883648493 */ const CURVE25519_SCALAR_FIELD_MODULUS = ed25519.CURVE.n; /** * Generates a random value within the field bound by q. * @param q - The upper bound (exclusive) for the random value. * @returns A random bigint value between 0 and q-1. */ function generateRandomFieldElem(q) { const byteLength = (q.toString(2).length + 7) >> 3; let r; do { const randomBuffer = randomBytes(byteLength); r = BigInt(`0x${randomBuffer.toString('hex')}`); } while (r >= q); return r; } /** * Computes the positive modulo of a over m. * @param a - The dividend. * @param m - The modulus. * @returns The positive remainder of a mod m. */ function positiveModulo(a, m) { return ((a % m) + m) % m; } /** * Serializes a bigint to a little-endian Uint8Array of the specified length. * @param val - The bigint value to serialize. * @param lengthInBytes - The desired length of the output array. * @returns The serialized value as a Uint8Array. * @throws Error if the value is too large for the specified length. */ function serializeLE(val, lengthInBytes) { const result = new Uint8Array(lengthInBytes); let tempVal = val; for (let i = 0; i < lengthInBytes; i++) { result[i] = Number(tempVal & BigInt(255)); tempVal >>= BigInt(8); } if (tempVal > BigInt(0)) { throw new Error(`Value ${val} is too large for the byte length ${lengthInBytes}`); } return result; } /** * Deserializes a little-endian Uint8Array to a bigint. * @param bytes - The Uint8Array to deserialize. * @returns The deserialized bigint value. */ function deserializeLE(bytes) { let result = BigInt(0); for (let i = 0; i < bytes.length; i++) { result |= BigInt(bytes[i]) << (BigInt(i) * BigInt(8)); } return result; } // GENERAL /** * Computes the SHA-256 hash of an array of Uint8Arrays. * @param byteArrays - The arrays to hash. * @returns The SHA-256 hash as a Buffer. */ function sha256(byteArrays) { const hash = createHash('sha256'); byteArrays.forEach((byteArray) => { hash.update(byteArray); }); return hash.digest(); } /** * Converts a bigint to an array of bits (least significant to most significant, in 2's complement representation). * @param x - The bigint to convert. * @param binSize - The number of bits to use in the representation. * @returns An array of booleans representing the bits of x. */ function toBinLE(x, binSize) { const res = []; for (let i = 0; i < binSize; ++i) { res.push(ctSignBit(x, BigInt(i))); } return res; } /** * Converts an array of bits (least significant to most significant, in 2's complement representation) to a bigint. * @param xBin - The array of bits to convert. * @returns The bigint represented by the bit array. */ function fromBinLE(xBin) { let res = 0n; for (let i = 0; i < xBin.length - 1; ++i) { res |= BigInt(xBin[i]) << BigInt(i); } return res - (BigInt(xBin[xBin.length - 1]) << BigInt(xBin.length - 1)); } /** * Binary adder between x and y (assumes xBin and yBin are of the same length and large enough to represent the sum). * @param xBin - The first operand as a bit array. * @param yBin - The second operand as a bit array. * @param carryIn - The initial carry-in value. * @param binSize - The number of bits to use in the operation. * @returns The sum as a bit array. */ function adder(xBin, yBin, carryIn, binSize) { const res = []; let carry = carryIn; for (let i = 0; i < binSize; ++i) { // res[i] = xBin[i] XOR yBin[i] XOR carry const yXorCarry = yBin[i] !== carry; res.push(xBin[i] !== yXorCarry); // newCarry = (xBin[i] AND yBin[i]) XOR (xBin[i] AND carry) XOR (yBin[i] AND carry) // = (yBin[i] XOR carry) ? xBin[i] : yBin[i] const newCarry = yBin[i] !== (yXorCarry && (xBin[i] !== yBin[i])); carry = newCarry; } return res; } /** * Constant-time addition of two bigints, using 2's complement representation. * @param x - The first operand. * @param y - The second operand. * @param binSize - The number of bits to use in the operation. * @returns The sum as a bigint. */ function ctAdd(x, y, binSize) { const resBin = adder(toBinLE(x, binSize), toBinLE(y, binSize), false, binSize); return fromBinLE(resBin); } /** * Constant-time subtraction of two bigints, using 2's complement representation. * @param x - The first operand. * @param y - The second operand. * @param binSize - The number of bits to use in the operation. * @returns The difference as a bigint. */ function ctSub(x, y, binSize) { const yBin = toBinLE(y, binSize); const yBinNot = []; for (let i = 0; i < binSize; ++i) { yBinNot.push(yBin[i] === false); } const resBin = adder(toBinLE(x, binSize), yBinNot, true, binSize); return fromBinLE(resBin); } /** * Returns the sign bit of a bigint in constant time. * @param x - The bigint to check. * @param binSize - The bit position to check (typically the highest bit). * @returns True if the sign bit is set, false otherwise. */ function ctSignBit(x, binSize) { return ((x >> binSize) & 1n) === 1n; } /** * Constant-time less-than comparison for two bigints. * @param x - The first operand. * @param y - The second operand. * @param binSize - The number of bits to use in the operation. * @returns True if x < y, false otherwise. */ function ctLt(x, y, binSize) { return ctSignBit(ctSub(x, y, binSize), binSize); } /** * Constant-time select between two bigints based on a boolean condition. * @param b - The condition; if true, select x, otherwise select y. * @param x - The value to select if b is true. * @param y - The value to select if b is false. * @param binSize - The number of bits to use in the operation. * @returns The selected bigint. */ function ctSelect(b, x, y, binSize) { return ctAdd(y, BigInt(b) * (ctSub(x, y, binSize)), binSize); } /** * Checks if a bigint fits in the range -2^binSize <= x < 2^binSize. * Not constant-time for arbitrary x, but is constant-time for all inputs for which the function returns true. * If you assert your inputs satisfy verifyBinSize(x, binSize), you need not care about the non constant-timeness of this function. * @param x - The bigint to check. * @param binSize - The number of bits to use in the check. * @returns True if x fits in the range, false otherwise. */ function verifyBinSize(x, binSize) { const bin = (x >> binSize).toString(2); return bin === '0' || bin === '-1'; } function isBrowser() { return ( // eslint-disable-next-line no-prototype-builtins typeof window !== 'undefined' && !window.process?.hasOwnProperty('type')); } function optionalLog(log, ...args) { if (log) { // eslint-disable-next-line no-console console.log(...args); } } function getBinSize(max) { // floor(log2(max)) + 1 to represent unsigned elements, a +1 for signed elements // and another +1 to account for the diff of two negative elements return BigInt(Math.floor(Math.log2(Number(max)))) + 3n; } /** * Compresses an array of bytes into 128-bit bigints. * * Takes an array of bytes whose length is a multiple of 16 and compresses each consecutive 16 bytes into a single 128-bit bigint. * * @param bytes - The input byte array. Its length must be a multiple of 16. * @returns An array of 128-bit bigints, each representing 16 bytes from the input. * @throws Error if the input length is not a multiple of 16. */ function compressUint128(bytes) { if (bytes.length % 16 !== 0) { throw Error(`bytes.length must be a multiple of 16 (found ${bytes.length})`); } const res = []; for (let n = 0; n < bytes.length / 16; ++n) { res.push(deserializeLE(bytes.slice(n * 16, (n + 1) * 16))); } return res; } /** * Decompresses an array of 128-bit bigints into a flattened byte array. * * Takes an array of 128-bit bigints and returns a Uint8Array containing the decompressed bytes (16 bytes per bigint). * * @param compressed - The input array of 128-bit bigints. Each bigint must be less than 2^128. * @returns A Uint8Array containing the decompressed bytes. * @throws Error if any bigint in the input is not less than 2^128. */ function decompressUint128(compressed) { compressed.forEach((c) => { if (c >= 1n << 128n) { throw Error(`input must be less than 2^128 (found ${c})`); } }); const res = []; for (let n = 0; n < compressed.length; ++n) { res.push(...serializeLE(compressed[n], 16)); } return new Uint8Array(res); } /** * Matrix class over FpField. Data is row-major. */ class Matrix { field; data; constructor(field, data) { this.field = field; const nrows = data.length; const ncols = data[0].length; for (let i = 1; i < nrows; ++i) { if (data[i].length !== ncols) { throw Error('All rows must have same number of columns.'); } } this.data = data.map((row) => row.map((c) => field.create(c))); } /** * Matrix multiplication between `this` and `rhs`. */ matMul(rhs) { const thisNrows = this.data.length; const thisNcols = this.data[0].length; const rhsNrows = rhs.data.length; const rhsNcols = rhs.data[0].length; if (thisNcols !== rhsNrows) { throw Error(`this.ncols must be equal to rhs.nrows (found ${thisNcols} and ${rhsNrows})`); } const data = []; for (let i = 0; i < thisNrows; ++i) { const row = []; for (let j = 0; j < rhsNcols; ++j) { let c = this.field.ZERO; for (let k = 0; k < thisNcols; ++k) { c = this.field.add(c, this.field.mul(this.data[i][k], rhs.data[k][j])); } row.push(c); } data.push(row); } return new Matrix(this.field, data); } /** * Element-wise addition between `this` and `rhs`. */ add(rhs, ct = false) { const thisNrows = this.data.length; const thisNcols = this.data[0].length; const rhsNrows = rhs.data.length; const rhsNcols = rhs.data[0].length; if (thisNrows !== rhsNrows) { throw Error(`this.nrows must be equal to rhs.nrows (found ${thisNrows} and ${rhsNrows})`); } if (thisNcols !== rhsNcols) { throw Error(`this.ncols must be equal to rhs.ncols (found ${thisNcols} and ${rhsNcols})`); } const binSize = getBinSize(this.field.ORDER - 1n); const data = []; for (let i = 0; i < thisNrows; ++i) { const row = []; for (let j = 0; j < thisNcols; ++j) { if (ct) { const sum = ctAdd(this.data[i][j], rhs.data[i][j], binSize); row.push(ctSelect(ctLt(sum, this.field.ORDER, binSize), sum, ctSub(sum, this.field.ORDER, binSize), binSize)); } else { row.push(this.field.add(this.data[i][j], rhs.data[i][j])); } } data.push(row); } return new Matrix(this.field, data); } /** * Element-wise subtraction between `this` and `rhs`. */ sub(rhs, ct = false) { const thisNrows = this.data.length; const thisNcols = this.data[0].length; const rhsNrows = rhs.data.length; const rhsNcols = rhs.data[0].length; if (thisNrows !== rhsNrows) { throw Error(`this.nrows must be equal to rhs.nrows (found ${thisNrows} and ${rhsNrows})`); } if (thisNcols !== rhsNcols) { throw Error(`this.ncols must be equal to rhs.ncols (found ${thisNcols} and ${rhsNcols})`); } const binSize = getBinSize(this.field.ORDER - 1n); const data = []; for (let i = 0; i < thisNrows; ++i) { const row = []; for (let j = 0; j < thisNcols; ++j) { if (ct) { const diff = ctSub(this.data[i][j], rhs.data[i][j], binSize); row.push(ctSelect(ctSignBit(diff, binSize), ctAdd(diff, this.field.ORDER, binSize), diff, binSize)); } else { row.push(this.field.sub(this.data[i][j], rhs.data[i][j])); } } data.push(row); } return new Matrix(this.field, data); } /** * Raises each element of `this` to the power `e`. */ pow(e) { const data = []; for (let i = 0; i < this.data.length; ++i) { const row = []; for (let j = 0; j < this.data[0].length; ++j) { row.push(this.field.pow(this.data[i][j], e)); } data.push(row); } return new Matrix(this.field, data); } /** * computs the determinant using gaus elimination * matches the determinant implementation in arcis */ det() { // Ensure the matrix is square const n = this.data.length; if (n === 0 || !this.is_square()) { throw Error('Matrix must be square and non-empty to compute the determinant.'); } let det = this.field.ONE; // Clone the data to avoid mutating the original matrix let rows = this.data.map((row) => [...row]); for (let i = 0; i < n; ++i) { // we partition into rows that have a leading zero and rows that don't const lzRows = rows.filter((r) => this.field.is0(r[0])); const nlzRows = rows.filter((r) => !this.field.is0(r[0])); // take pivot element const pivotRow = nlzRows.shift(); if (pivotRow === undefined) { // no pivot row implies the rank is less than n i.e. the determinant is zero return this.field.ZERO; } const pivot = pivotRow[0]; // multiply pivot onto the determinant det = this.field.mul(det, pivot); // subtract all leading non zero values with the pivot element (forward elimination). const pivotInverse = this.field.inv(pivot); // precomputing pivot row such that the leading value is one. This reduces the number of // multiplications in the forward elimination multiplications by 50% const normalizedPivotRow = pivotRow.map((v) => this.field.mul(pivotInverse, v)); // forward elimination with normalized pivot row const nlzRowsProcessed = nlzRows.map((row) => { const lead = row[0]; return row.map((value, index) => this.field.sub(value, this.field.mul(lead, normalizedPivotRow[index]))); }); // concat the reamining rows (without pivot row) and remove the pivot column (all first // elements (i.e. zeros) from the remaining rows). rows = nlzRowsProcessed.concat(lzRows).map((row) => row.slice(1)); } return det; } is_square() { const n = this.data.length; for (let i = 1; i < n; ++i) { if (this.data[i].length !== n) { return false; } } return true; } } function randMatrix(field, nrows, ncols) { const data = []; for (let i = 0; i < nrows; ++i) { const row = []; for (let j = 0; j < ncols; ++j) { row.push(generateRandomFieldElem(field.ORDER)); } data.push(row); } return new Matrix(field, data); } /** * Curve25519 base field as an IField instance. */ const CURVE25519_BASE_FIELD = ed25519.CURVE.Fp; // hardcode security level to 128 bits const SECURITY_LEVEL = 128; // We refer to https://tosc.iacr.org/index.php/ToSC/article/view/8695/8287 for more details. /** * Description and parameters for the Rescue cipher or hash function, including round constants, MDS matrix, and key schedule. * See: https://tosc.iacr.org/index.php/ToSC/article/view/8695/8287 */ class RescueDesc { mode; field; // The smallest prime that does not divide p-1. alpha; // The inverse of alpha modulo p-1. alphaInverse; nRounds; m; // A Maximum Distance Separable matrix. mdsMat; // Its inverse. mdsMatInverse; // The round keys, needed for encryption and decryption. roundKeys; /** * Constructs a RescueDesc for a given field and mode (cipher or hash). * Initializes round constants, MDS matrix, and key schedule. * @param field - The field to use (e.g., CURVE25519_BASE_FIELD). * @param mode - The mode: block cipher or hash function. */ constructor(field, mode) { this.field = field; this.mode = mode; switch (this.mode.kind) { case 'cipher': { this.m = this.mode.key.length; if (this.m < 2) { throw Error(`parameter m must be at least 2 (found ${this.m})`); } break; } case 'hash': { this.m = this.mode.m; break; } default: { this.m = 0; break; } } const alphaAndInverse = getAlphaAndInverse(this.field.ORDER); this.alpha = alphaAndInverse[0]; this.alphaInverse = alphaAndInverse[1]; this.nRounds = getNRounds(this.field.ORDER, this.mode, this.alpha, this.m); const mdsMatrixAndInverse = getMdsMatrixAndInverse(this.field, this.m); this.mdsMat = mdsMatrixAndInverse[0]; this.mdsMatInverse = mdsMatrixAndInverse[1]; // generate the round constants using SHAKE256 const roundConstants = this.sampleConstants(this.nRounds); switch (this.mode.kind) { case 'cipher': { // do the key schedule this.roundKeys = rescuePermutation(this.mode, this.alpha, this.alphaInverse, this.mdsMat, roundConstants, new Matrix(this.field, toVec(this.mode.key))); break; } case 'hash': { this.roundKeys = roundConstants; break; } default: { this.roundKeys = []; break; } } } /** * Samples round constants for the Rescue permutation, using SHAKE256. * @param nRounds - The number of rounds. * @returns An array of round constant matrices. */ sampleConstants(nRounds) { const field = this.field; const m = this.m; // setup randomness // dkLen is the output length from the Keccak instance behind shake. // this is irrelevant for our extendable output function (xof), but still we use // the default value from one-time shake256 hashing, as defined in shake256's definition // in noble-hashes-sha3. const hasher = shake256.create({ dkLen: 256 / 8 }); // buffer to create field elements from bytes // we add 16 bytes to get a distribution statistically close to uniform const bufferLen = Math.ceil(field.BITS / 8) + 16; switch (this.mode.kind) { case 'cipher': { hasher.update('encrypt everything, compute anything'); const rFieldArray = Array.from({ length: m * m + 2 * m }, () => { // create field element from the shake hash const randomness = hasher.xof(bufferLen); // we need not check whether the obtained field element f is in any subgroup, // because we use only prime fields (i.e. there are no subgroups) return field.create(deserializeLE(randomness)); }); // create matrix and vectors const matData = Array.from({ length: m }, () => rFieldArray.splice(0, m)); let roundConstantMat = new Matrix(field, matData); const initData = Array.from({ length: m }, () => rFieldArray.splice(0, 1)); const initialRoundConstant = new Matrix(field, initData); const roundData = Array.from({ length: m }, () => rFieldArray.splice(0, 1)); const roundConstantAffineTerm = new Matrix(field, roundData); // check for inversability while (field.is0(roundConstantMat.det())) { // resample matrix const resampleArray = Array.from({ length: m * m }, () => { const randomness = hasher.xof(bufferLen); return field.create(deserializeLE(randomness)); }); const resampleData = Array.from({ length: m }, () => resampleArray.splice(0, m)); roundConstantMat = new Matrix(field, resampleData); } const roundConstants = [initialRoundConstant]; for (let r = 0; r < 2 * this.nRounds; ++r) { roundConstants.push(roundConstantMat.matMul(roundConstants[r]).add(roundConstantAffineTerm)); } return roundConstants; } case 'hash': { hasher.update(`Rescue-XLIX(${this.field.ORDER},${m},${this.mode.capacity},${SECURITY_LEVEL})`); // this.permute requires an odd number of round keys // prepending a 0 matrix makes it equivalent to Algorithm 3 from https://eprint.iacr.org/2020/1143.pdf const zeros = []; for (let i = 0; i < m; ++i) { zeros.push([0n]); } const roundConstants = [new Matrix(field, zeros)]; const rFieldArray = Array.from({ length: 2 * m * nRounds }, () => { // create field element from the shake hash const randomness = hasher.xof(bufferLen); // we need not check whether the obtained field element f is in any subgroup, // because we use only prime fields (i.e. there are no subgroups) return field.create(deserializeLE(randomness)); }); for (let r = 0; r < 2 * nRounds; ++r) { const data = []; for (let i = 0; i < m; ++i) { data.push([rFieldArray[r * m + i]]); } roundConstants.push(new Matrix(field, data)); } return roundConstants; } default: return []; } } /** * Applies the Rescue permutation to a state matrix. * @param state - The input state matrix. * @returns The permuted state matrix. */ permute(state) { return rescuePermutation(this.mode, this.alpha, this.alphaInverse, this.mdsMat, this.roundKeys, state)[2 * this.nRounds]; } /** * Applies the inverse Rescue permutation to a state matrix. * @param state - The input state matrix. * @returns The inverse-permuted state matrix. */ permuteInverse(state) { return rescuePermutationInverse(this.mode, this.alpha, this.alphaInverse, this.mdsMatInverse, this.roundKeys, state)[2 * this.nRounds]; } } function getAlphaAndInverse(p) { const pMinusOne = p - 1n; let alpha = 0n; for (const a of [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n, 37n, 41n, 43n, 47n]) { if (pMinusOne % a !== 0n) { alpha = a; break; } } if (alpha === 0n) { throw Error('Could not find prime alpha that does not divide p-1.'); } const alphaInverse = invert(alpha, pMinusOne); return [alpha, alphaInverse]; } function getNRounds(p, mode, alpha, m) { function dcon(n) { return Math.floor(0.5 * (Number(alpha) - 1) * m * (n - 1) + 2.0); } function v(n, rate) { return m * (n - 1) + rate; } function binomial(n, k) { function factorial(x) { if (x === 0n || x === 1n) { return 1n; } return x * factorial(x - 1n); } return factorial(BigInt(n)) / (factorial(BigInt(n - k)) * factorial(BigInt(k))); } switch (mode.kind) { case 'cipher': { const l0 = Math.ceil((2 * SECURITY_LEVEL) / ((m + 1) * (Math.log2(Number(p)) - Math.log2(Number(alpha) - 1)))); let l1 = 0; if (alpha === 3n) { l1 = Math.ceil((SECURITY_LEVEL + 2) / (4 * m)); } else { l1 = Math.ceil((SECURITY_LEVEL + 3) / (5.5 * m)); } return 2 * Math.max(l0, l1, 5); } case 'hash': { // get number of rounds for Groebner basis attack const rate = m - mode.capacity; const target = 1n << BigInt(SECURITY_LEVEL); let l1 = 1; let tmp = binomial(v(l1, rate) + dcon(l1), v(l1, rate)); while (tmp * tmp <= target && l1 <= 23) { l1 += 1; tmp = binomial(v(l1, rate) + dcon(l1), v(l1, rate)); } // set a minimum value for sanity and add 50% return Math.ceil(1.5 * Math.max(5, l1)); } default: return 0; } } function buildCauchy(field, size) { const data = []; for (let i = 1n; i <= size; ++i) { const row = []; for (let j = 1n; j <= size; ++j) { row.push(field.inv(i + j)); } data.push(row); } return new Matrix(field, data); } function buildInverseCauchy(field, size) { function product(arr) { return arr.reduce((acc, curr) => field.mul(acc, field.create(curr)), field.ONE); } function prime(arr, val) { return product(arr.map((u) => { if (u !== val) { return val - u; } return 1n; })); } const data = []; for (let i = 1n; i <= size; ++i) { const row = []; for (let j = 1n; j <= size; ++j) { const a = product(Array.from({ length: size }, (_, key) => -i - BigInt(1 + key))); const aPrime = prime(Array.from({ length: size }, (_, key) => BigInt(1 + key)), j); const b = product(Array.from({ length: size }, (_, key) => j + BigInt(1 + key))); const bPrime = prime(Array.from({ length: size }, (_, key) => -BigInt(1 + key)), -i); row.push(field.mul(a, field.mul(b, field.mul(field.inv(aPrime), field.mul(field.inv(bPrime), field.inv(-i - j)))))); } data.push(row); } return new Matrix(field, data); } function getMdsMatrixAndInverse(field, m) { const mdsMat = buildCauchy(field, m); const mdsMatInverse = buildInverseCauchy(field, m); return [mdsMat, mdsMatInverse]; } function exponentForEven(mode, alpha, alphaInverse) { switch (mode.kind) { case 'cipher': { return alphaInverse; } case 'hash': { return alpha; } default: return 0n; } } function exponentForOdd(mode, alpha, alphaInverse) { switch (mode.kind) { case 'cipher': { return alpha; } case 'hash': { return alphaInverse; } default: return 0n; } } function rescuePermutation(mode, alpha, alphaInverse, mdsMat, subkeys, state) { const exponentEven = exponentForEven(mode, alpha, alphaInverse); const exponentOdd = exponentForOdd(mode, alpha, alphaInverse); const states = [state.add(subkeys[0])]; for (let r = 0; r < subkeys.length - 1; ++r) { let s = states[r]; if (r % 2 === 0) { s = s.pow(exponentEven); } else { s = s.pow(exponentOdd); } states.push(mdsMat.matMul(s).add(subkeys[r + 1])); } return states; } function rescuePermutationInverse(mode, alpha, alphaInverse, mdsMatInverse, subkeys, state) { const exponentEven = exponentForEven(mode, alpha, alphaInverse); const exponentOdd = exponentForOdd(mode, alpha, alphaInverse); // the initial state will need to be removed afterwards const states = [state]; for (let r = 0; r < subkeys.length - 1; ++r) { let s = states[r]; s = mdsMatInverse.matMul(s.sub(subkeys[subkeys.length - 1 - r])); if (r % 2 === 0) { s = s.pow(exponentEven); } else { s = s.pow(exponentOdd); } states.push(s); } states.push(states[states.length - 1].sub(subkeys[0])); states.shift(); return states; } function toVec(data) { const dataVec = []; for (let i = 0; i < data.length; ++i) { dataVec.push([data[i]]); } return dataVec; } /** * The Rescue-Prime hash function, as described in https://eprint.iacr.org/2020/1143.pdf. * Used with fixed m = 6 and capacity = 1 (rate = 5). According to Section 2.2, this offers log2(CURVE25519_BASE_FIELD.ORDER) / 2 bits of security against collision, preimage, and second-preimage attacks. * See the referenced paper for further details. */ class RescuePrimeHash { desc; rate; /** * Constructs a RescuePrimeHash instance with m = 6 and capacity = 1. */ constructor() { this.desc = new RescueDesc(CURVE25519_BASE_FIELD, { kind: 'hash', m: 6, capacity: 1 }); this.rate = 6 - 1; } // This is Algorithm 1 from https://eprint.iacr.org/2020/1143.pdf, though with the padding (see Algorithm 2). // The hash function outputs this.rate elements. /** * Computes the Rescue-Prime hash of a message, with padding as described in Algorithm 2 of the paper. * @param message - The input message as an array of bigints. * @returns The hash output as an array of bigints (length = rate). */ digest(message) { message.push(1n); while (message.length % this.rate !== 0) { message.push(0n); } const zeros = []; for (let i = 0; i < this.desc.m; ++i) { zeros.push([0n]); } let state = new Matrix(this.desc.field, zeros); for (let r = 0; r < message.length / this.rate; ++r) { const data = []; for (let i = 0; i < this.rate; ++i) { data[i] = [message[r * this.rate + i]]; } for (let i = this.rate; i < this.desc.m; ++i) { data[i] = [0n]; } const s = new Matrix(this.desc.field, data); state = this.desc.permute(state.add(s, true)); } const res = []; for (let i = 0; i < this.rate; ++i) { res.push(state.data[i][0]); } return res; } } /** * HMACRescuePrime provides a message authentication code (MAC) using the Rescue-Prime hash function. * We refer to https://datatracker.ietf.org/doc/html/rfc2104 for more details. */ class HMACRescuePrime { hasher; /** * Constructs a new HMACRescuePrime instance. */ constructor() { this.hasher = new RescuePrimeHash(); } /** * Computes the HMAC digest of a message with a given key using Rescue-Prime. * @param key - The key as an array of bigints. * @param message - The message as an array of bigints. * @returns The HMAC digest as an array of bigints. * @throws Error if the key is longer than the hash function's rate. */ digest(key, message) { // We follow https://datatracker.ietf.org/doc/html/rfc2104, though since Rescue-Prime is not based // on the Merkle-Damgard construction we cannot have an exact anology between the // parameters. For our purpose, we set B = L = hasher.rate. if (key.length > this.hasher.rate) { throw Error(`length of key is supposed to be at most the hash function's rate (found ${key.length} and ${this.hasher.rate})`); } const ipad = deserializeLE(new Uint8Array(32).fill(0x36)); const opad = deserializeLE(new Uint8Array(32).fill(0x5c)); // the key is first extended to length B for (let i = 0; i < this.hasher.rate - key.length; ++i) { key.push(0n); } // inner padding const keyPlusIpad = key.map((k) => k + ipad); keyPlusIpad.push(...message); const innerDigest = this.hasher.digest(keyPlusIpad); // outer padding const keyPlusOpad = key.map((k) => k + opad); keyPlusOpad.push(...innerDigest); return this.hasher.digest(keyPlusOpad); } } /** * HKDF (HMAC-based Extract-and-Expand Key Derivation Function) using the Rescue-Prime hash function. * Follows RFC 5869. Only supports L = HashLen. */ class HKDFRescuePrime { hmac; /** * Constructs a new HKDFRescuePrime instance. */ constructor() { this.hmac = new HMACRescuePrime(); } /** * HKDF-Extract step: derives a pseudorandom key (PRK) from the input keying material (IKM) and salt. * @param salt - The salt value as an array of bigints. * @param ikm - The input keying material as an array of bigints. * @returns The pseudorandom key (PRK) as an array of bigints. */ extract(salt, ikm) { if (salt.length === 0) { // HashLen = hasher.rate for Rescue-Prime for (let i = 0; i < this.hmac.hasher.rate; ++i) { salt.push(0n); } } return this.hmac.digest(salt, ikm); } /** * HKDF-Expand step: expands the pseudorandom key (PRK) with info to produce output keying material (OKM). * Only supports L = HashLen = 5, i.e. N = 1. * @param prk - The pseudorandom key as an array of bigints. * @param info - The context and application specific information as an array of bigints. * @returns The output keying material (OKM) as an array of bigints. */ expand(prk, info) { // we only support L = HashLen = 5, i.e. N = 1 // message = empty string | info | 0x01 info.push(1n); return this.hmac.digest(prk, info); } /** * Performs the full HKDF (extract and expand) to derive output keying material (OKM). * @param salt - The salt value as an array of bigints. * @param ikm - The input keying material as an array of bigints. * @param info - The context and application specific information as an array of bigints. * @returns The output keying material (OKM) as an array of bigints. */ okm(salt, ikm, info) { const prk = this.extract(salt, ikm); return this.expand(prk, info); } } /** * The Rescue cipher in Counter (CTR) mode, with a fixed block size m = 5. * See: https://tosc.iacr.org/index.php/ToSC/article/view/8695/8287 */ class RescueCipher { desc; /** * Constructs a RescueCipher instance using a shared secret. * The key is derived using HKDF-RescuePrime and used to initialize the RescueDesc. * @param sharedSecret - The shared secret to derive the cipher key from. */ constructor(sharedSecret) { const hkdf = new HKDFRescuePrime(); const rescueKey = hkdf.okm([], [deserializeLE(sharedSecret)], []); this.desc = new RescueDesc(CURVE25519_BASE_FIELD, { kind: 'cipher', key: rescueKey }); } /** * Encrypts the plaintext vector in Counter (CTR) mode (raw, returns bigints). * @param plaintext - The array of plaintext bigints to encrypt. * @param nonce - A 16-byte nonce for CTR mode. * @returns The ciphertext as an array of bigints. * @throws Error if the nonce is not 16 bytes long. */ encrypt_raw(plaintext, nonce) { if (nonce.length !== 16) { throw Error(`nonce must be of length 16 (found ${nonce.length})`); } const binSize = getBinSize(this.desc.field.ORDER - 1n); function encryptBatch(desc, ptxt, cntr) { if (cntr.length !== 5) { throw Error(`counter must be of length 5 (found ${cntr.length})`); } const encryptedCounter = desc.permute(new Matrix(desc.field, toVec(cntr))); const ciphertext = []; for (let i = 0; i < ptxt.length; ++i) { if (!verifyBinSize(ptxt[i], binSize - 1n) || ctSignBit(ptxt[i], binSize) || !ctLt(ptxt[i], desc.field.ORDER, binSize)) { throw Error(`plaintext must be non-negative and at most ${desc.field.ORDER}`); } const sum = ctAdd(ptxt[i], encryptedCounter.data[i][0], binSize); ciphertext.push(ctSelect(ctLt(sum, desc.field.ORDER, binSize), sum, ctSub(sum, desc.field.ORDER, binSize), binSize)); } return ciphertext; } const nBlocks = Math.ceil(plaintext.length / 5); const counter = getCounter(deserializeLE(nonce), nBlocks); const ciphertext = []; for (let i = 0; i < nBlocks; ++i) { const cnt = 5 * i; const newCiphertext = encryptBatch(this.desc, plaintext.slice(cnt, Math.min(cnt + 5, plaintext.length)), counter.slice(cnt, cnt + 5)); for (let j = 0; j < newCiphertext.length; ++j) { ciphertext.push(newCiphertext[j]); } } return ciphertext; } /** * Encrypts the plaintext vector in Counter (CTR) mode and serializes each block. * @param plaintext - The array of plaintext bigints to encrypt. * @param nonce - A 16-byte nonce for CTR mode. * @returns The ciphertext as an array of arrays of numbers (each 32 bytes). */ encrypt(plaintext, nonce) { return this.encrypt_raw(plaintext, nonce).map((c) => Array.from(serializeLE(c, 32))); } /** * Decrypts the ciphertext vector in Counter (CTR) mode (raw, expects bigints). * @param ciphertext - The array of ciphertext bigints to decrypt. * @param nonce - A 16-byte nonce for CTR mode. * @returns The decrypted plaintext as an array of bigints. * @throws Error if the nonce is not 16 bytes long. */ decrypt_raw(ciphertext, nonce) { if (nonce.length !== 16) { throw Error(`nonce must be of length 16 (found ${nonce.length})`); } const binSize = getBinSize(this.desc.field.ORDER - 1n); function decryptBatch(desc, ctxt, cntr) { if (cntr.length !== 5) { throw Error(`counter must be of length 5 (found ${cntr.length})`); } const encryptedCounter = desc.permute(new Matrix(desc.field, toVec(cntr))); const decrypted = []; for (let i = 0; i < ctxt.length; ++i) { const diff = ctSub(ctxt[i], encryptedCounter.data[i][0], binSize); decrypted.push(ctSelect(ctSignBit(diff, binSize), ctAdd(diff, desc.field.ORDER, binSize), diff, binSize)); } return decrypted; } const nBlocks = Math.ceil(ciphertext.length / 5); const counter = getCounter(deserializeLE(nonce), nBlocks); const decrypted = []; for (let i = 0; i < nBlocks; ++i) { const cnt = 5 * i; const newDecrypted = decryptBatch(this.desc, ciphertext.slice(cnt, Math.min(cnt + 5, ciphertext.length)), counter.slice(cnt, cnt + 5)); for (let j = 0; j < newDecrypted.length; ++j) { decrypted.push(newDecrypted[j]); } } return decrypted; } /** * Deserializes and decrypts the ciphertext vector in Counter (CTR) mode. * @param ciphertext - The array of arrays of numbers (each 32 bytes) to decrypt. * @param nonce - A 16-byte nonce for CTR mode. * @returns The decrypted plaintext as an array of bigints. */ decrypt(ciphertext, nonce) { return this.decrypt_raw(ciphertext.map((c) => { if (c.length !== 32) { throw Error(`ciphertext must be of length 32 (found ${c.length})`); } return deserializeLE(Uint8Array.from(c)); }), nonce); } } /** * Generates the counter values for Rescue cipher CTR mode. * @param nonce - The initial nonce as a bigint. * @param nBlocks - The number of blocks to generate counters for. * @returns An array of counter values as bigints. */ function getCounter(nonce, nBlocks) { const counter = []; for (let i = 0n; i < nBlocks; ++i) { counter.push(nonce); counter.push(i); counter.push(0n); counter.push(0n); counter.push(0n); } return counter; } // The arcisEd25519 signature scheme. This is essentially ed25519 but we use the hash function // SHA3-512 instead of SHA-512 since its multiplicative depth is much lower, which // makes it much better suited to be evaluated in MPC. // Those are the parameters specified [here](https://datatracker.ietf.org/doc/html/rfc8032#section-5.1) // (except for the hash function, see above). The below is copied from [here](https://github.com/paulmillr/noble-curves/blob/main/src/ed25519.ts#L111). const arcisEd25519Defaults = (() => ({ a: BigInt('57896044618658097711785492504343953926634992332820282019728792003956564819948'), d: BigInt('37095705934669439343138083508754565189542113879843219016388785533085940283555'), Fp: ed25519.CURVE.Fp, n: BigInt('7237005577332262213973186563042994240857116359379907606001950938285454250989'), h: BigInt(8), Gx: BigInt('15112221349535400772501151409588531511454012693041857206046113283949847762202'), Gy: BigInt('46316835694926478169428394003475163141307993866256225615783033603165251855960'), hash: sha3_512, randomBytes: randomBytes$1, adjustScalarBytes, uvRatio, }))(); /** * Clamps a 32-byte scalar as required by the Ed25519 signature scheme. * See: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.5 * @param bytes - The 32-byte scalar to clamp. * @returns The clamped scalar as a Uint8Array. */ function adjustScalarBytes(bytes) { const clamped = bytes; clamped[0] &= 248; clamped[31] &= 127; clamped[31] |= 64; return clamped; } /** * Dummy function for compatibility; not used in the ArcisEd25519 signature scheme. * Always returns { isValid: true, value: 0n }. * @returns An object with isValid: true and value: 0n. */ function uvRatio() { return { isValid: true, value: 0n }; } /** * Ed25519 curve instance using SHA3-512 for hashing, suitable for MPC (ArcisEd25519 signature scheme). * This is essentially Ed25519 but with SHA3-512 instead of SHA-512 for lower multiplicative depth. * See: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1 */ const arcisEd25519 = (() => twistedEdwards(arcisEd25519Defaults))(); /** * AES-128 cipher in Counter (CTR) mode, using HKDF-SHA3-256 to derive the key from a shared secret. * See: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf (Section 6.5) for details on CTR mode. */ class Aes128Cipher { key; /** * Constructs an AES-128 cipher instance using a shared secret. * The key is derived using HKDF-SHA3-256. * @param sharedSecret - The shared secret to derive the AES key from. */ constructor(sharedSecret) { const aesKey = hkdfSync('sha3-256', sharedSecret, new Uint8Array(), new Uint8Array(), 16); this.key = new Uint8Array(aesKey); } /** * Encrypts the plaintext array in Counter (CTR) mode. * @param plaintext - The data to encrypt. * @param nonce - An 8-byte nonce for CTR mode. * @returns The encrypted ciphertext as a Uint8Array. * @throws Error if the nonce is not 8 bytes long. */ encrypt(plaintext, nonce) { if (nonce.length !== 8) { throw Error(`nonce must be of length 8 (found ${nonce.length})`); } const paddedNonce = Buffer.concat([nonce, Buffer.alloc(16 - nonce.length, 0)]); const cipher = createCipheriv('aes-128-ctr', this.key, paddedNonce); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); return new Uint8Array(ciphertext); } /** * Decrypts the ciphertext array in Counter (CTR) mode. * @param ciphertext - The data to decrypt. * @param nonce - An 8-byte nonce for CTR mode. * @returns The decrypted plaintext as a Uint8Array. * @throws Error if the nonce is not 8 bytes long. */ decrypt(ciphertext, nonce) { if (nonce.length !== 8) { throw Error(`nonce must be of length 8 (found ${nonce.length})`); } const paddedNonce = Buffer.concat([nonce, Buffer.alloc(16 - nonce.length, 0)]); const cipher = createDecipheriv('aes-128-ctr', this.key, paddedNonce); const decrypted = Buffer.concat([cipher.update(ciphertext), cipher.final()]); return new Uint8Array(decrypted); } } /** * AES-192 cipher in Counter (CTR) mode, using HKDF-SHA3-256 to derive the key from a shared secret. * See: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf (Section 6.5) for details on CTR mode. */ class Aes192Cipher { key; /** * Constructs an AES-192 cipher instance using a shared secret. * The key is derived using HKDF-SHA3-256. * @param sharedSecret - The shared secret to derive the AES key from. */ constructor(sharedSecret) { const aesKey = hkdfSync('sha3-256', sharedSecret, new Uint8Array(), new Uint8Array(), 24); this.key = new Uint8Array(aesKey); } /** * Encrypts the plaintext array in Counter (CTR) mode. * @param plaintext - The data to encrypt. * @param nonce - An 8-byte nonce for CTR mode. * @returns The encrypted ciphertext as a Uint8Array. * @throws Error if the nonce is not 8 bytes long. */ encrypt(plaintext, nonce) { if (nonce.length !== 8) { throw Error(`nonce must be of length 8 (found ${nonce.length})`); } const paddedNonce = Buffer.concat([nonce, Buffer.alloc(16 - nonce.length, 0)]); const cipher = createCipheriv('aes-192-ctr', this.key, paddedNonce); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); return new Uint8Array(ciphertext); } /** * Decrypts the ciphertext array in Counter (CTR) mode. * @param ciphertext - The data to decrypt. * @param nonce - An 8-byte nonce for CTR mode. * @returns The decrypted plaintext as a Uint8Array. * @throws Error if the nonce is not 8 bytes long. */ decrypt(ciphertext, nonce) { if (nonce.length !== 8) { throw Error(`nonce must be of length 8 (found ${nonce.length})`); } const paddedNonce = Buffer.concat([nonce, Buffer.alloc(16 - nonce.length, 0)]); const cipher = createDecipheriv('aes-192-ctr', this.key, paddedNonce); const decrypted = Buffer.concat([cipher.update(ciphertext), cipher.final()]); return new Uint8Array(decrypted); } } /** * AES-256 cipher in Counter (CTR) mode, using HKDF-SHA3-256 to derive the key from a shared secret. * See: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf (Section 6.5) for details on CTR mode. */ class Aes256Cipher { key; /** * Constructs an AES-256 cipher instance using a shared secret. * The key is derived using HKDF-SHA3-256. * @param sharedSecret - The shared secret to derive the AES key from. */ constructor(sharedSecret) { const aesKey = hkdfSync('sha3-256', sharedSecret, new Uint8Array(), new Uint8Array(), 32); this.key = new Uint8Array(aesKey); } /** * Encrypts the plaintext array in Counter (CTR) mode. * @param plaintext - The data to encrypt. * @param nonce - An 8-byte nonce for CTR mode. * @returns The encrypted ciphertext as a Uint8Array. * @throws Error if the nonce is not 8 bytes long. */ encrypt(plaintext, nonce) { if (nonce.length !== 8) { throw Error(`nonce must be of length 8 (found ${nonce.length})`); } const paddedNonce = Buffer.concat([nonce, Buffer.alloc(16 - nonce.length, 0)]); const cipher = createCipheriv('aes-256-ctr', this.key, paddedNonce); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); return new Uint8Array(ciphertext); } /** * Decrypts the ciphertext array in Counter (CTR) mode. * @param ciphertext - The data to decrypt. * @param nonce - An 8-byte nonce for CTR mode. * @returns The decrypted plaintext as a Uint8Array. * @throws Error if the nonce is not 8 bytes long. */ decrypt(ciphertext, nonce) { if (nonce.length !== 8) { throw Error(`nonce must be of length 8 (found ${nonce.length})`); } const paddedNonce = Buffer.concat([nonce, Buffer.alloc(16 - nonce.length, 0)]); const cipher = createDecipheriv('aes-256-ctr', this.key, paddedNonce); const decrypted = Buffer.concat([cipher.update(ciphertext), cipher.final()]); return new Uint8Array(decrypted); } } var address = "BKck65TgoKRokMjQM3