Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for RFC9380 DST G1 scheme #39

Merged
merged 7 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,201 changes: 1,003 additions & 1,198 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tlock-js",
"version": "0.6.2",
"version": "0.7.0",
"description": "A library to encrypt data that can only be decrypted in the future using drand",
"source": "src/index.ts",
"main": "index.js",
Expand All @@ -24,29 +24,29 @@
"author": "drand.love",
"license": "(Apache-2.0 OR MIT)",
"devDependencies": {
"@types/chai": "^4.3.1",
"@types/chai": "^4.3.5",
"@types/chai-string": "^1.4.2",
"@types/jest": "^29.2.6",
"@types/node": "^18.0.4",
"@types/jest": "^29.5.3",
"@types/node": "^20.4.2",
"@types/yup": "^0.29.14",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@typescript-eslint/eslint-plugin": "^6.1.0",
"@typescript-eslint/parser": "^6.1.0",
"chai": "^4.3.7",
"chai-string": "^1.5.0",
"eslint": "^8.19.0",
"eslint": "^8.45.0",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.3.1",
"jest": "^29.6.1",
"jest-fetch-mock": "^3.0.3",
"ts-jest": "^29.0.5",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.7.4"
"typescript": "^5.1.6"
},
"dependencies": {
"@noble/bls12-381": "^1.4.0",
"@noble/hashes": "^1.1.2",
"@noble/hashes": "^1.3.1",
"@stablelib/chacha20poly1305": "^1.0.1",
"buffer": "^6.0.3",
"drand-client": "1.1.0"
"drand-client": "1.2.1"
},
"browserslist": [
"> 0.5%",
Expand Down
149 changes: 76 additions & 73 deletions src/crypto/ibe.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import * as bls from "@noble/bls12-381"
import {PointG1, PointG2, utils} from "@noble/bls12-381"
import {Fp12, PointG1, PointG2, utils} from "@noble/bls12-381"
import {sha256} from "@noble/hashes/sha256"
import { Buffer } from "buffer"
import {bytesToNumberBE, fp12ToBytes, xor} from "./utils"

export interface Ciphertext {
U: PointG1
export interface Ciphertext<T> {
U: T
V: Uint8Array
W: Uint8Array
}

export interface CiphertextOnG2 {
U: PointG2,
V: Uint8Array,
W: Uint8Array

interface Mul<T> {
multiply(scalar: bigint): T;
}

export async function encryptOnG1(master: PointG1, ID: Uint8Array, msg: Uint8Array): Promise<Ciphertext> {
async function encrypt<T, U>(
master: T,
ID: Uint8Array,
msg: Uint8Array,
base: Mul<T>,
hashToCurve: (id: Uint8Array) => Promise<U>,
pairing: (m: T, q: U) => Fp12
): Promise<Ciphertext<T>> {

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 bls.PointG2.hashToCurve(ID)
const Gid = bls.pairing(master, Qid)
const Qid = await hashToCurve(ID)
const Gid = pairing(master, Qid)

// 2. Derive random sigma
const sigma = utils.randomBytes(msg.length)

// 3. Derive r from sigma and msg and get a field element
const r = h3(sigma, msg)
const U = bls.PointG1.BASE.multiply(r)

const U = base.multiply(r)
// 5. Compute V = sigma XOR H2(rGid)
const rGid = Gid.pow(r)
const hrGid = await gtToHash(rGid, msg.length)
Expand All @@ -50,91 +56,88 @@ export async function encryptOnG1(master: PointG1, ID: Uint8Array, msg: Uint8Arr
}
}

export async function encryptOnG2(master: PointG2, ID: Uint8Array, msg: Uint8Array): Promise<CiphertextOnG2> {
if (msg.length >> 8 > 1) {
throw new Error("cannot encrypt messages larger than our hash output: 256 bits.")
}


// 1. Compute Gid = e(Q_id, master)
const Qid = await bls.PointG1.hashToCurve(ID)
const Gid = bls.pairing(Qid, master)

// 2. Derive random sigma
const sigma = utils.randomBytes(msg.length)

// 3. Derive r from sigma and msg and get a field element
const r = h3(sigma, msg)
const U = bls.PointG2.BASE.multiply(r)

// 5. Compute V = sigma XOR H2(rGid)
const rGid = Gid.pow(r)
const hrGid = await gtToHash(rGid, msg.length)

const V = xor(sigma, hrGid)
export async function encryptOnG1(master: PointG1, ID: Uint8Array, msg: Uint8Array): Promise<Ciphertext<PointG1>> {
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)
)
}

// 6. Compute M XOR H(sigma)
const hsigma = h4(sigma, msg.length)
// uses the DST for G2 erroneously
export async function encryptOnG2(master: PointG2, ID: Uint8Array, msg: Uint8Array): Promise<Ciphertext<PointG2>> {
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)
)
}

const W = xor(msg, hsigma)
export async function encryptOnG2RFC9380(master: PointG2, ID: Uint8Array, msg: Uint8Array): Promise<Ciphertext<PointG2>> {
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)
)
}

return {
U: U,
V: V,
W: W,
}
interface Eq<T> {
equals(other: T): boolean
}

export async function decryptOnG1(p: PointG2, c: Ciphertext): Promise<Uint8Array> {
async function decrypt<T extends Eq<T>, U>(
point: U,
ciphertext: Ciphertext<T>,
base: Mul<T>,
pairing: (m: T, q: U) => Fp12
): Promise<Uint8Array> {
// 1. Compute sigma = V XOR H2(e(rP,private))
const gidt = bls.pairing(c.U, p)
const hgidt = gtToHash(gidt, c.W.length)
const gidt = pairing(ciphertext.U, point)
const hgidt = gtToHash(gidt, ciphertext.W.length)

if (hgidt.length != c.V.length) {
if (hgidt.length != ciphertext.V.length) {
throw new Error("XorSigma is of invalid length")
}
const sigma = xor(hgidt, c.V)
const sigma = xor(hgidt, ciphertext.V)

// 2. Compute M = W XOR H4(sigma)
const hsigma = h4(sigma, c.W.length)
const hsigma = h4(sigma, ciphertext.W.length)

const msg = xor(hsigma, c.W)
const msg = xor(hsigma, ciphertext.W)

// 3. Check U = rP
const r = h3(sigma, msg)
const rP = bls.PointG1.BASE.multiply(r)
const rP = base.multiply(r)

if (!rP.equals(c.U)) {
if (!rP.equals(ciphertext.U)) {
throw new Error("invalid proof: rP check failed")
}

return msg
}

export async function decryptOnG2(p: PointG1, c: CiphertextOnG2): Promise<Uint8Array> {
// 1. Compute sigma = V XOR H2(e(private, rP))
const gidt = bls.pairing(p, c.U)
const hgidt = gtToHash(gidt, c.W.length)

if (hgidt.length != c.V.length) {
throw new Error("XorSigma is of invalid length")
}
const sigma = xor(hgidt, c.V)

// 2. Compute M = W XOR H4(sigma)
const hsigma = h4(sigma, c.W.length)

const msg = xor(hsigma, c.W)

// 3. Check U = rP
const r = h3(sigma, msg)
const rP = bls.PointG2.BASE.multiply(r)
export async function decryptOnG1(point: PointG2, ciphertext: Ciphertext<PointG1>): Promise<Uint8Array> {
return decrypt(
point,
ciphertext,
PointG1.BASE,
(m: PointG1, Qid: PointG2) => bls.pairing(m, Qid)
)
}

if (!rP.equals(c.U)) {
throw new Error("invalid proof: rP check failed")
}
export async function decryptOnG2(point: PointG1, ciphertext: Ciphertext<PointG2>) {
return decrypt<PointG2, PointG1>(
point,
ciphertext,
PointG2.BASE,
(p: PointG2, q: PointG1) => bls.pairing(q, p)
)
}

return msg
export async function decryptOnG2RFC9380(point: PointG1, ciphertext: Ciphertext<PointG2>) {
return decrypt<PointG2, PointG1>(
point,
ciphertext,
PointG2.BASE,
(p: PointG2, q: PointG1) => bls.pairing(q, p)
)
}

export function gtToHash(gt: bls.Fp12, len: number): Uint8Array {
Expand Down
17 changes: 15 additions & 2 deletions src/drand/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
// config for the testnet chain info
import {ChainInfo} from "drand-client"

export const MAINNET_CHAIN_URL = "https://api.drand.sh/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493"
export const MAINNET_CHAIN_INFO: ChainInfo = {
export const MAINNET_CHAIN_URL = "https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"

export const MAINNET_CHAIN_INFO = {
public_key: "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a",
period: 3,
genesis_time: 1692803367,
hash: "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971",
groupHash: "f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e",
schemeID: "bls-unchained-g1-rfc9380",
metadata: {
beaconID: "quicknet"
}
}
export const MAINNET_CHAIN_URL_NON_RFC = "https://api.drand.sh/dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493"
export const MAINNET_CHAIN_INFO_NON_RFC: ChainInfo = {
hash: "dbd506d6ef76e5f386f41c651dcb808c5bcbd75471cc4eafa3f4df7ad4e4c493",
public_key: "a0b862a7527fee3a731bcb59280ab6abd62d5c0b6ea03dc4ddf6612fdfc9d01f01c31542541771903475eb1ec6615f8d0df0b8b6dce385811d6dcf8cbefb8759e5e616a3dfd054c928940766d9a5b9db91e3b697e5d70a975181e007f87fca5e",
period: 3,
Expand Down
3 changes: 3 additions & 0 deletions src/drand/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface Point {
toRawBytes(isCompressed?: boolean): Uint8Array
}
37 changes: 15 additions & 22 deletions src/drand/timelock-decrypter.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Stanza} from "../age/age-encrypt-decrypt"
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, CiphertextOnG2} from "../crypto/ibe"
import {Ciphertext} from "../crypto/ibe"
import {Point} from "./index"

export function createTimelockDecrypter(network: ChainClient) {
return async (recipients: Array<Stanza>): Promise<Uint8Array> => {
Expand Down Expand Up @@ -35,14 +36,19 @@ export function createTimelockDecrypter(network: ChainClient) {
switch (chainInfo.schemeID) {
case "pedersen-bls-unchained": {
const g2 = PointG2.fromHex(beacon.signature)
const ciphertext = parseCiphertext(body)
const ciphertext = parseCiphertext(body, PointG1.BASE, PointG1.fromHex)
return await ibe.decryptOnG1(g2, ciphertext)
}
case "bls-unchained-on-g1": {
const g1 = PointG1.fromHex(beacon.signature)
const cipherText = parseCiphertextOnG2(body)
const cipherText = parseCiphertext(body, PointG2.BASE, PointG2.fromHex)
return ibe.decryptOnG2(g1, 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)
}
default:
throw Error(`Unsupported scheme: ${chainInfo.schemeID} - you must use a drand network with an unchained scheme for timelock decryption!`)
}
Expand All @@ -60,26 +66,13 @@ export function createTimelockDecrypter(network: ChainClient) {
return roundNumberParsed
}

function parseCiphertext(body: Uint8Array): Ciphertext {
const g1Length = PointG1.BASE.toRawBytes(true).byteLength
const g1Bytes = body.subarray(0, g1Length)
const theRest = body.subarray(g1Length)
const eachHalf = theRest.length / 2

const U = PointG1.fromHex(Buffer.from(g1Bytes).toString("hex"))
const V = theRest.subarray(0, eachHalf)
const W = theRest.subarray(eachHalf)

return {U, V, W}
}

function parseCiphertextOnG2(body: Uint8Array): CiphertextOnG2 {
const g1Length = PointG2.BASE.toRawBytes(true).byteLength
const g1Bytes = body.subarray(0, g1Length)
const theRest = body.subarray(g1Length)
function parseCiphertext<T>(body: Uint8Array, base: Point, fromHex: (buf: Buffer) => T): Ciphertext<T> {
const pointLength = base.toRawBytes(true).byteLength
const pointBytes = body.subarray(0, pointLength)
const theRest = body.subarray(pointLength)
const eachHalf = theRest.length / 2

const U = PointG2.fromHex(Buffer.from(g1Bytes).toString("hex"))
const U = fromHex(Buffer.from(pointBytes))
const V = theRest.subarray(0, eachHalf)
const W = theRest.subarray(eachHalf)

Expand Down
12 changes: 9 additions & 3 deletions src/drand/timelock-encrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {Buffer} from "buffer"
import * as ibe from "../crypto/ibe"
import {ChainClient} from "drand-client"
import {Stanza} from "../age/age-encrypt-decrypt"
import {Ciphertext, CiphertextOnG2} from "../crypto/ibe"
import {Ciphertext} from "../crypto/ibe"
import {Point} from "./index"

export function createTimelockEncrypter(client: ChainClient, roundNumber: number) {
if (roundNumber < 1) {
Expand All @@ -14,7 +15,7 @@ export function createTimelockEncrypter(client: ChainClient, roundNumber: number
return async (fileKey: Uint8Array): Promise<Array<Stanza>> => {
const chainInfo = await client.chain().info()
const id = hashedRoundNumber(roundNumber)
let ciphertext: Ciphertext | CiphertextOnG2
let ciphertext: Ciphertext<Point>
switch (chainInfo.schemeID) {
case "pedersen-bls-unchained": {
const point = PointG1.fromHex(chainInfo.public_key)
Expand All @@ -26,6 +27,11 @@ export function createTimelockEncrypter(client: ChainClient, roundNumber: number
ciphertext = await ibe.encryptOnG2(point, id, fileKey)
}
break;
case "bls-unchained-g1-rfc9380": {
const point = PointG2.fromHex(chainInfo.public_key)
ciphertext = await ibe.encryptOnG2RFC9380(point, id, fileKey)
}
break;
default:
throw Error(`Unsupported scheme: ${chainInfo.schemeID} - you must use a drand network with an unchained scheme for timelock encryption!`)
}
Expand All @@ -44,6 +50,6 @@ export function hashedRoundNumber(round: number): Uint8Array {
return sha256(roundNumberBuffer)
}

function serialisedCiphertext(ciphertext: Ciphertext | CiphertextOnG2): Uint8Array {
function serialisedCiphertext(ciphertext: Ciphertext<Point>): Uint8Array {
return Buffer.concat([ciphertext.U.toRawBytes(true), ciphertext.V, ciphertext.W])
}
Loading