diff --git a/package-lock.json b/package-lock.json index d047d37..e765626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "tlock-js", - "version": "0.7.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tlock-js", - "version": "0.7.0", + "version": "0.9.0", "license": "(Apache-2.0 OR MIT)", "dependencies": { - "@noble/bls12-381": "^1.4.0", - "@noble/hashes": "^1.3.1", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@stablelib/chacha20poly1305": "^1.0.1", "buffer": "^6.0.3", - "drand-client": "1.2.3" + "drand-client": "1.2.5" }, "devDependencies": { "@types/chai": "^4.3.5", @@ -1216,33 +1216,21 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/@noble/bls12-381": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/bls12-381/-/bls12-381-1.4.0.tgz", - "integrity": "sha512-mIYqC2jMX7Lcu1QtU/FFftMPDSXNCdlGex6BSf5nPojHjnzzBgs1klFWpB1R8YjqHmOO9xrCzF19f2c42+z3vg==", - "deprecated": "Switch to @noble/curves for improved security & updates", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", "dependencies": { - "@noble/hashes": "1.3.2" + "@noble/hashes": "1.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -2357,12 +2345,12 @@ } }, "node_modules/drand-client": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/drand-client/-/drand-client-1.2.3.tgz", - "integrity": "sha512-2MZRiQoZP0hlf7kGPzAVC/e6GGLvWE5QiNYv5X/PIzZdftt36v6E+hGYEp3aEdZ9WgotNO35vpcrXcZaCRy7eA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/drand-client/-/drand-client-1.2.5.tgz", + "integrity": "sha512-fytRLLH7hIoSzZScrG06jgv0P30+neUGIhHoCcykWaNDFgy9PLqD/V4I0BouyVOCiWc03wTgRacH9xxZfyiWug==", "dependencies": { "@babel/traverse": "^7.23.2", - "@noble/curves": "^1.1.0", + "@noble/curves": "^1.4.0", "buffer": "^6.0.3" }, "engines": { diff --git a/package.json b/package.json index 0decb14..6775de0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tlock-js", - "version": "0.8.0", + "version": "0.9.0", "description": "A library to encrypt data that can only be decrypted in the future using drand", "source": "src/index.ts", "main": "index.js", @@ -42,11 +42,11 @@ "typescript": "^5.1.6" }, "dependencies": { - "@noble/bls12-381": "^1.4.0", - "@noble/hashes": "^1.3.1", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", "@stablelib/chacha20poly1305": "^1.0.1", "buffer": "^6.0.3", - "drand-client": "1.2.3" + "drand-client": "1.2.5" }, "browserslist": [ "> 0.5%", diff --git a/src/crypto/fp.ts b/src/crypto/fp.ts new file mode 100644 index 0000000..c86d05a --- /dev/null +++ b/src/crypto/fp.ts @@ -0,0 +1,99 @@ +import * as mod from "@noble/curves/abstract/modular" + +// these are all some helpers around the field elements used in the BLS lib that got abstracted away +// when migrating from @noble/bls12-381 -> @noble/curves +declare const Fp: Readonly & Required, "isOdd">>>; +type Fp = bigint; +type BigintTuple = [bigint, bigint]; +type Fp2 = { + c0: bigint; + c1: bigint; +}; + +function fp2FromBigTuple(t: BigintTuple): Fp2 { + return { + c0: t[0], + c1: t[1] + } +} + +declare const Fp2: mod.IField +type BigintSix = [bigint, bigint, bigint, bigint, bigint, bigint] +type Fp6 = { + c0: Fp2; + c1: Fp2; + c2: Fp2; +}; + +declare const Fp6: mod.IField + +function fp6FromBigSix(b: BigintSix): Fp6 { + return { + c0: { + c0: b[0], + c1: b[1], + }, + c1: { + c0: b[2], + c1: b[3], + }, + c2: { + c0: b[4], + c1: b[5], + } + } +} + +type Fp12 = { + c0: Fp6; + c1: Fp6; +}; +type BigintTwelve = [ + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint, + bigint +]; + +function fp12FromBigTwelve(b: BigintTwelve): Fp12 { + return { + c0: { + c0: { + c0: b[0], + c1: b[1], + }, + c1: { + c0: b[2], + c1: b[3], + }, + c2: { + c0: b[4], + c1: b[5], + } + }, + c1: { + c0: { + c0: b[6], + c1: b[7], + }, + c1: { + c0: b[8], + c1: b[9], + }, + c2: { + c0: b[10], + c1: b[11], + } + } + } +} + +export {Fp, Fp2, Fp6, Fp12, fp2FromBigTuple, fp6FromBigSix, fp12FromBigTwelve, BigintTwelve} \ No newline at end of file diff --git a/src/crypto/ibe.ts b/src/crypto/ibe.ts index 29abd72..2264d71 100644 --- a/src/crypto/ibe.ts +++ b/src/crypto/ibe.ts @@ -1,47 +1,37 @@ -import * as bls from "@noble/bls12-381" -import {Fp12, PointG1, PointG2, utils} from "@noble/bls12-381" import {sha256} from "@noble/hashes/sha256" -import { Buffer } from "buffer" +import {randomBytes} from "@noble/hashes/utils" +import {bls12_381} from "@noble/curves/bls12-381" +import {Buffer} from "buffer" import {bytesToNumberBE, fp12ToBytes, xor} from "./utils" +import {Fp12} from "./fp" -export interface Ciphertext { - U: T +export interface Ciphertext { + U: Uint8Array V: Uint8Array W: Uint8Array } +const PointG1 = bls12_381.G1 +const PointG2 = bls12_381.G2 -interface Mul { - multiply(scalar: bigint): T; -} - -async function encrypt( - master: T, - ID: Uint8Array, - msg: Uint8Array, - base: Mul, - hashToCurve: (id: Uint8Array) => Promise, - pairing: (m: T, q: U) => Fp12 -): Promise> { - +export async function encryptOnG1(master: Uint8Array, ID: Uint8Array, msg: Uint8Array): Promise { if (msg.length >> 8 > 1) { throw new Error("cannot encrypt messages larger than our hash output: 256 bits.") } // 1. Compute Gid = e(master,Q_id) - const Qid = await hashToCurve(ID) - const Gid = pairing(master, Qid) - + const Qid = PointG2.hashToCurve(ID, {DST: "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_"}) as never + const m = PointG1.ProjectivePoint.fromHex(master) + const Gid = bls12_381.pairing(m, Qid) // 2. Derive random sigma - const sigma = utils.randomBytes(msg.length) + const sigma = randomBytes(msg.length) // 3. Derive r from sigma and msg and get a field element const r = h3(sigma, msg) - const U = base.multiply(r) + const U = PointG1.ProjectivePoint.BASE.multiply(r) // 5. Compute V = sigma XOR H2(rGid) - const rGid = Gid.pow(r) - const hrGid = await gtToHash(rGid, msg.length) - + const rGid = bls12_381.fields.Fp12.pow(Gid, r) + const hrGid = gtToHash(rGid, msg.length) const V = xor(sigma, hrGid) // 6. Compute M XOR H(sigma) @@ -50,48 +40,59 @@ async function encrypt( const W = xor(msg, hsigma) return { - U: U, + U: U.toRawBytes(), V: V, W: W, } } -export async function encryptOnG1(master: PointG1, ID: Uint8Array, msg: Uint8Array): Promise> { - return encrypt(master, ID, msg, PointG1.BASE, - (id: Uint8Array) => bls.PointG2.hashToCurve(id, { DST: "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_" }), - (m, Qid) => bls.pairing(m, Qid) - ) -} - // uses the DST for G2 erroneously -export async function encryptOnG2(master: PointG2, ID: Uint8Array, msg: Uint8Array): Promise> { - return encrypt(master, ID, msg, PointG2.BASE, - (id: Uint8Array) => bls.PointG1.hashToCurve(id, { DST: "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_" }), - (m, Qid) => bls.pairing(Qid, m) - ) +export async function encryptOnG2(master: Uint8Array, ID: Uint8Array, msg: Uint8Array): Promise { + return encOnG2(master, ID, msg, "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_") } -export async function encryptOnG2RFC9380(master: PointG2, ID: Uint8Array, msg: Uint8Array): Promise> { - return encrypt(master, ID, msg, PointG2.BASE, - (id: Uint8Array) => bls.PointG1.hashToCurve(id, { DST: "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" }), - (m, Qid) => bls.pairing(Qid, m) - ) +export async function encryptOnG2RFC9380(master: Uint8Array, ID: Uint8Array, msg: Uint8Array): Promise { + return encOnG2(master, ID, msg, "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_") + } -interface Eq { - equals(other: T): boolean +async function encOnG2(master: Uint8Array, ID: Uint8Array, msg: Uint8Array, dst: string): Promise { + if (msg.length >> 8 > 1) { + throw new Error("cannot encrypt messages larger than our hash output: 256 bits.") + } + // 1. Compute Gid = e(master,Q_id) + const Qid = PointG1.hashToCurve(ID, {DST: dst}) as never + const m = PointG2.ProjectivePoint.fromHex(master) + const Gid = bls12_381.pairing(Qid, m) + // 2. Derive random sigma + const sigma = randomBytes(msg.length) + + // 3. Derive r from sigma and msg and get a field element + const r = h3(sigma, msg) + const U = PointG2.ProjectivePoint.BASE.multiply(r) + // 5. Compute V = sigma XOR H2(rGid) + const rGid = bls12_381.fields.Fp12.pow(Gid, r) + const hrGid = gtToHash(rGid, msg.length) + const V = xor(sigma, hrGid) + + // 6. Compute M XOR H(sigma) + const hsigma = h4(sigma, msg.length) + + const W = xor(msg, hsigma) + + return { + U: U.toRawBytes(), + V: V, + W: W, + } } -async function decrypt, U>( - point: U, - ciphertext: Ciphertext, - base: Mul, - pairing: (m: T, q: U) => Fp12 -): Promise { +export async function decryptOnG1(key: Uint8Array, ciphertext: Ciphertext): Promise { // 1. Compute sigma = V XOR H2(e(rP,private)) - const gidt = pairing(ciphertext.U, point) + const Qid = PointG1.ProjectivePoint.fromHex(ciphertext.U) + const m = PointG2.ProjectivePoint.fromHex(key) + const gidt = bls12_381.pairing(Qid, m) const hgidt = gtToHash(gidt, ciphertext.W.length) - if (hgidt.length != ciphertext.V.length) { throw new Error("XorSigma is of invalid length") } @@ -104,43 +105,43 @@ async function decrypt, U>( // 3. Check U = rP const r = h3(sigma, msg) - const rP = base.multiply(r) + const rP = PointG1.ProjectivePoint.BASE.multiply(r) - if (!rP.equals(ciphertext.U)) { + if (!rP.equals(Qid)) { throw new Error("invalid proof: rP check failed") } return msg } -export async function decryptOnG1(point: PointG2, ciphertext: Ciphertext): Promise { - return decrypt( - point, - ciphertext, - PointG1.BASE, - (m: PointG1, Qid: PointG2) => bls.pairing(m, Qid) - ) -} +export async function decryptOnG2(key: Uint8Array, ciphertext: Ciphertext): Promise { + // 1. Compute sigma = V XOR H2(e(rP,private)) + const Qid = PointG1.ProjectivePoint.fromHex(key) + const m = PointG2.ProjectivePoint.fromHex(ciphertext.U) + const gidt = bls12_381.pairing(Qid, m) + const hgidt = gtToHash(gidt, ciphertext.W.length) + if (hgidt.length != ciphertext.V.length) { + throw new Error("XorSigma is of invalid length") + } + const sigma = xor(hgidt, ciphertext.V) -export async function decryptOnG2(point: PointG1, ciphertext: Ciphertext) { - return decrypt( - point, - ciphertext, - PointG2.BASE, - (p: PointG2, q: PointG1) => bls.pairing(q, p) - ) -} + // 2. Compute M = W XOR H4(sigma) + const hsigma = h4(sigma, ciphertext.W.length) + + const msg = xor(hsigma, ciphertext.W) + + // 3. Check U = rP + const r = h3(sigma, msg) + const rP = PointG2.ProjectivePoint.BASE.multiply(r) + + if (!rP.equals(m)) { + throw new Error("invalid proof: rP check failed") + } -export async function decryptOnG2RFC9380(point: PointG1, ciphertext: Ciphertext) { - return decrypt( - point, - ciphertext, - PointG2.BASE, - (p: PointG2, q: PointG1) => bls.pairing(q, p) - ) + return msg } -export function gtToHash(gt: bls.Fp12, len: number): Uint8Array { +export function gtToHash(gt: Fp12, len: number): Uint8Array { return sha256 .create() .update("IBE-H2") @@ -171,7 +172,7 @@ function h3(sigma: Uint8Array, msg: Uint8Array) { // assuming Big Endianness data[0] = data[0] >> BitsToMaskForBLS12381 const n = bytesToNumberBE(data) - if (n < bls.CURVE.r) { + if (n < bls12_381.fields.Fr.ORDER) { return n } } diff --git a/src/crypto/utils.ts b/src/crypto/utils.ts index 297a6f6..cf8c406 100644 --- a/src/crypto/utils.ts +++ b/src/crypto/utils.ts @@ -1,5 +1,5 @@ import {Buffer} from "buffer" -import {Fp, Fp12, Fp2} from "@noble/bls12-381" +import {Fp, Fp12, Fp2, Fp6} from "./fp" // returns a new array with the xor of a ^ b export function xor(a: Uint8Array, b: Uint8Array): Uint8Array { @@ -39,7 +39,7 @@ export function bytesToHex(uint8a: Uint8Array): string { // weirdly all the child FPs have to be reversed when serialising to bytes export function fpToBytes(fp: Fp): Uint8Array { // 48 bytes = 96 hex bytes - const hex = BigInt(fp.value).toString(16).padStart(96, "0") + const hex = fp.toString(16).padStart(96, "0") const buf = Buffer.alloc(hex.length / 2) buf.write(hex, "hex") return buf @@ -51,7 +51,7 @@ export function fp2ToBytes(fp2: Fp2): Uint8Array { // fp6 isn't exported by noble... let's take off the guard rails // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function fp6ToBytes(fp6: any): Uint8Array { +export function fp6ToBytes(fp6: Fp6): Uint8Array { return Buffer.concat([fp6.c2, fp6.c1, fp6.c0].map(fp2ToBytes)) } diff --git a/src/drand/timelock-decrypter.ts b/src/drand/timelock-decrypter.ts index 694c8c7..6fc124b 100644 --- a/src/drand/timelock-decrypter.ts +++ b/src/drand/timelock-decrypter.ts @@ -1,10 +1,10 @@ -import {PointG1, PointG2} from "@noble/bls12-381" import {Buffer} from "buffer" import {ChainClient, fetchBeacon, roundTime} from "drand-client" import {Stanza} from "../age/age-encrypt-decrypt" import * as ibe from "../crypto/ibe" import {Ciphertext} from "../crypto/ibe" import {Point} from "./index" +import {bls12_381} from "@noble/curves/bls12-381" export function createTimelockDecrypter(network: ChainClient) { return async (recipients: Array): Promise => { @@ -35,19 +35,16 @@ export function createTimelockDecrypter(network: ChainClient) { switch (chainInfo.schemeID) { case "pedersen-bls-unchained": { - const g2 = PointG2.fromHex(beacon.signature) - const ciphertext = parseCiphertext(body, PointG1.BASE, PointG1.fromHex) - return await ibe.decryptOnG1(g2, ciphertext) + const ciphertext = parseCiphertext(body, bls12_381.G1.ProjectivePoint.BASE) + return await ibe.decryptOnG1(Buffer.from(beacon.signature, "hex"), ciphertext) } case "bls-unchained-on-g1": { - const g1 = PointG1.fromHex(beacon.signature) - const cipherText = parseCiphertext(body, PointG2.BASE, PointG2.fromHex) - return ibe.decryptOnG2(g1, cipherText) + const ciphertext = parseCiphertext(body, bls12_381.G2.ProjectivePoint.BASE) + return await ibe.decryptOnG2(Buffer.from(beacon.signature, "hex"), ciphertext) } case "bls-unchained-g1-rfc9380": { - const g1 = PointG1.fromHex(beacon.signature) - const cipherText = parseCiphertext(body, PointG2.BASE, PointG2.fromHex) - return ibe.decryptOnG2RFC9380(g1, cipherText) + const ciphertext = parseCiphertext(body, bls12_381.G2.ProjectivePoint.BASE) + return await ibe.decryptOnG2(Buffer.from(beacon.signature, "hex"), ciphertext) } default: throw Error(`Unsupported scheme: ${chainInfo.schemeID} - you must use a drand network with an unchained scheme for timelock decryption!`) @@ -66,13 +63,13 @@ export function createTimelockDecrypter(network: ChainClient) { return roundNumberParsed } - function parseCiphertext(body: Uint8Array, base: Point, fromHex: (buf: Buffer) => T): Ciphertext { + function parseCiphertext(body: Uint8Array, base: Point): Ciphertext { const pointLength = base.toRawBytes(true).byteLength const pointBytes = body.subarray(0, pointLength) const theRest = body.subarray(pointLength) const eachHalf = theRest.length / 2 - const U = fromHex(Buffer.from(pointBytes)) + const U = pointBytes const V = theRest.subarray(0, eachHalf) const W = theRest.subarray(eachHalf) diff --git a/src/drand/timelock-encrypter.ts b/src/drand/timelock-encrypter.ts index 7ac1baf..4900b03 100644 --- a/src/drand/timelock-encrypter.ts +++ b/src/drand/timelock-encrypter.ts @@ -1,11 +1,9 @@ -import {PointG1, PointG2} from "@noble/bls12-381" import {sha256} from "@noble/hashes/sha256" import {Buffer} from "buffer" import * as ibe from "../crypto/ibe" import {ChainClient} from "drand-client" import {Stanza} from "../age/age-encrypt-decrypt" import {Ciphertext} from "../crypto/ibe" -import {Point} from "./index" export function createTimelockEncrypter(client: ChainClient, roundNumber: number) { if (roundNumber < 1) { @@ -14,22 +12,20 @@ export function createTimelockEncrypter(client: ChainClient, roundNumber: number return async (fileKey: Uint8Array): Promise> => { const chainInfo = await client.chain().info() + const pk = Buffer.from(chainInfo.public_key, "hex") const id = hashedRoundNumber(roundNumber) - let ciphertext: Ciphertext + let ciphertext: Ciphertext switch (chainInfo.schemeID) { case "pedersen-bls-unchained": { - const point = PointG1.fromHex(chainInfo.public_key) - ciphertext = await ibe.encryptOnG1(point, id, fileKey) + ciphertext = await ibe.encryptOnG1(pk, id, fileKey) } break; case "bls-unchained-on-g1": { - const point = PointG2.fromHex(chainInfo.public_key) - ciphertext = await ibe.encryptOnG2(point, id, fileKey) + ciphertext = await ibe.encryptOnG2(pk, id, fileKey) } break; case "bls-unchained-g1-rfc9380": { - const point = PointG2.fromHex(chainInfo.public_key) - ciphertext = await ibe.encryptOnG2RFC9380(point, id, fileKey) + ciphertext = await ibe.encryptOnG2RFC9380(pk, id, fileKey) } break; default: @@ -50,6 +46,6 @@ export function hashedRoundNumber(round: number): Uint8Array { return sha256(roundNumberBuffer) } -function serialisedCiphertext(ciphertext: Ciphertext): Uint8Array { - return Buffer.concat([ciphertext.U.toRawBytes(true), ciphertext.V, ciphertext.W]) +function serialisedCiphertext(ciphertext: Ciphertext): Uint8Array { + return Buffer.concat([ciphertext.U, ciphertext.V, ciphertext.W]) } diff --git a/src/index.ts b/src/index.ts index b0409e2..3068be8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -46,6 +46,10 @@ export async function timelockDecrypt( return await decryptAge(cipher, timelockDecrypter) } +const userAgentOpts = { + userAgent: `tlock-js-${LIB_VERSION}` +} + export function testnetClient(): HttpChainClient { const opts = { ...defaultChainOptions, @@ -55,9 +59,7 @@ export function testnetClient(): HttpChainClient { } } const chain = new HttpCachingChain(TESTNET_CHAIN_URL, opts) - return new HttpChainClient(chain, opts, { - userAgent: `tlock-js-${LIB_VERSION}` - }) + return new HttpChainClient(chain, opts, userAgentOpts) } export function mainnetClient(): HttpChainClient { @@ -69,7 +71,7 @@ export function mainnetClient(): HttpChainClient { } } const chain = new HttpCachingChain(MAINNET_CHAIN_URL, opts) - return new HttpChainClient(chain, opts) + return new HttpChainClient(chain, opts, userAgentOpts) } export function nonRFCMainnetClient(): HttpChainClient { @@ -81,7 +83,7 @@ export function nonRFCMainnetClient(): HttpChainClient { } } const chain = new HttpCachingChain(MAINNET_CHAIN_URL_NON_RFC, opts) - return new HttpChainClient(chain, opts) + return new HttpChainClient(chain, opts, userAgentOpts) } export { diff --git a/src/version.ts b/src/version.ts index ed763e8..801645e 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const LIB_VERSION = "0.7.0"; +export const LIB_VERSION = "0.9.0"; diff --git a/test/crypto/ibe.test.ts b/test/crypto/ibe.test.ts index 5149049..1ce8275 100644 --- a/test/crypto/ibe.test.ts +++ b/test/crypto/ibe.test.ts @@ -1,27 +1,28 @@ -import * as bls from "@noble/bls12-381" -import {Fp, Fp2, Fp12} from "@noble/bls12-381" import {expect} from "chai" import {fp12ToBytes, fp2ToBytes, fpToBytes} from "../../src/crypto/utils" import {gtToHash} from "../../src/crypto/ibe" +import {fp12FromBigTwelve, fp2FromBigTuple} from "../../src/crypto/fp" +import {bls12_381} from "@noble/curves/bls12-381" + describe("fpToBytes", () => { it("should reverse the order of values in the Fps", () => { - const combined = Buffer.concat([fpToBytes(new Fp(1n)), fpToBytes(new Fp(2n))]) - const fp2 = fp2ToBytes(Fp2.fromBigTuple([2n, 1n])) + const combined = Buffer.concat([fpToBytes(1n), fpToBytes(2n)]) + const fp2 = fp2ToBytes(fp2FromBigTuple([2n, 1n])) expect(Buffer.compare(fp2, combined)).to.equal(0) }) it("Fp2s should be reversed when combined to an Fp12", () => { - const one = fp2ToBytes(Fp2.fromBigTuple([1n, 2n])) - const two = fp2ToBytes(Fp2.fromBigTuple([3n, 4n])) - const three = fp2ToBytes(Fp2.fromBigTuple([5n, 6n])) - const four = fp2ToBytes(Fp2.fromBigTuple([7n, 8n])) - const five = fp2ToBytes(Fp2.fromBigTuple([9n, 10n])) - const six = fp2ToBytes(Fp2.fromBigTuple([11n, 12n])) + const one = fp2ToBytes(fp2FromBigTuple([1n, 2n])) + const two = fp2ToBytes(fp2FromBigTuple([3n, 4n])) + const three = fp2ToBytes(fp2FromBigTuple([5n, 6n])) + const four = fp2ToBytes(fp2FromBigTuple([7n, 8n])) + const five = fp2ToBytes(fp2FromBigTuple([9n, 10n])) + const six = fp2ToBytes(fp2FromBigTuple([11n, 12n])) const combined = Buffer.concat([one, two, three, four, five, six]) - const fullFp12 = fp12ToBytes(Fp12.fromBigTwelve([11n, 12n, 9n, 10n, 7n, 8n, 5n, 6n, 3n, 4n, 1n, 2n])) + const fullFp12 = fp12ToBytes(fp12FromBigTwelve([11n, 12n, 9n, 10n, 7n, 8n, 5n, 6n, 3n, 4n, 1n, 2n])) expect(Buffer.compare(fullFp12, combined)).to.equal(0) }) @@ -30,14 +31,14 @@ describe("fpToBytes", () => { // a number with more than 64bits const expectedHexValue = "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffff" // the same value as a `bigint` - const bytes = fpToBytes(new Fp(1099511627775n)) + const bytes = fpToBytes(1099511627775n) expect(Buffer.from(bytes).toString("hex")).to.equal(expectedHexValue) }) it("correctly pass the test vectors generated from the go codebase", () => { // note these test vectors are the reverse order of the go ones - const test = Fp12.fromBigTwelve([ + const test = fp12FromBigTwelve([ BigInt("0x1250ebd871fc0a92a7b2d83168d0d727272d441befa15c503dd8e90ce98db3e7b6d194f60839c508a84305aaca1789b6"), BigInt("0x089a1c5b46e5110b86750ec6a532348868a84045483c92b7af5af689452eafabf1a8943e50439f1d59882a98eaa0170f"), BigInt("0x1368bb445c7c2d209703f239689ce34c0378a68e72a6b3b216da0e22a5031b54ddff57309396b38c881c4c849ec23e87"), @@ -56,23 +57,25 @@ describe("fpToBytes", () => { const toBytesOutput = fp12ToBytes(test) expect(Buffer.from(toBytesOutput).toString("hex")).to.equal(expectedOutput) - expect(bls.pairing(bls.PointG1.BASE, bls.PointG2.BASE)).to.deep.equal(test) - expect(fp12ToBytes(bls.pairing(bls.PointG1.BASE, bls.PointG2.BASE))).to.deep.equal(fp12ToBytes(test)) + + expect(bls12_381.pairing(bls12_381.G1.ProjectivePoint.BASE, bls12_381.G2.ProjectivePoint.BASE)).to.deep.equal(test) + expect(fp12ToBytes(bls12_381.pairing(bls12_381.G1.ProjectivePoint.BASE, bls12_381.G2.ProjectivePoint.BASE))).to.deep.equal(fp12ToBytes(test)) // the drand kyber actually multiplies when using `.add()` const expectedAdded = "079ab7b345eb23c944c957a36a6b74c37537163d4cbf73bad9751de1dd9c68ef72cb21447e259880f72a871c3eda1b0c017f1c95cf79b22b459599ea57e613e00cb75e35de1f837814a93b443c54241015ac9761f8fb20a44512ff5cfc04ac7f0f6b8b52b2b5d0661cbf232820a257b8c5594309c01c2a45e64c6a7142301e4fb36e6e16b5a85bd2e437599d103c3ace06d8046c6b3424c4cd2d72ce98d279f2290a28a87e8664cb0040580d0c485f34df45267f8c215dcbcd862787ab555c7e113286dee21c9c63a458898beb35914dc8daaac453441e7114b21af7b5f47d559879d477cf2a9cbd5b40c86becd071280900410bb2751d0a6af0fe175dcf9d864ecaac463c6218745b543f9e06289922434ee446030923a3e4c4473b4e3b1914081abd33a78d31eb8d4c1bb3baab0529bb7baf1103d848b4cead1a8e0aa7a7b260fbe79c67dbe41ca4d65ba8a54a72b61692a61ce5f4d7a093b2c46aa4bca6c4a66cf873d405ebc9c35d8aa639763720177b23beffaf522d5e41d3c5310ea3331409cebef9ef393aa00f2ac64673675521e8fc8fddaf90976e607e62a740ac59c3dddf95a6de4fba15beb30c43d4e3f803a3734dbeb064bf4bc4a03f945a4921e49d04ab8d45fd753a28b8fa082616b4b17bbcb685e455ff3bf8f60c3bd32a0c185ef728cf41a1b7b700b7e445f0b372bc29e370bc227d443c70ae9dbcf73fee8acedbd317a286a53266562d817269c004fb0f149dd925d2c590a960936763e519c2b62e14c7759f96672cd852194325904197b0b19c6b528ab33566946af39b" - expect(Buffer.from(fp12ToBytes(test.multiply(test))).toString("hex")).to.equal(expectedAdded) + + expect(Buffer.from(fp12ToBytes(bls12_381.fields.Fp12.mul(test, test))).toString("hex")).to.equal(expectedAdded) const expectedGtToHash = "cb87319f24560b5231579a09ad79f12e" expect(Buffer.from(gtToHash(test, 16)).toString("hex")).to.equal(expectedGtToHash) const expectedG1Compressed = "97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb" - const actualG1Compressed = Buffer.from(bls.PointG1.BASE.toRawBytes(true)).toString("hex") + const actualG1Compressed = Buffer.from(bls12_381.G1.ProjectivePoint.BASE.toRawBytes(true)).toString("hex") expect(actualG1Compressed).to.equal(expectedG1Compressed) // compressed g2 not yet implemented in noble const expectedG2Uncompressed = "13e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80606c4a02ea734cc32acd2b02bc28b99cb3e287e85a763af267492ab572e99ab3f370d275cec1da1aaa9075ff05f79be0ce5d527727d6e118cc9cdc6da2e351aadfd9baa8cbdd3a76d429a695160d12c923ac9cc3baca289e193548608b82801" - const actualG2Uncompressed = Buffer.from(bls.PointG2.BASE.toRawBytes(false)).toString("hex") + const actualG2Uncompressed = Buffer.from(bls12_381.G2.ProjectivePoint.BASE.toRawBytes(false)).toString("hex") expect(actualG2Uncompressed).to.equal(expectedG2Uncompressed) }) }) diff --git a/test/crypto/point.test.ts b/test/crypto/point.test.ts new file mode 100644 index 0000000..0f99f64 --- /dev/null +++ b/test/crypto/point.test.ts @@ -0,0 +1,12 @@ +import {bls12_381} from "@noble/curves/bls12-381" +import {hashedRoundNumber} from "../../src/drand/timelock-encrypter" + +describe("beacon", () => { + it("verifies for the new bls lib", () => { + const pubKey = "8200fc249deb0148eb918d6e213980c5d01acd7fc251900d9260136da3b54836ce125172399ddc69c4e3e11429b62c11" + const round = 19369534 + const sig = bls12_381.G2.ProjectivePoint.fromHex("a33833d2098f5e0c4df334fb6c5b1c2de3ab293c77825f55d816254dabf7f4f3d429b6207e1cd2a808876e06058a1f8102bb6f6927b654b391259ea99c3566a4eb55feb9665dbaf9d33af08a10b1d8d8b35d91fd3536eb4c197be0041beb5dc2") + const msg = hashedRoundNumber(round) + expect(bls12_381.verify(sig, msg, pubKey)).toBeTruthy() + }) +}) diff --git a/test/drand/integration.test.ts b/test/drand/integration.test.ts index 089b18d..0331a68 100644 --- a/test/drand/integration.test.ts +++ b/test/drand/integration.test.ts @@ -4,16 +4,26 @@ import {HttpCachingChain, HttpChainClient} from "drand-client" describe("integration", () => { it("should be able to encrypt and decrypt with the testnet client", async () => { + const client = testnetClient() const message = "hello world and other things" - const ciphertext = await timelockEncrypt(1, Buffer.from(message), testnetClient()) - const plaintext = await timelockDecrypt(ciphertext, testnetClient()) + const ciphertext = await timelockEncrypt(1, Buffer.from(message), client) + const plaintext = await timelockDecrypt(ciphertext, client) expect(plaintext.toString("utf8")).toEqual(message) }) it("should be able to encrypt and decrypt with g1/g2 swapped client", async () => { + const client = mainnetClient() const message = "hello world and other things" - const ciphertext = await timelockEncrypt(1, Buffer.from(message), mainnetClient()) - const plaintext = await timelockDecrypt(ciphertext, mainnetClient()) + const ciphertext = await timelockEncrypt(1, Buffer.from(message), client) + const plaintext = await timelockDecrypt(ciphertext, client) + + expect(plaintext.toString("utf8")).toEqual(message) + }) + it("should be able to encrypt and decrypt with g1/g2 swapped non-rfc client", async () => { + const client = nonRFCMainnetClient() + const message = "hello world and other things" + const ciphertext = await timelockEncrypt(1, Buffer.from(message), client) + const plaintext = await timelockDecrypt(ciphertext, client) expect(plaintext.toString("utf8")).toEqual(message) })