UNPKG

blocklock-js

Version:

A library for encrypting and decrypting data for the future

485 lines (420 loc) 13.8 kB
// eslint-disable @typescript-eslint/no-unused-vars import { dataSlice, hexlify, getBytes, keccak256, randomBytes, solidityPacked, BytesLike, isHexString } from "ethers" import * as mcl from "mcl-wasm" import type {G1, G2, Fr, Fp, Fp2} from "mcl-wasm" import {utf8ToBytes} from "@noble/curves/abstract/utils" /** * Mcl wrapper for BLS operations * Adapted from: https://github.com/kilic/evmbls */ class BlsBn254 { static readonly FIELD_ORDER = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47n public readonly G1: G1 public readonly G2: G2 private constructor() { this.G1 = new mcl.G1() const g1x: Fp = new mcl.Fp() const g1y: Fp = new mcl.Fp() const g1z: Fp = new mcl.Fp() g1x.setStr("01", 16) g1y.setStr("02", 16) g1z.setInt(1) this.G1.setX(g1x) this.G1.setY(g1y) this.G1.setZ(g1z) this.G2 = new mcl.G2() const g2x = createFp2( "0x1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed", "0x198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2", ) const g2y = createFp2( "0x12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa", "0x090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b", ) const g2z = createFp2("0x01", "0x00") this.G2.setX(g2x) this.G2.setY(g2y) this.G2.setZ(g2z) } public static async create() { await mcl.init(mcl.BN_SNARK1) mcl.setETHserialization(true) mcl.setMapToMode(0) // FT return new BlsBn254() } public newG1(): G1 { return new mcl.G1() } public newG2(): G2 { return new mcl.G2() } public newFp(): Fp { return new mcl.Fp() } public expandMsg(domain: Uint8Array, msg: Uint8Array, outLen: number): Uint8Array { if (domain.length > 255) { throw new Error("bad domain size") } const domainLen = domain.length if (domainLen > 255) { throw new Error("InvalidDSTLength") } const zpad = new Uint8Array(136) const b_0 = solidityPacked( ["bytes", "bytes", "uint8", "uint8", "uint8", "bytes", "uint8"], [zpad, msg, outLen >> 8, outLen & 0xff, 0, domain, domainLen], ) const b0 = keccak256(b_0) const b_i = solidityPacked( ["bytes", "uint8", "bytes", "uint8"], [b0, 1, domain, domain.length], ) let bi = keccak256(b_i) const out = new Uint8Array(outLen) const ell = Math.floor((outLen + 32 - 1) / 32) // keccak256 blksize for (let i = 1; i < ell; i++) { const b_i = solidityPacked( ["bytes32", "uint8", "bytes", "uint8"], [toHex(BigInt(b0) ^ BigInt(bi)), 1 + i, domain, domain.length], ) const bi_bytes = getBytes(bi) for (let j = 0; j < 32; j++) { out[(i - 1) * 32 + j] = bi_bytes[j] } bi = keccak256(b_i) } const bi_bytes = getBytes(bi) for (let j = 0; j < 32; j++) { out[(ell - 1) * 32 + j] = bi_bytes[j] } return out } public hashToField(domain: Uint8Array, msg: Uint8Array, count: number): bigint[] { const u = 48 const _msg = this.expandMsg(domain, msg, count * u) const els: bigint[] = [] for (let i = 0; i < count; i++) { const el = mod(BigInt(hexlify(_msg.slice(i * u, (i + 1) * u))), BlsBn254.FIELD_ORDER) els.push(el) } return els } public hashToPoint(domain: Uint8Array, msg: Uint8Array): G1 { const hashRes = this.hashToField(domain, msg, 2) const e0 = hashRes[0] const e1 = hashRes[1] const p0 = this.mapToPoint(toHex(e0)) const p1 = this.mapToPoint(toHex(e1)) const p = mcl.add(p0, p1) p.normalize() return p } public serialiseFp(p: Fp | Fp2): `0x${string}` { // NB: big-endian return ("0x" + Array.from(p.serialize()) .reverse() .map((value) => value.toString(16).padStart(2, "0")) .join("")) as `0x${string}` } public serialiseG1Point(p: G1): [bigint, bigint] { p.normalize() const x = BigInt(this.serialiseFp(p.getX())) const y = BigInt(this.serialiseFp(p.getY())) return [x, y] } public serialiseG2Point(p: G2): [bigint, bigint, bigint, bigint] { const x = this.serialiseFp(p.getX()) const y = this.serialiseFp(p.getY()) return [ BigInt(dataSlice(x, 32)), BigInt(dataSlice(x, 0, 32)), BigInt(dataSlice(y, 32)), BigInt(dataSlice(y, 0, 32)), ] } public deserialiseFp2([x, y]: [bigint, bigint]): Fp2 { const xx = new mcl.Fp() const yy = new mcl.Fp() xx.setStr(toHex(x), 16) yy.setStr(toHex(y), 16) const out = new mcl.Fp2() out.set_a(xx) out.set_b(yy) return out } public g1FromEvmHex(input: BytesLike) { const hex = hexlify(input) const trimmed = hex.startsWith("0x") ? hex.slice(2) : hex const x = trimmed.slice(0, trimmed.length / 2) const y = trimmed.slice(trimmed.length / 2, trimmed.length) return this.g1FromEvm(BigInt(`0x${x}`), BigInt(`0x${y}`)) } public g1FromEvm(g1X: bigint, g1Y: bigint) { const x = toHex(g1X) const y = toHex(g1Y) const Mx = this.newFp() const My = this.newFp() const Mz = this.newFp() Mx.setStr(x, 16) My.setStr(y, 16) Mz.setInt(1) const M = this.newG1() M.setX(Mx) M.setY(My) M.setZ(Mz) return M } public g2FromEvm([x1, x2, y1, y2]: [bigint, bigint, bigint, bigint]) { const out = this.newG2() out.setX(this.deserialiseFp2([x1, x2])) out.setY(this.deserialiseFp2([y1, y2])) // this is swapped because EVM (: out.setZ(this.deserialiseFp2([1n, 0n])) return out } public g2From(input: BytesLike): G2 { const out = this.G2.clone() out.deserialize(bytes(input)) return out } public createKeyPair(_secretKey?: `0x${string}`) { if (!_secretKey) { _secretKey = hexlify(randomBytes(31)) as `0x${string}` } const secretKey: Fr = new mcl.Fr() secretKey.setHashOf(_secretKey) const pubKey: G2 = mcl.mul(this.G2, secretKey) pubKey.normalize() return { secretKey, _secretKey, pubKey, } } public createKeyPairG1PublicKey(_secretKey?: `0x${string}`) { if (!_secretKey) { _secretKey = hexlify(randomBytes(31)) as `0x${string}` } const secretKey: Fr = new mcl.Fr() secretKey.setHashOf(_secretKey) const pubKey: G1 = mcl.mul(this.G1, secretKey) pubKey.normalize() return { secretKey, _secretKey, pubKey, } } public sign(M: G1, secret: Fr) { const signature: G1 = mcl.mul(M, secret) signature.normalize() return { signature, M, } } public verify(h_m: G1, pk: G2, signature: G1): boolean { return mcl.pairing(h_m, pk).isEqual(mcl.pairing(signature, this.G2)) } public toArgs(pubKey: G2, M: G1, signature: G1) { return { signature: this.serialiseG1Point(signature), pubKey: this.serialiseG2Point(pubKey), M: this.serialiseG1Point(M), } } public mapToPoint(eHex: `0x${string}`): G1 { const C2 = 0x183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea3n const C3 = 0x16789af3a83522eb353c98fc6b36d713d5d8d1cc5dffffffan const C4 = 0x10216f7ba065e00de81ac1e7808072c9dd2b2385cd7b438469602eb24829a9bdn const Z = 1n const g = this.g.bind(this) const neg = this.neg.bind(this) const add = this.add.bind(this) const sub = this.sub.bind(this) const mul = this.mul.bind(this) const inv0 = this.inv0.bind(this) const sgn0 = this.sgn0.bind(this) const legendre = this.legendre.bind(this) const sqrt = this.sqrt.bind(this) const u = BigInt(eHex) let tv1 = mul(mul(u, u), g(Z)) const tv2 = add(1n, tv1) tv1 = sub(1n, tv1) const tv3 = inv0(mul(tv1, tv2)) const tv5 = mul(mul(mul(u, tv1), tv3), C3) const x1 = add(C2, neg(tv5)) const x2 = add(C2, tv5) const tv7 = mul(tv2, tv2) const tv8 = mul(tv7, tv3) const x3 = add(Z, mul(C4, mul(tv8, tv8))) let x let y if (legendre(g(x1)) === 1n) { x = x1 y = sqrt(g(x1)) } else if (legendre(g(x2)) === 1n) { x = x2 y = sqrt(g(x2)) } else { x = x3 y = sqrt(g(x3)) } if (sgn0(u) != sgn0(y)) { y = neg(y) } const g1x: Fp = new mcl.Fp() const g1y: Fp = new mcl.Fp() const g1z: Fp = new mcl.Fp() g1x.setStr(x.toString(), 10) g1y.setStr(y.toString(), 10) g1z.setInt(1) const point: G1 = new mcl.G1() point.setX(g1x) point.setY(g1y) point.setZ(g1z) return point } private g(x: bigint): bigint { const mul = this.mul.bind(this) const add = this.add.bind(this) return add(mul(mul(x, x), x), 3n) } private neg(x: bigint) { return mod(-x, BlsBn254.FIELD_ORDER) } private mul(a: bigint, b: bigint) { return mod(a * b, BlsBn254.FIELD_ORDER) } private add(a: bigint, b: bigint) { return mod(a + b, BlsBn254.FIELD_ORDER) } private sub(a: bigint, b: bigint) { return mod(a - b, BlsBn254.FIELD_ORDER) } private exp(x: bigint, n: bigint): bigint { const mul = this.mul.bind(this) let result = 1n let base = mod(x, BlsBn254.FIELD_ORDER) let e_prime = n while (e_prime > 0) { if (mod(e_prime, 2n) == 1n) { result = mul(result, base) } e_prime = e_prime >> 1n base = mul(base, base) } return result } private sqrt(u: bigint) { return this.exp(u, 0xc19139cb84c680a6e14116da060561765e05aa45a1c72a34f082305b61f3f52n) } private sgn0(x: bigint) { return mod(x, 2n) } private inv0(x: bigint) { if (x === 0n) { return 0n } return this.exp(x, BlsBn254.FIELD_ORDER - 2n) } private legendre(u: bigint): 1n | 0n | -1n { const x = this.exp(u, 0x183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea3n) if (x === BlsBn254.FIELD_ORDER - 1n) { return -1n } if (x !== 0n && x !== 1n) { throw Error("Legendre symbol calc failed") } return x } public aggregate(acc: G1 | G2, other: G1 | G2) { const _acc = mcl.add(acc, other); _acc.normalize(); return _acc; } } function bytes(b: BytesLike): Uint8Array { if (typeof b === "string" && !isHexString(b)) { return utf8ToBytes(b) } return getBytes(b) } export function byteSwap(hex: string, n: number) { const bytes = getBytes("0x" + hex) if (bytes.byteLength !== n) throw new Error(`Invalid length: ${bytes.byteLength}`) return Array.from(bytes) .reverse() .map((v) => v.toString(16).padStart(2, "0")) .join("") } // mcl format: x = a + bi // kyber format: x = b + ai export function kyberMarshalG2(p: G2) { return [ byteSwap(p.getX().get_b().serializeToHexStr(), 32), byteSwap(p.getX().get_a().serializeToHexStr(), 32), byteSwap(p.getY().get_b().serializeToHexStr(), 32), byteSwap(p.getY().get_a().serializeToHexStr(), 32), ].join("") } export function kyberMarshalG1(p: G1) { return [ byteSwap(p.getX().serializeToHexStr(), 32), byteSwap(p.getY().serializeToHexStr(), 32), ].join("") } function mod(a: bigint, b: bigint) { return ((a % b) + b) % b } export function toHex(n: bigint): `0x${string}` { return ("0x" + n.toString(16).padStart(64, "0")) as `0x${string}` } function createFp2(a: string, b: string) { const fp2_a: Fp = new mcl.Fp() const fp2_b: Fp = new mcl.Fp() fp2_a.setStr(a) fp2_b.setStr(b) const fp2: Fp2 = new mcl.Fp2() fp2.set_a(fp2_a) fp2.set_b(fp2_b) return fp2 } function bytesEqual(bytes1: BytesLike, bytes2: BytesLike): boolean { const arr1 = getBytes(bytes1) const arr2 = getBytes(bytes2) if (arr1.length !== arr2.length) { return false } return arr1.every((value, index) => value === arr2[index]) } export function serialiseG2Point(p: G2): [bigint, bigint, bigint, bigint] { const x = serialiseFp(p.getX()) const y = serialiseFp(p.getY()) return [ BigInt(dataSlice(x, 32)), BigInt(dataSlice(x, 0, 32)), BigInt(dataSlice(y, 32)), BigInt(dataSlice(y, 0, 32)), ] } export function serialiseFp(p: Fp | Fp2): `0x${string}` { // NB: big-endian return ("0x" + Array.from(p.serialize()) .reverse() .map((value) => value.toString(16).padStart(2, "0")) .join("")) as `0x${string}` } export {BlsBn254, bytesEqual}