From 25c2d48c452a1cbec54019c430e78b2b43c065ea Mon Sep 17 00:00:00 2001
From: driemworks
Date: Tue, 17 Dec 2024 12:01:05 -0600
Subject: [PATCH 1/6] feat: add multi-curve support to timelock.js
---
Cargo.lock | 3 +-
docs/tlock.md | 86 +++++
.../web}/react-tlock-demo/.gitignore | 0
.../web}/react-tlock-demo/README.md | 0
.../web}/react-tlock-demo/config-overrides.js | 0
.../web}/react-tlock-demo/package-lock.json | 38 +-
.../web}/react-tlock-demo/package.json | 4 +-
.../web}/react-tlock-demo/public/favicon.ico | Bin
.../web}/react-tlock-demo/public/index.html | 0
.../web}/react-tlock-demo/public/logo192.png | Bin
.../web}/react-tlock-demo/public/logo512.png | Bin
.../react-tlock-demo/public/manifest.json | 0
.../web}/react-tlock-demo/public/robots.txt | 0
.../web}/react-tlock-demo/src/App.css | 0
.../web}/react-tlock-demo/src/App.js | 66 +++-
.../web}/react-tlock-demo/src/index.css | 0
.../web}/react-tlock-demo/src/index.js | 0
.../web}/react-tlock-demo/src/logo.svg | 0
.../react-tlock-demo/src/reportWebVitals.d.ts | 0
.../react-tlock-demo/src/reportWebVitals.js | 0
.../web}/react-tlock-demo/src/setupTests.js | 0
py/src/timelock.egg-info/PKG-INFO | 81 ++++
py/src/timelock.egg-info/SOURCES.txt | 10 +
py/src/timelock.egg-info/dependency_links.txt | 1 +
py/src/timelock.egg-info/requires.txt | 1 +
py/src/timelock.egg-info/top_level.txt | 1 +
rustfmt.toml | 2 +-
ts/README.md | 60 +--
.../react-tlock-demo/config-overrides.d.ts | 2 -
ts/examples/react-tlock-demo/src/App.d.ts | 3 -
ts/examples/react-tlock-demo/src/index.d.ts | 1 -
.../react-tlock-demo/src/setupTests.d.ts | 1 -
ts/package-lock.json | 13 +-
ts/package.json | 4 +-
ts/src/index.ts | 3 +-
ts/src/interfaces/DrandIdentityBuilder.ts | 67 ++++
ts/src/interfaces/IDNIdentityBuilder.ts | 2 +-
ts/src/interfaces/IIdentityBuilder.ts | 2 +-
ts/src/timelock.test.spec.ts | 42 ++-
ts/src/timelock.ts | 120 ++++--
wasm/Cargo.toml | 4 +-
wasm/src/js.rs | 347 ++++++++++--------
42 files changed, 693 insertions(+), 271 deletions(-)
create mode 100644 docs/tlock.md
rename {ts/examples => examples/web}/react-tlock-demo/.gitignore (100%)
rename {ts/examples => examples/web}/react-tlock-demo/README.md (100%)
rename {ts/examples => examples/web}/react-tlock-demo/config-overrides.js (100%)
rename {ts/examples => examples/web}/react-tlock-demo/package-lock.json (99%)
rename {ts/examples => examples/web}/react-tlock-demo/package.json (91%)
rename {ts/examples => examples/web}/react-tlock-demo/public/favicon.ico (100%)
rename {ts/examples => examples/web}/react-tlock-demo/public/index.html (100%)
rename {ts/examples => examples/web}/react-tlock-demo/public/logo192.png (100%)
rename {ts/examples => examples/web}/react-tlock-demo/public/logo512.png (100%)
rename {ts/examples => examples/web}/react-tlock-demo/public/manifest.json (100%)
rename {ts/examples => examples/web}/react-tlock-demo/public/robots.txt (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/App.css (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/App.js (51%)
rename {ts/examples => examples/web}/react-tlock-demo/src/index.css (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/index.js (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/logo.svg (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/reportWebVitals.d.ts (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/reportWebVitals.js (100%)
rename {ts/examples => examples/web}/react-tlock-demo/src/setupTests.js (100%)
create mode 100644 py/src/timelock.egg-info/PKG-INFO
create mode 100644 py/src/timelock.egg-info/SOURCES.txt
create mode 100644 py/src/timelock.egg-info/dependency_links.txt
create mode 100644 py/src/timelock.egg-info/requires.txt
create mode 100644 py/src/timelock.egg-info/top_level.txt
delete mode 100644 ts/examples/react-tlock-demo/config-overrides.d.ts
delete mode 100644 ts/examples/react-tlock-demo/src/App.d.ts
delete mode 100644 ts/examples/react-tlock-demo/src/index.d.ts
delete mode 100644 ts/examples/react-tlock-demo/src/setupTests.d.ts
create mode 100644 ts/src/interfaces/DrandIdentityBuilder.ts
diff --git a/Cargo.lock b/Cargo.lock
index 1d5e9ef..b6d5153 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3696,14 +3696,13 @@ dependencies = [
[[package]]
name = "timelock_wasm_wrapper"
-version = "0.0.1"
+version = "0.1.0"
dependencies = [
"ark-bls12-381",
"ark-ec",
"ark-serialize",
"ark-std",
"getrandom",
- "libc",
"parity-scale-codec",
"pyo3",
"rand_chacha",
diff --git a/docs/tlock.md b/docs/tlock.md
new file mode 100644
index 0000000..2a03f65
--- /dev/null
+++ b/docs/tlock.md
@@ -0,0 +1,86 @@
+# Timelock Encryption
+
+Our timelock encryption scheme is a hybrid cryptosystem using both AES-GCM and FullIdent (Identity based encryption). The goal is to be able to encrypt any-length messages for future rounds of the ETF post finality gadget.
+
+## Background
+
+### AES-GCM
+AES-GCM is a symmetric stream cipher, meaning you need to use the same key and nonce to encrypt and decrypt messages.
+
+1. $ct \leftarrow AES.Enc(message, key, nonce)$
+2. $m \leftarrow AES.Dec(ct, key, nonce)$
+
+
+### BF-IBE
+
+
+Identity based encryption is a scheme were a message can be encrypted for an arbitrary string, rather than some specific public key. For example, a message could be encrypted for "bob@encryptme.com" so that only the owner of the identity "bob@encryptme.com" is able to decrypt the message. Our construction uses the BF-IBE "FullIdent" scheme, which is IND-ID-CCA secure.
+
+The scheme is instantiated with a private input of a master secret key and public input as the output of a bilinear Diffie-Hellman parameter generator, which is PPT algorithm that outputs a prime number $q$, the description of two groups $G_1$, $G_2$ of order $q$, and the description of an admissible bilinear map $\hat{e} : G_1 \times G_1 \to G_2$. In our case, we will instead us a bilinear map $e: G_1 \times G_2 \to G_2$ (a type III pairing).
+
+It consists of four PPT algorithms (Setup, Extract, Encrypt, Decrypt) defined as:
+
+- $(pp, s) \leftarrow Setup(1^\lambda)$ where $\lambda$ is the security parameter, $pp$ is the output (system) params and $s$ is the IBE master secret key. The system params are a generator $G \in \mathbb{G}_1$ and commitment to the master key, $P_{pub} = sG$.
+
+- $sk_{ID} \leftarrow Extract(mk, ID)$ outputs the private key for an $ID \in \{0, 1\}^*$.
+
+- $Encrypt(pp, ID, m) \to ct$ outputs the ciphertext $ct$ for any message $m \in \{0, 1\}^*$.
+
+- $Decrypt(sk_{ID}, ct) \to m$ outputs the decrypted message $m$
+
+We use the BF-IBE "FullIdent" scheme to encrypt messages such that their decryption key is broadcast as the output of at specific future time step of the computational reference clock. FullIdent is IND-ID-CCA secure. In FullIdent, public parameters are stored in $\mathbb{G}_1$, and the scheme uses type 1 pairings. We will instead use type 3 pairings, so our public parameters are in $\mathbb{G}_2$ instead.
+
+$\mathbf{Setup}$
+
+Let $e: \mathbb{G}_1 \times \mathbb{G}_2 \to \mathbb{G}_2$ be a bilinear map, $H_1: \{0, 1\}^* \to \mathbb{G}_1$ a hash-to-G1 function, $H_2: \mathbb{G}_2 \to \{0, 1\}^n$ for some $n$, $H_3: \{0, 1\}^n \times \{0, 1\}^n \to \mathbb{Z}_q$, and a cryptographic hash function $H_4: \{0, 1\}^n \to \{0, 1\}^n$. Choose a random $s \xleftarrow{R} \mathbb{Z}_p$ and a generator $P \xleftarrow{R} \mathbb{G}_1$. Then, broadcast the value $P_{pub} = sP$.
+
+$\mathbf{Extract}$
+
+Compute the IBE secret for an identity $ID$ with $d_{ID} = sQ_{ID}$ where $Q_{ID} = H_1(ID)$
+
+$\mathbf{Encryption}$
+
+Let $M \in \{0, 1\}^n$ be the message and $t > 0$ be some future time slot in the CRC $\mathcal{C}$ for which we want to encrypt a message and assume it has a unique id, $ID_t$.
+
+- Compute $Q_{ID_t} = H_1(ID_t) \in \mathbb{G}_1$
+- Choose a random $\sigma \in \{0, 1\}^n$
+- set $r = H_4(\sigma, M)$
+ - Calculate the ciphertext
+ $C = \left = \left< rP, \sigma \oplus H_2(g^r_{ID}), M \oplus H_4(\sigma) \right>$
+
+
+where $g_{ID} = e(Q_{ID}, P_{pub}) \in \mathbb{G}_2$
+
+$\mathbf{Decryption}$
+A benefit of the FullIdent scheme is that the decryption algorithm allows for verification that the ciphertext was properly encrypted, so it's not possible to attempt to decrypt data that isn't yours.
+
+For a ciphertext $C = \left $ encrypted using the time slot $t$. Then $C$ can be decrypted with the private key $d_{ID_t} = s Q_{ID_t} \in \mathbb{G}_1$, where $s$ is the IBE master secret, as such:
+
+
+- Compute $V \oplus H_2(e(d_{ID_t}, U)) = \sigma$
+- Compute $W \oplus H_4(\sigma) = M$
+- Set $r = H_3(\sigma, M)$. Check if $U = rP$. If not, reject the ciphertext.
+- Output $M$ as the decryption of $C$
+
+
+## Tlock
+
+### Encryption
+
+We want to encrypt a message $m \in \{0, 1\}^*$ for an identity $ID \in \{0, 1\}^*$ with some threshold $t > 0$.
+
+1. Choose $s \xleftarrow{R} \mathbb{Z}_p$ and broadcast $P_{pub} = sP$ where $P \in \mathbb{G}_2$ is a commonly agreed on generator. Also randomly sample a 96-bit nonce, $N$.
+2. Encrypt the message using AES-GCM, producing: $ct \leftarrow AES.Enc(m, s, N)$
+3. Encrypt the AES key for the identity using IBE: $ct' \leftarrow IBE.Enc(s, ID)$
+
+Then the ciphertext contains $(ct, ct')$.
+
+### Decryption
+
+Timelock decryption can occur when a threshold of signers have produced valid BLS signatures for the given identity.
+
+Given ciphertexts $(ct, ct')$, a nonce $N$ and an identity $ID$, decryption is as follows:
+1. Collect at least a thresold of BLS sigantures and DLEQ proofs. Interpolate the signatures and aggregate the proofs to get $(\sigma = interpolate(\{\sigma_i\}_{i \in [n]}), \pi = \sum_i \pi_i)$ and verify the proof. If it is invalid, then do not proceed.
+2. The signature $\sigma$ is the IBE secret associated with the public key $P_{pub}$. Then use the secret to decrypt the ciphertext $ct'$ to recover the AES key: $k \leftarrow IBE.Decrypt(P_{pub}, ct', \sigma)$. If decryption fails, then we stop.
+3. Use the recovered $k$ to attempt to decrypt the ciphertext $ct$: $m \leftarrow AES.Decrypt(ct, N, k)$.
+
diff --git a/ts/examples/react-tlock-demo/.gitignore b/examples/web/react-tlock-demo/.gitignore
similarity index 100%
rename from ts/examples/react-tlock-demo/.gitignore
rename to examples/web/react-tlock-demo/.gitignore
diff --git a/ts/examples/react-tlock-demo/README.md b/examples/web/react-tlock-demo/README.md
similarity index 100%
rename from ts/examples/react-tlock-demo/README.md
rename to examples/web/react-tlock-demo/README.md
diff --git a/ts/examples/react-tlock-demo/config-overrides.js b/examples/web/react-tlock-demo/config-overrides.js
similarity index 100%
rename from ts/examples/react-tlock-demo/config-overrides.js
rename to examples/web/react-tlock-demo/config-overrides.js
diff --git a/ts/examples/react-tlock-demo/package-lock.json b/examples/web/react-tlock-demo/package-lock.json
similarity index 99%
rename from ts/examples/react-tlock-demo/package-lock.json
rename to examples/web/react-tlock-demo/package-lock.json
index cb250de..50be8c9 100644
--- a/ts/examples/react-tlock-demo/package-lock.json
+++ b/examples/web/react-tlock-demo/package-lock.json
@@ -8,16 +8,18 @@
"name": "react-tlock-demo",
"version": "0.1.0",
"dependencies": {
- "@driemworks/timelock.js": "^1.0.0-dev",
+ "@ideallabs/timelock.js": "^1.0.0-dev",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "buffer": "^6.0.3",
"crypto-browserify": "^3.12.1",
"js-crypto-hkdf": "^1.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"stream-browserify": "^3.0.0",
+ "uint64be": "^3.0.0",
"vm-browserify": "^1.1.2",
"web-vitals": "^2.1.4"
},
@@ -2236,19 +2238,12 @@
"postcss-selector-parser": "^6.0.10"
}
},
- "node_modules/@driemworks/timelock.js": {
- "version": "1.0.0-dev",
- "resolved": "https://registry.npmjs.org/@driemworks/timelock.js/-/timelock.js-1.0.0-dev.tgz",
- "integrity": "sha512-yjwK55UPybSC6qfA3uqQKgHLWnImGSfz1Yzapl1e+Of2ow6bBjePdzkO93Xsy0DCSaXKgrPLpjB50Sx+xdRZQw==",
- "dependencies": {
- "timelock-wasm-wrapper": "file:../wasm/pkg/"
- }
+ "node_modules/@driemworks/wasm/pkg": {
+ "extraneous": true
},
- "node_modules/@driemworks/timelock.js/node_modules/timelock-wasm-wrapper": {
- "resolved": "node_modules/@driemworks/wasm/pkg",
- "link": true
+ "node_modules/@driemworks/wasm/pkg/js": {
+ "extraneous": true
},
- "node_modules/@driemworks/wasm/pkg": {},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz",
@@ -2377,9 +2372,15 @@
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"deprecated": "Use @eslint/object-schema instead"
},
- "node_modules/@ideallabs/wasm/pkg": {
- "extraneous": true
+ "node_modules/@ideallabs/timelock.js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@ideallabs/timelock.js/-/timelock.js-1.0.0.tgz",
+ "integrity": "sha512-PVKh0SWUSyoExivuKCJlLdHcfh3LfBHenL3dfxETzwyug/FR4cLseiDKKfkaFeCyN9m2eON77XfEaIheL9RcAA==",
+ "dependencies": {
+ "timelock-wasm-wrapper": "file:../wasm/pkg/"
+ }
},
+ "node_modules/@ideallabs/wasm/pkg": {},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -15571,6 +15572,10 @@
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
},
+ "node_modules/timelock-wasm-wrapper": {
+ "resolved": "node_modules/@ideallabs/wasm/pkg",
+ "link": true
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -15824,6 +15829,11 @@
"node": ">=4.2.0"
}
},
+ "node_modules/uint64be": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-3.0.0.tgz",
+ "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg=="
+ },
"node_modules/unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
diff --git a/ts/examples/react-tlock-demo/package.json b/examples/web/react-tlock-demo/package.json
similarity index 91%
rename from ts/examples/react-tlock-demo/package.json
rename to examples/web/react-tlock-demo/package.json
index 69ba50c..e6c85c9 100644
--- a/ts/examples/react-tlock-demo/package.json
+++ b/examples/web/react-tlock-demo/package.json
@@ -3,16 +3,18 @@
"version": "0.1.0",
"private": true,
"dependencies": {
- "@driemworks/timelock.js": "^1.0.0-dev",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
+ "buffer": "^6.0.3",
"crypto-browserify": "^3.12.1",
+ "@ideallabs/timelock.js": "^1.0.0-dev",
"js-crypto-hkdf": "^1.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-scripts": "5.0.1",
"stream-browserify": "^3.0.0",
+ "uint64be": "^3.0.0",
"vm-browserify": "^1.1.2",
"web-vitals": "^2.1.4"
},
diff --git a/ts/examples/react-tlock-demo/public/favicon.ico b/examples/web/react-tlock-demo/public/favicon.ico
similarity index 100%
rename from ts/examples/react-tlock-demo/public/favicon.ico
rename to examples/web/react-tlock-demo/public/favicon.ico
diff --git a/ts/examples/react-tlock-demo/public/index.html b/examples/web/react-tlock-demo/public/index.html
similarity index 100%
rename from ts/examples/react-tlock-demo/public/index.html
rename to examples/web/react-tlock-demo/public/index.html
diff --git a/ts/examples/react-tlock-demo/public/logo192.png b/examples/web/react-tlock-demo/public/logo192.png
similarity index 100%
rename from ts/examples/react-tlock-demo/public/logo192.png
rename to examples/web/react-tlock-demo/public/logo192.png
diff --git a/ts/examples/react-tlock-demo/public/logo512.png b/examples/web/react-tlock-demo/public/logo512.png
similarity index 100%
rename from ts/examples/react-tlock-demo/public/logo512.png
rename to examples/web/react-tlock-demo/public/logo512.png
diff --git a/ts/examples/react-tlock-demo/public/manifest.json b/examples/web/react-tlock-demo/public/manifest.json
similarity index 100%
rename from ts/examples/react-tlock-demo/public/manifest.json
rename to examples/web/react-tlock-demo/public/manifest.json
diff --git a/ts/examples/react-tlock-demo/public/robots.txt b/examples/web/react-tlock-demo/public/robots.txt
similarity index 100%
rename from ts/examples/react-tlock-demo/public/robots.txt
rename to examples/web/react-tlock-demo/public/robots.txt
diff --git a/ts/examples/react-tlock-demo/src/App.css b/examples/web/react-tlock-demo/src/App.css
similarity index 100%
rename from ts/examples/react-tlock-demo/src/App.css
rename to examples/web/react-tlock-demo/src/App.css
diff --git a/ts/examples/react-tlock-demo/src/App.js b/examples/web/react-tlock-demo/src/App.js
similarity index 51%
rename from ts/examples/react-tlock-demo/src/App.js
rename to examples/web/react-tlock-demo/src/App.js
index 6a3afa9..a2a7835 100644
--- a/ts/examples/react-tlock-demo/src/App.js
+++ b/examples/web/react-tlock-demo/src/App.js
@@ -16,17 +16,23 @@
import './App.css'
import React, { useEffect, useState } from 'react'
-import { Timelock, IdealNetworkIdentityBuilder } from '@driemworks/timelock.js'
+import { Timelock, IdealNetworkIdentityBuilder, DrandIdentityBuilder, SupportedCurve } from '@ideallabs/timelock.js'
import hkdf from 'js-crypto-hkdf'
function App() {
- const [timelock, setTimelock] = useState(null)
+
+ const [timelockDrand, setTimelockDrand] = useState(null)
+ const [timelockIdeal, setTimelockIdeal] = useState(null)
useEffect(() => {
- Timelock.build().then((tlock) => {
- setTimelock(tlock)
- console.log('timelock wasm ready')
+ Timelock.build(SupportedCurve.BLS12_381).then((tlock) => {
+ setTimelockDrand(tlock)
+ })
+
+ Timelock.build(SupportedCurve.BLS12_377).then((tlock) => {
+ setTimelockIdeal(tlock)
})
+
}, [])
const fromHexString = (hexString) =>
@@ -34,7 +40,8 @@ function App() {
hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
)
- const runDemo = async () => {
+ // 83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a
+ const runDemoDrand = async () => {
// 1. Setup parameters for encryption
// use an hkdf to generate an ephemeral secret key
const seed = new TextEncoder().encode('my-secret-seed')
@@ -47,19 +54,57 @@ function App() {
// A randomness beacon public key (ex: IDN public key)
// We first get it as hex and then convert to a Uint8Array
const pkHex =
- 'b68da85d953219f84d86c5167481f505edf04ab586f28aefd238475026f5f46ba707f41bd2702f3639a4eddff8cad50041dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100'
+ '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a'
const pubkey = fromHexString(pkHex)
// A future round number of the randomness beacon
+ const roundNumber = 1000
+ // 2. Encrypt the message
+ let ct = await timelockDrand.encrypt(
+ encodedMessage,
+ roundNumber,
+ DrandIdentityBuilder,
+ pubkey,
+ esk.key
+ )
+ console.log('Timelocked ciphertext: ' + JSON.stringify(ct))
+
+ // 3. Acquire a signature for decryption from he pulse output by the beacon at the given roundNumber
+ const sigHex =
+ 'b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39'
+ const sig = fromHexString(sigHex)
+ // Decrypt the ciphertext with the signature
+ const plaintext = await timelockDrand.decrypt(ct, sig)
+ console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`)
+ }
+
+ const runDemoIdeal = async () => {
+ // 1. Setup parameters for encryption
+ // use an hkdf to generate an ephemeral secret key
+ const seed = new TextEncoder().encode('my-secret-seed')
+ const hash = 'SHA-256'
+ const length = 32
+ const esk = await hkdf.compute(seed, hash, length, '')
+ // the message to encrypt for the future
+ const message = 'Hello, Timelock!'
+ const encodedMessage = new TextEncoder().encode(message)
+ // A randomness beacon public key (ex: IDN public key)
+ // We first get it as hex and then convert to a Uint8Array
+ const pkHex =
+ 'b68da85d953219f84d86c5167481f505edf04ab586f28aefd238475026f5f46ba707f41bd2702f3639a4eddff8cad50041dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100'
+ // we only want the pubkey portion in G2, where we have [48-sig bytes][96 pk bytes]
+ const pubkey = fromHexString(pkHex).slice(48)
+ // A future round number of the randomness beacon
const roundNumber = 10
// 2. Encrypt the message
- let ct = await timelock.encrypt(
+ let ct = await timelockIdeal.encrypt(
encodedMessage,
roundNumber,
IdealNetworkIdentityBuilder,
pubkey,
esk.key
)
+
console.log('Timelocked ciphertext: ' + JSON.stringify(ct))
// 3. Acquire a signature for decryption from he pulse output by the beacon at the given roundNumber
@@ -67,7 +112,7 @@ function App() {
'e6cdf6c9d11c13e013b2c6cfd11dab46d8f1ace226ff845ffff4c7d6f64992892c54fb5d1f0f87dd300ce66f53598e01'
const sig = fromHexString(sigHex)
// Decrypt the ciphertext with the signature
- const plaintext = await timelock.decrypt(ct, sig)
+ const plaintext = await timelockIdeal.decrypt(ct, sig)
console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`)
}
@@ -78,7 +123,8 @@ function App() {
Open the developer console (F12) and then click the button below to
execute the demo.
-
+
+
)
}
diff --git a/ts/examples/react-tlock-demo/src/index.css b/examples/web/react-tlock-demo/src/index.css
similarity index 100%
rename from ts/examples/react-tlock-demo/src/index.css
rename to examples/web/react-tlock-demo/src/index.css
diff --git a/ts/examples/react-tlock-demo/src/index.js b/examples/web/react-tlock-demo/src/index.js
similarity index 100%
rename from ts/examples/react-tlock-demo/src/index.js
rename to examples/web/react-tlock-demo/src/index.js
diff --git a/ts/examples/react-tlock-demo/src/logo.svg b/examples/web/react-tlock-demo/src/logo.svg
similarity index 100%
rename from ts/examples/react-tlock-demo/src/logo.svg
rename to examples/web/react-tlock-demo/src/logo.svg
diff --git a/ts/examples/react-tlock-demo/src/reportWebVitals.d.ts b/examples/web/react-tlock-demo/src/reportWebVitals.d.ts
similarity index 100%
rename from ts/examples/react-tlock-demo/src/reportWebVitals.d.ts
rename to examples/web/react-tlock-demo/src/reportWebVitals.d.ts
diff --git a/ts/examples/react-tlock-demo/src/reportWebVitals.js b/examples/web/react-tlock-demo/src/reportWebVitals.js
similarity index 100%
rename from ts/examples/react-tlock-demo/src/reportWebVitals.js
rename to examples/web/react-tlock-demo/src/reportWebVitals.js
diff --git a/ts/examples/react-tlock-demo/src/setupTests.js b/examples/web/react-tlock-demo/src/setupTests.js
similarity index 100%
rename from ts/examples/react-tlock-demo/src/setupTests.js
rename to examples/web/react-tlock-demo/src/setupTests.js
diff --git a/py/src/timelock.egg-info/PKG-INFO b/py/src/timelock.egg-info/PKG-INFO
new file mode 100644
index 0000000..16d4c0c
--- /dev/null
+++ b/py/src/timelock.egg-info/PKG-INFO
@@ -0,0 +1,81 @@
+Metadata-Version: 2.1
+Name: timelock
+Version: 0.0.1.dev0
+Summary: This provides python bindings for usage of timelock encryption
+Author-email: Ideal Labs
+Project-URL: Homepage, https://github.com/ideal-lab5/timelock
+Project-URL: Issues, https://github.com/ideal-lab5/timelock/issues
+Classifier: Programming Language :: Python :: 3
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Requires-Python: >=3.8
+Description-Content-Type: text/markdown
+License-File: LICENSE
+Requires-Dist: timelock_wasm_wrapper>=0.0.1
+
+# Python Bindings for the Timelock Library
+
+Python bindings for the [Timelock](https://github.com/ideal-lab5/timelock) library. It enables timelock encryption and decryption with support for Drand's quicknet. In the futurue we will expand the supported networks to include other beacons as well.
+
+## Install
+
+The library can be installed [from PyPi](https://pypi.org/project/timelock/):
+
+``` sh
+pip install timelock
+```
+
+## Build
+
+Build with:
+
+```
+pip install --upgrade build
+python -m build
+```
+
+## Publish
+
+Note that this requires the timelock-wasm-wrapper python package be published as well.
+
+``` sh
+pip install --upgrade twine
+twine upload --repository testpypi dist/*
+```
+
+## Usage
+
+See the [example](../examples/python/drand_tlock.py) for an e2e demo.
+
+### Encrypt a message
+``` python
+from timelock import Timelock
+# Setup encryption input
+# The drand quicknet public key
+pk_hex = "83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a"
+timelock = Timelock(pk_hex)
+# An ephemeral secret key
+sk = bytearray([0x01, 0x02, 0x03, 0x04] * 8)
+# A "future" round number
+round_number = 1000
+# The message to encrypt
+plaintext = "Hello, Timelock!"
+# timelock encrypt
+ct = timelock.tle(round_number, plaintext, sk)
+```
+
+### Decrypt a Message
+
+``` python
+# get a signature at some point in the future
+signature_hex = "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
+sig = bytearray.fromhex(signature_hex)
+# and finally decrypt the message
+maybe_plaintext = timelock.tld(ct, sig)
+maybe_plaintext = maybe_plaintext.decode("utf-8")
+assert plaintext == maybe_plaintext
+```
+
+## License
+
+Apahce-2.0
diff --git a/py/src/timelock.egg-info/SOURCES.txt b/py/src/timelock.egg-info/SOURCES.txt
new file mode 100644
index 0000000..ebd6735
--- /dev/null
+++ b/py/src/timelock.egg-info/SOURCES.txt
@@ -0,0 +1,10 @@
+LICENSE
+README.md
+pyproject.toml
+src/timelock/__init__.py
+src/timelock/timelock.py
+src/timelock.egg-info/PKG-INFO
+src/timelock.egg-info/SOURCES.txt
+src/timelock.egg-info/dependency_links.txt
+src/timelock.egg-info/requires.txt
+src/timelock.egg-info/top_level.txt
\ No newline at end of file
diff --git a/py/src/timelock.egg-info/dependency_links.txt b/py/src/timelock.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/py/src/timelock.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/py/src/timelock.egg-info/requires.txt b/py/src/timelock.egg-info/requires.txt
new file mode 100644
index 0000000..c6c3cfe
--- /dev/null
+++ b/py/src/timelock.egg-info/requires.txt
@@ -0,0 +1 @@
+timelock_wasm_wrapper>=0.0.1
diff --git a/py/src/timelock.egg-info/top_level.txt b/py/src/timelock.egg-info/top_level.txt
new file mode 100644
index 0000000..0017cf2
--- /dev/null
+++ b/py/src/timelock.egg-info/top_level.txt
@@ -0,0 +1 @@
+timelock
diff --git a/rustfmt.toml b/rustfmt.toml
index 79e3fcf..82684e6 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -8,7 +8,7 @@ reorder_imports = true
# Consistency
newline_style = "Unix"
# Format comments
-comment_width = 100
+comment_width = 80
wrap_comments = true
# Misc
chain_width = 80
diff --git a/ts/README.md b/ts/README.md
index 1f7b2eb..c53cd63 100644
--- a/ts/README.md
+++ b/ts/README.md
@@ -1,6 +1,6 @@
# Timelock Encyrption TypeScript Wrapper
-This library provides a TypeScript wrapper for a WebAssembly (WASM) implementation of timelock encryption, designed for seamless use in web-based environments. It integrates easily with frameworks like React and supports timelock encryption, timelock decryption, and AES-GCM decryption functionality.
+This is a typescript library for timelock encryption. It is a "thin" wrapper that calls the WebAssembly (WASM) implementation of timelock encryption. It is designed for use in web-based environments and easily integrates with frameworks like React, Vue, etc. The library supports both the experiemental [Ideal Network beacon](https://docs.idealabs.network) as well as [Drand's](https://drand.love) Quicknet.
## Installation
@@ -10,25 +10,51 @@ The package can be installed from the npm registry with:
npm i @ideallabs/timelock.js
```
+## Build
+
+From the root, run
+
+```
+npm run build
+```
+
+In addition to transpiling the project, this builds the latest wasm and makes it available to the typescript wrapper.
+
+> Note: After running, navigate to the produced dist/index.js file and add `.js` endings to imports. See: https://github.com/ideal-lab5/timelock/issues/8
+
+
+## Test
+
+From the root, run:
+
+```shell
+npm run test
+```
+
## Usage
+See the [example](../examples/web/react-tlock-demo/) for a full demonstration.
+
### Initialization
Before using any encryption or decryption methods, initialize the library by creating a Timelock instance:
``` js
-import { Timelock } from '@ideallabs/timelock.js'
-
-const timelock = await Timelock.build();
+import { SupportedBeacon, Timelock } from '@ideallabs/timelock.js'
+// Use curve BLS12-381 (e.g. Drand Quicknet)
+const timelockBls12_381 = await Timelock.build(SupportedCurve.BLS12_381);
+// Use curve BLS12-377 (e.g. IDN Beacon)
+const timelockBls12_377 = await Timelock.build(SupportedCurve.BLS12_377);
```
### Encrypting a Message
-Encrypt data for a specific protocol round:
+Messages can be encrypted for future rounds of a supported beacon's protocol by specifying the be acon public key, round number, and message. Internally the library uses AES-GCM by default (this can be customized by implementing a custom [StreamCipherProvider](https://docs.rs/timelock/0.0.1/timelock/stream_ciphers/trait.StreamCipherProvider.html)).
``` js
// import a pre-defined IdentityHandler implementation or create your own
import { Timelock, IdealNetworkIdentityHandler } from '@ideallabs/timelock.js'
+
import hkdf from 'js-crypto-hkdf'
// 1. Setup parameters for encryption
// use an hkdf to securely generate an ephemeral secret key for AES-GCM encryption
@@ -53,11 +79,17 @@ let ct = await timelock.encrypt(
roundNumber,
IdealNetworkIdentityHandler,
pubkey,
- esk.key
+ esk.key,
)
console.log('Timelocked ciphertext: ' + JSON.stringify(ct))
```
+#### Identity Handlers
+
+Any given randomness beacon may sign messages in its own unique way. For example, in Drand's Quicknet the beacon signs the sha256 hash of the round number of the procol as a big endian array (8 bytes from a u64 round number). In the Ideal network, the message is the sha256 hash of the round number concatenated with the validator set id of the set of validators that produced the beacon.
+
+This library offers pre-defined identity handlers for usage with Drand Quicknet and the IDN beacon, the [DrandIdentityHandler](./src/interfaces/DrandIdentityBuilder.ts) and [IdealNetworkIdentityHandler](./src/interfaces/IDNIdentityBuilder.ts), respectively. For beacons that construct messages differently, a custom identity handler must be implemented.
+
### Decrypting a Message
Decrypt data using a beacon signature:
@@ -86,22 +118,6 @@ const plaintext = await timelock.forceDecrypt(ciphertext, esk.key);
console.log('Plaintext:', plaintext);
```
-## Build
-
-Build the project with:
-
-``` shell
-npm run build
-```
-
-## Test
-
-From the root, run:
-
-```shell
-npm run test
-```
-
## License
Apache-2.0
\ No newline at end of file
diff --git a/ts/examples/react-tlock-demo/config-overrides.d.ts b/ts/examples/react-tlock-demo/config-overrides.d.ts
deleted file mode 100644
index 368b0c6..0000000
--- a/ts/examples/react-tlock-demo/config-overrides.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-declare function _exports(config: any): any
-export = _exports
diff --git a/ts/examples/react-tlock-demo/src/App.d.ts b/ts/examples/react-tlock-demo/src/App.d.ts
deleted file mode 100644
index c592e6e..0000000
--- a/ts/examples/react-tlock-demo/src/App.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default App
-declare function App(): React.JSX.Element
-import React from 'react'
diff --git a/ts/examples/react-tlock-demo/src/index.d.ts b/ts/examples/react-tlock-demo/src/index.d.ts
deleted file mode 100644
index 336ce12..0000000
--- a/ts/examples/react-tlock-demo/src/index.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export {}
diff --git a/ts/examples/react-tlock-demo/src/setupTests.d.ts b/ts/examples/react-tlock-demo/src/setupTests.d.ts
deleted file mode 100644
index 336ce12..0000000
--- a/ts/examples/react-tlock-demo/src/setupTests.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export {}
diff --git a/ts/package-lock.json b/ts/package-lock.json
index ad4b581..8bfa565 100644
--- a/ts/package-lock.json
+++ b/ts/package-lock.json
@@ -1,15 +1,15 @@
{
"name": "@ideallabs/timelock.js",
- "version": "0.0.1",
+ "version": "1.0.0-dev",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ideallabs/timelock.js",
- "version": "0.0.1",
+ "version": "1.0.0-dev",
"license": "Apache-2.0",
"dependencies": {
- "timelock-wasm-wrapper": "file:../wasm/pkg/"
+ "timelock-wasm-wrapper": "file:../wasm/pkg/js/"
},
"devDependencies": {
"@babel/preset-env": "^7.26.0",
@@ -28,6 +28,11 @@
"version": "0.0.1",
"license": "Apache-2.0"
},
+ "../wasm/pkg/js": {
+ "name": "timelock_wasm_wrapper",
+ "version": "0.1.0",
+ "license": "Apache-2.0"
+ },
"node_modules/@ampproject/remapping": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
@@ -4685,7 +4690,7 @@
}
},
"node_modules/timelock-wasm-wrapper": {
- "resolved": "../wasm/pkg",
+ "resolved": "../wasm/pkg/js",
"link": true
},
"node_modules/tmpl": {
diff --git a/ts/package.json b/ts/package.json
index 6753ccd..7f7bba3 100644
--- a/ts/package.json
+++ b/ts/package.json
@@ -1,13 +1,13 @@
{
"name": "@ideallabs/timelock.js",
- "version": "1.0.0",
+ "version": "1.0.0-dev",
"description": "A typescript interface for timelock encryption.",
"license": "Apache-2.0",
"repository": "https://github.com/ideal-lab5/timelock",
"main": "dist/index.js",
"type": "module",
"dependencies": {
- "timelock-wasm-wrapper": "file:../wasm/pkg/"
+ "timelock-wasm-wrapper": "file:../wasm/pkg/js/"
},
"scripts": {
"build:wasm": "cd ../wasm && ./wasm_build.sh",
diff --git a/ts/src/index.ts b/ts/src/index.ts
index 45ba6cc..8023f19 100644
--- a/ts/src/index.ts
+++ b/ts/src/index.ts
@@ -16,4 +16,5 @@
export * from './timelock'
export { IdentityBuilder } from './interfaces/IIdentityBuilder'
-export { IdealNetworkIdentityBuilder } from './interfaces/IDNIdentityBuilder'
\ No newline at end of file
+export { IdealNetworkIdentityBuilder } from './interfaces/IDNIdentityBuilder'
+export { DrandIdentityBuilder } from './interfaces/DrandIdentityBuilder'
\ No newline at end of file
diff --git a/ts/src/interfaces/DrandIdentityBuilder.ts b/ts/src/interfaces/DrandIdentityBuilder.ts
new file mode 100644
index 0000000..cc54dd9
--- /dev/null
+++ b/ts/src/interfaces/DrandIdentityBuilder.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2024 by Ideal Labs, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { IdentityBuilder } from './IIdentityBuilder'
+import { Buffer } from "buffer";
+
+/**
+ * Convert a hex encoded string to a uint8array
+ * @param hexString The hex string (e.g. 0xabc123...)
+ * @returns A Uint8Array
+ */
+function hexToUint8Array(hexString) {
+ if (hexString.length % 2 !== 0) {
+ throw new Error("Hex string must have an even length.");
+ }
+
+ const uint8Array = new Uint8Array(hexString.length / 2);
+
+ for (let i = 0; i < hexString.length; i += 2) {
+ uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
+ }
+
+ return uint8Array;
+}
+
+/**
+ * Compute the sha256 hash of the data
+ * @param data: Some Uint8Array
+ * @returns The hash
+ */
+async function sha256(data: Uint8Array): Promise {
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+ return hashHex;
+}
+
+/**
+ * Build a message that is signed by Drand Quicknet: sha256(round_number as big endian)
+ * @param round: The round number
+ * @returns The message
+ */
+function generateMessage(round: number): Promise {
+ const buffer = Buffer.alloc(8);
+ buffer.writeBigUInt64BE(BigInt(round), 0);
+ return sha256(buffer).then(result => hexToUint8Array(result))
+}
+
+/**
+ * An IdentityBuilder for the Drand Quicknet beacon
+ */
+export const DrandIdentityBuilder: IdentityBuilder = {
+ build: (roundNumber) => generateMessage(roundNumber),
+}
\ No newline at end of file
diff --git a/ts/src/interfaces/IDNIdentityBuilder.ts b/ts/src/interfaces/IDNIdentityBuilder.ts
index a4f61b7..b384d55 100644
--- a/ts/src/interfaces/IDNIdentityBuilder.ts
+++ b/ts/src/interfaces/IDNIdentityBuilder.ts
@@ -21,5 +21,5 @@ import { build_encoded_commitment } from 'timelock-wasm-wrapper'
* An IdentityBuilder for the Ideal Network
*/
export const IdealNetworkIdentityBuilder: IdentityBuilder = {
- build: (bn) => build_encoded_commitment(bn, 0),
+ build: (bn) => Promise.resolve(build_encoded_commitment(bn, 0)),
}
\ No newline at end of file
diff --git a/ts/src/interfaces/IIdentityBuilder.ts b/ts/src/interfaces/IIdentityBuilder.ts
index e39e234..ccaffc5 100644
--- a/ts/src/interfaces/IIdentityBuilder.ts
+++ b/ts/src/interfaces/IIdentityBuilder.ts
@@ -25,5 +25,5 @@ export interface IdentityBuilder {
* @param x : The identity data
* @returns : The constructed identity
*/
- build: (x: X) => Uint8Array
+ build: (x: X) => Promise
}
diff --git a/ts/src/timelock.test.spec.ts b/ts/src/timelock.test.spec.ts
index 72c63b3..fc4a966 100644
--- a/ts/src/timelock.test.spec.ts
+++ b/ts/src/timelock.test.spec.ts
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import { expect, describe } from '@jest/globals'
-import { Timelock } from './timelock'
+import { expect, describe, test } from '@jest/globals'
+import { Result, SupportedCurve, Timelock, u8a } from './timelock'
import { IdealNetworkIdentityBuilder } from './interfaces/IDNIdentityBuilder'
import init, {
build_encoded_commitment,
@@ -26,22 +26,26 @@ import init, {
jest.mock('timelock-wasm-wrapper')
-describe('Timelock Class', () => {
+describe('Timelock Encryption', () => {
beforeEach(() => {
jest.clearAllMocks()
})
test('should initialize WASM and create an instance', async () => {
- const instance = await Timelock.build()
+ const instance = await Timelock.build(SupportedCurve.BLS12_377)
expect(init).toHaveBeenCalledTimes(1)
expect(instance).toBeInstanceOf(Timelock)
})
test('should encrypt data using tle', async () => {
- const instance = await Timelock.build()
+ const instance = await Timelock.build(SupportedCurve.BLS12_377)
const encodedMessage = new Uint8Array([1, 2, 3])
- const beaconPublicKey = new Uint8Array([4, 5, 6])
- const ephemeralSecretKey = new Uint8Array([7, 8, 9])
+
+ const beaconPublicKeyHex = 'abcdef'
+ const ephemeralSecretKeyHex = '123456'
+
+ const beaconPublicKey = u8a(beaconPublicKeyHex)
+ const ephemeralSecretKey = u8a(ephemeralSecretKeyHex)
const expectedResult = new Uint8Array(1);
@@ -49,8 +53,8 @@ describe('Timelock Class', () => {
encodedMessage,
42,
IdealNetworkIdentityBuilder,
- beaconPublicKey,
- ephemeralSecretKey
+ beaconPublicKeyHex,
+ ephemeralSecretKeyHex,
)
expect(result).toStrictEqual(expectedResult)
@@ -59,31 +63,35 @@ describe('Timelock Class', () => {
0,
encodedMessage,
ephemeralSecretKey,
- beaconPublicKey
+ beaconPublicKey,
+ SupportedCurve.BLS12_377
)
})
test('should decrypt data using tld', async () => {
- const instance = await Timelock.build()
+ const instance = await Timelock.build(SupportedCurve.BLS12_377)
const ciphertext = new Uint8Array([10, 11, 12])
- const signature = new Uint8Array([13, 14, 15])
+ const signatureHex = '123456'
+ const signature = u8a(signatureHex)
const expectedResult = new Uint8Array(2);
- const result = await instance.decrypt(ciphertext, signature)
+ const result = await instance.decrypt(ciphertext, signatureHex)
- expect(tld).toHaveBeenCalledWith(ciphertext, signature)
expect(result).toStrictEqual(expectedResult)
+ expect(tld).toHaveBeenCalledWith(ciphertext, signature, SupportedCurve.BLS12_377)
})
test('should force decrypt data using decrypt', async () => {
- const instance = await Timelock.build()
+ const instance = await Timelock.build(SupportedCurve.BLS12_377)
const ciphertext = new Uint8Array([16, 17, 18])
- const ephemeralSecretKey = new Uint8Array([19, 20, 21])
+ const ephemeralSecretKey = 'qwerty'
+ const esk = u8a(ephemeralSecretKey);
const expectedResult = new Uint8Array(3)
const result = await instance.forceDecrypt(ciphertext, ephemeralSecretKey)
- expect(decrypt).toHaveBeenCalledWith(ciphertext, ephemeralSecretKey)
expect(result).toStrictEqual(expectedResult)
+
+ expect(decrypt).toHaveBeenCalledWith(ciphertext, esk, SupportedCurve.BLS12_377)
})
})
diff --git a/ts/src/timelock.ts b/ts/src/timelock.ts
index a2c2f2e..87bd263 100644
--- a/ts/src/timelock.ts
+++ b/ts/src/timelock.ts
@@ -32,6 +32,27 @@ export enum TimelockErrors {
ERR_UNEXPECTED_TYPE = "The wasm returned something that could not be converted to a UInt8Array."
}
+/**
+ * Curves supported by the timelock library
+ */
+export enum SupportedCurve {
+ BLS12_377 = 'bls12_377',
+ BLS12_381 = 'bls12_381'
+}
+
+/**
+ * A wrapper type to handle generic results
+ */
+export type Result = T | Error
+
+function ok(data: T | null): Result {
+ return data
+}
+
+function error(message: string): Result {
+ return new Error(message)
+}
+
/**
* The Timelock class handles initialization of the WASM modules required to use the Timelock library
* from web based contexts. It gracefully ensures that the WASM is available before attempting to call the respective functions.
@@ -40,20 +61,29 @@ export class Timelock {
/**
* Indicates if the wasm has been initialized or not
*/
- private wasmReady: false
+ private wasmReady: boolean
+
+ /**
+ * The curve used by the beacon
+ */
+ public curveId: SupportedCurve
/**
* A private constructor to enforce usage of `build`
*/
- private constructor() { }
+ private constructor(curveId: SupportedCurve) {
+ this.curveId = curveId
+ this.wasmReady = false
+ }
/**
* Loads the wasm and constructs a new Timelock instance
+ * @param curveId: The curve used by the beacon
* @returns A Timelock instance
*/
- public static async build() {
+ public static async build(curveId: SupportedCurve) {
await init()
- return new Timelock()
+ return new Timelock(curveId)
}
/**
@@ -62,20 +92,30 @@ export class Timelock {
* @param encodedMessage: The message to encrypt, encoded as a Uint8Array
* @param roundNumber: The round of the protocol when the message can be decrypted
* @param identityBuilder: An IdentityBuilder implementation
- * @param beaconPublicKey: The public key of the randomness beacon
- * @param ephemeralSecretKey: An ephemeral secret key passed to AES-GCM
+ * @param beaconPublicKeyHex: The hex-encoded public key of the randomness beacon
+ * @param ephemeralSecretKeyHex: A hex-encoded ephemeral secret key passed to AES-GCM
* @returns The timelocked ciphertext if successful, otherwise an error message.
*/
public async encrypt(
encodedMessage: Uint8Array,
roundNumber: number,
identityBuilder: IdentityBuilder,
- beaconPublicKey: Uint8Array,
- ephemeralSecretKey: Uint8Array
- ): Promise {
- await this.checkWasm()
- let id = identityBuilder.build(roundNumber)
- return new Uint8Array(tle(id, encodedMessage, ephemeralSecretKey, beaconPublicKey))
+ beaconPublicKeyHex: string,
+ ephemeralSecretKeyHex: string
+ ): Promise> {
+ try {
+ await this.checkWasm()
+ const beaconPublicKey = u8a(beaconPublicKeyHex)
+ const ephemeralSecretKey = u8a(ephemeralSecretKeyHex)
+ const id = await identityBuilder.build(roundNumber)
+ return ok(
+ new Uint8Array(
+ tle(id, encodedMessage, ephemeralSecretKey, beaconPublicKey, this.curveId)
+ )
+ )
+ } catch (err) {
+ return error(err.message)
+ }
}
/**
@@ -87,10 +127,15 @@ export class Timelock {
*/
public async decrypt(
ciphertext: Uint8Array,
- signature: Uint8Array
- ): Promise {
- await this.checkWasm()
- return new Uint8Array(tld(ciphertext, signature))
+ signatureHex: string
+ ): Promise> {
+ try {
+ await this.checkWasm()
+ const signature = u8a(signatureHex)
+ return ok(new Uint8Array(tld(ciphertext, signature, this.curveId)))
+ } catch (err) {
+ return error(err.message)
+ }
}
/**
@@ -102,19 +147,48 @@ export class Timelock {
*/
public async forceDecrypt(
ciphertext: Uint8Array,
- ephemeralSecretKey: Uint8Array
- ): Promise {
- await this.checkWasm()
- return new Uint8Array(decrypt(ciphertext, ephemeralSecretKey))
+ ephemeralSecretKeyHex: string
+ ): Promise> {
+ try {
+ await this.checkWasm()
+ const ephemeralSecretKey = u8a(ephemeralSecretKeyHex)
+ return ok(new Uint8Array(decrypt(ciphertext, ephemeralSecretKey, this.curveId)))
+ } catch (err) {
+ return error(err.message)
+ }
}
/**
* Check if the wasm has been initialized.
- * If it hasn't, gracefully load it and continue.
+ * If it hasn't, gracefully load it and continue, else throw an error if it is unavailable
*/
- async checkWasm() {
+ private async checkWasm() {
if (!this.wasmReady) {
- await init()
+ try {
+ await init()
+ this.wasmReady = true
+ } catch (err) {
+ throw new Error("Failed to initialize WASM " + err.message)
+ }
}
}
}
+
+/**
+ * Converts the hex-encoded string to a Uint8Array
+ * @param hex A hex-encoded string
+ * @returns A Uint8Array
+ */
+export function u8a(hexString: string): Uint8Array {
+ const length = hexString.length;
+ if (length % 2 !== 0) {
+ throw new Error("Invalid hex string: Length must be even.");
+ }
+
+ const bytes = new Uint8Array(length / 2);
+ for (let i = 0; i < length; i += 2) {
+ bytes[i / 2] = (parseInt(hexString[i], 16) << 4) | parseInt(hexString[i + 1], 16);
+ }
+
+ return bytes;
+}
\ No newline at end of file
diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml
index 24ad3c1..68b43dc 100644
--- a/wasm/Cargo.toml
+++ b/wasm/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "timelock_wasm_wrapper"
-version = "0.0.2"
+version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
description = "Wasm bidings for the timelock encryption crate"
@@ -44,4 +44,4 @@ wasm-bindgen-test = "0.3.0"
[features]
default = []
-python = [ "pyo3" ]
+python = ["pyo3", "pyo3/abi3"]
diff --git a/wasm/src/js.rs b/wasm/src/js.rs
index 7dc5908..16ab3f5 100644
--- a/wasm/src/js.rs
+++ b/wasm/src/js.rs
@@ -16,14 +16,11 @@
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use codec::Encode;
-use rand_chacha::ChaCha20Rng;
-use rand_core::{OsRng, SeedableRng};
-use serde::{Deserialize, Serialize};
-use serde_big_array::BigArray;
-use sha2::Digest;
+use rand_core::OsRng;
use sp_consensus_beefy_etf::{known_payloads, Commitment, Payload};
-
+use serde::{Serialize, Deserialize};
use timelock::{
+ curves::drand::TinyBLS381,
ibe::fullident::Identity,
stream_ciphers::{
AESGCMStreamCipherProvider, AESOutput, StreamCipherProvider,
@@ -31,7 +28,7 @@ use timelock::{
tlock::{tld as timelock_decrypt, tle as timelock_encrypt, TLECiphertext},
};
-use w3f_bls::{DoublePublicKey, DoublePublicKeyScheme, EngineBLS, TinyBLS377};
+use w3f_bls::{EngineBLS, TinyBLS377};
use wasm_bindgen::prelude::*;
/// a helper function to deserialize arkworks elements from bytes
@@ -41,33 +38,57 @@ fn convert_from_bytes(
E::deserialize_compressed(&bytes[..]).ok()
}
+/// Supported Beacon Types
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum SupportedCurve {
+ Bls12_377,
+ Bls12_381,
+}
+
/// The encrypt wrapper used by the WASM blob to call tlock.rs encrypt function
-/// * 'id_js': ID string for which the message will be encrypted
-/// * 'message_js': Message which will be encrypted
-/// * 'sk_js': secret key passed in from UI. This should be obtained elsewhere
+/// * `id_js`: ID string for which the message will be encrypted
+/// * `message_js`: Message which will be encrypted
+/// * `sk_js`: secret key passed in from UI. This should be obtained elsewhere
/// later on.
-/// * 'p_pub_js': the public key commitment for the IBE system
+/// * `p_pub_js`: the public key commitment for the IBE system
+/// * `
+///
#[wasm_bindgen]
pub fn tle(
id_js: JsValue,
message_js: JsValue,
sk_js: JsValue,
p_pub_js: JsValue,
+ supported_curve_js: JsValue,
+) -> Result {
+ let curve: SupportedCurve = serde_wasm_bindgen::from_value(supported_curve_js.clone())
+ .map_err(|_| JsError::new("could not decode the curve type"))?;
+
+ match curve {
+ SupportedCurve::Bls12_377 => do_tle::(id_js, message_js, sk_js, p_pub_js),
+ SupportedCurve::Bls12_381 => do_tle::(id_js, message_js, sk_js, p_pub_js),
+ }
+}
+
+pub fn do_tle(
+ id_js: JsValue,
+ message_js: JsValue,
+ sk_js: JsValue,
+ p_pub_js: JsValue,
) -> Result {
let msk_bytes: [u8; 32] = serde_wasm_bindgen::from_value(sk_js.clone())
.map_err(|_| JsError::new("could not decode secret key"))?;
- let pp_conversion: Vec =
+ let p_pub_vec: Vec =
serde_wasm_bindgen::from_value(p_pub_js.clone())
.map_err(|_| JsError::new("could not decode p_pub"))?;
- let pp_bytes: [u8; 144] = pp_conversion
+ let pp_bytes: [u8; 96] = p_pub_vec
.try_into()
.map_err(|_| JsError::new("could not convert public params"))?;
- let double_pub_key =
- convert_from_bytes::, 144>(
- &pp_bytes.clone(),
- )
- .ok_or(JsError::new("Could not convert secret key"))?;
- let pp = double_pub_key.1;
+ let pp = convert_from_bytes::<::PublicKeyGroup, 96>(
+ &pp_bytes.clone(),
+ )
+ .ok_or(JsError::new("Could not convert secret key"))?;
let id_bytes: Vec = serde_wasm_bindgen::from_value(id_js.clone())
.map_err(|_| JsError::new("could not decode id"))?;
@@ -77,8 +98,8 @@ pub fn tle(
.map_err(|_| JsError::new("could not decode message"))?;
let mut ciphertext_bytes: Vec = Vec::new();
- let ciphertext: TLECiphertext =
- timelock_encrypt::(
+ let ciphertext: TLECiphertext =
+ timelock_encrypt::(
pp,
msk_bytes,
&message_bytes,
@@ -96,46 +117,73 @@ pub fn tle(
}
/// The decrypt wrapper used by the WASM blob to call tlock.rs encrypt function
-/// * 'ciphertext_js': The string to be decrypted
-/// * 'sig_vec_js': The array of BLS signatures required to rebuild the secret
+/// * `ciphertext_js`: The string to be decrypted
+/// * `sig_vec_js`: The array of BLS signatures required to rebuild the secret
/// key and decrypt the message
#[wasm_bindgen]
pub fn tld(
ciphertext_js: JsValue,
sig_vec_js: JsValue,
+ supported_curve_js: JsValue,
+) -> Result {
+ let curve: SupportedCurve = serde_wasm_bindgen::from_value(supported_curve_js.clone())
+ .map_err(|_| JsError::new("could not decode the curve type"))?;
+
+ match curve {
+ SupportedCurve::Bls12_377 => do_tld::(ciphertext_js, sig_vec_js),
+ SupportedCurve::Bls12_381 => do_tld::(ciphertext_js, sig_vec_js),
+ }
+}
+
+/// Timelock decryption
+fn do_tld(
+ ciphertext_js: JsValue,
+ sig_vec_js: JsValue,
) -> Result {
let sig_conversion: Vec =
serde_wasm_bindgen::from_value(sig_vec_js.clone())
.map_err(|_| JsError::new("could not decode secret key"))?;
let sig_bytes = sig_conversion.as_slice();
let sig_point =
- ::SignatureGroup::deserialize_compressed(
- sig_bytes,
- )
- .map_err(|_| JsError::new("could not deserialize sig_vec"))?;
+ ::SignatureGroup::deserialize_compressed(sig_bytes)
+ .map_err(|_| JsError::new("could not deserialize sig_vec"))?;
let ciphertext_vec: Vec =
serde_wasm_bindgen::from_value(ciphertext_js.clone())
.map_err(|_| JsError::new("could not decode ciphertext"))?;
let ciphertext_bytes: &[u8] = ciphertext_vec.as_slice();
- let ciphertext: TLECiphertext =
+ let ciphertext: TLECiphertext =
TLECiphertext::deserialize_compressed(ciphertext_bytes)
.map_err(|_| JsError::new("Could not deserialize ciphertext"))?;
- let result: Vec = timelock_decrypt::<
- TinyBLS377,
- AESGCMStreamCipherProvider,
- >(ciphertext, sig_point)
+ let result: Vec = timelock_decrypt::(
+ ciphertext, sig_point,
+ )
.map_err(|e| JsError::new(&format!("decryption has failed {:?}", e)))?;
serde_wasm_bindgen::to_value(&result)
.map_err(|_| JsError::new("plaintext conversion has failed"))
}
-/// Bypass Tlock by attempting to decrypt the ciphertext with some secret key
-/// under the stream cipher only
#[wasm_bindgen]
pub fn decrypt(
ciphertext_js: JsValue,
sk_vec_js: JsValue,
+ supported_curve_js: JsValue,
+) -> Result {
+ let curve: SupportedCurve = serde_wasm_bindgen::from_value(supported_curve_js.clone())
+ .map_err(|_| JsError::new("could not decode the curve type"))?;
+
+ match curve {
+ SupportedCurve::Bls12_377 => do_decrypt::(ciphertext_js, sk_vec_js),
+ SupportedCurve::Bls12_381 => do_decrypt::(ciphertext_js, sk_vec_js),
+ }
+}
+
+/// Bypass Tlock by attempting to decrypt the ciphertext with some secret key
+/// under the stream cipher only
+// #[wasm_bindgen]
+pub fn do_decrypt(
+ ciphertext_js: JsValue,
+ sk_vec_js: JsValue,
) -> Result {
let sk_bytes: Vec =
serde_wasm_bindgen::from_value(sk_vec_js.clone())
@@ -152,7 +200,7 @@ pub fn decrypt(
serde_wasm_bindgen::from_value(ciphertext_js.clone())
.map_err(|_| JsError::new("could not decode ciphertext"))?;
let ciphertext_bytes: &[u8] = ciphertext_vec.as_slice();
- let ciphertext: TLECiphertext =
+ let ciphertext: TLECiphertext =
TLECiphertext::deserialize_compressed(ciphertext_bytes)
.map_err(|_| JsError::new("Could not deserialize ciphertext"))?;
@@ -174,18 +222,6 @@ extern "C" {
fn log(s: &str);
}
-/// Struct for testing that allows for the serialization of the double public
-/// key type
-#[derive(
- Serialize, CanonicalSerialize, CanonicalDeserialize, Deserialize, Debug,
-)]
-pub struct KeyChain {
- #[serde(with = "BigArray")]
- pub double_public: [u8; 144],
-
- pub sk: [u8; 32],
-}
-
/// Builds an encoded commitment for use in timelock encryption using the Ideal
/// Network
#[wasm_bindgen]
@@ -210,50 +246,47 @@ pub fn build_encoded_commitment(
})
}
-/// This function is used purely for testing purposes.
-/// It takes in a seed and generates a secret key and public params.
-#[wasm_bindgen]
-pub fn generate_keys(seed: JsValue) -> Result {
- let seed_vec: Vec = serde_wasm_bindgen::from_value(seed)
- .map_err(|_| JsError::new("Could not convert seed to string"))?;
- let seed_vec = seed_vec.as_slice();
-
- let mut hasher = sha2::Sha256::default();
- hasher.update(seed_vec);
- let hash = hasher.finalize();
- let seed_hash: [u8; 32] = hash.into();
- let mut rng: ChaCha20Rng = ChaCha20Rng::from_seed(seed_hash);
- let keypair = w3f_bls::KeypairVT::::generate(&mut rng);
- let sk_gen: ::Scalar = keypair.secret.0;
- let double_public: DoublePublicKey = DoublePublicKey(
- keypair.into_public_key_in_signature_group().0,
- keypair.public.0,
- );
- let mut sk_bytes = Vec::new();
- sk_gen.serialize_compressed(&mut sk_bytes).unwrap();
- let mut double_public_bytes = Vec::new();
- double_public.serialize_compressed(&mut double_public_bytes).unwrap();
- let kc = KeyChain {
- double_public: double_public_bytes.try_into().unwrap(),
- sk: sk_bytes.try_into().unwrap(),
- };
- serde_wasm_bindgen::to_value(&kc)
- .map_err(|_| JsError::new("could not convert secret key to JsValue"))
-}
-
#[cfg(test)]
mod test {
use super::*;
-
- use std::any::Any;
- use w3f_bls::{EngineBLS, TinyBLS377};
+ use rand_chacha::ChaCha20Rng;
+ use rand_core::SeedableRng;
+ use sha2::Digest;
+ use w3f_bls::{
+ DoublePublicKey, DoublePublicKeyScheme, EngineBLS, TinyBLS377,
+ };
use wasm_bindgen_test::*;
enum TestStatusReport {
EncryptSuccess { ciphertext: JsValue },
DecryptSuccess { plaintext: JsValue },
EncryptFailure { _error: JsError },
- DecryptFailure { error: JsError },
+ DecryptFailure { _error: JsError },
+ }
+
+ /// This function is used purely for testing purposes.
+ /// It takes in a seed and generates a secret key and public params
+ fn generate_keys(seed: JsValue) -> ([u8; 144], [u8; 32]) {
+ let seed_vec: Vec = serde_wasm_bindgen::from_value(seed).unwrap();
+ let seed_vec = seed_vec.as_slice();
+
+ let mut hasher = sha2::Sha256::default();
+ hasher.update(seed_vec);
+ let hash = hasher.finalize();
+ let seed_hash: [u8; 32] = hash.into();
+ let mut rng: ChaCha20Rng = ChaCha20Rng::from_seed(seed_hash);
+ let keypair = w3f_bls::KeypairVT::::generate(&mut rng);
+ let sk_gen: ::Scalar = keypair.secret.0;
+ let double_public: DoublePublicKey = DoublePublicKey(
+ keypair.into_public_key_in_signature_group().0,
+ keypair.public.0,
+ );
+ let mut sk_bytes = Vec::new();
+ sk_gen.serialize_compressed(&mut sk_bytes).unwrap();
+ let mut double_public_bytes = Vec::new();
+ double_public.serialize_compressed(&mut double_public_bytes).unwrap();
+
+ (double_public_bytes.try_into().unwrap(), sk_bytes.try_into().unwrap())
}
fn setup_test(
@@ -261,24 +294,21 @@ mod test {
message: Vec,
succesful_decrypt: bool,
standard_tle: bool,
+ beacon: &str,
handler: &dyn Fn(TestStatusReport) -> (),
) {
let seed_bytes = "seeeeeeed".as_bytes();
let seed = serde_wasm_bindgen::to_value(seed_bytes).unwrap();
- let keys_js = generate_keys(seed).ok().unwrap();
- let key_chain: KeyChain =
- serde_wasm_bindgen::from_value(keys_js).unwrap();
- let sk: [u8; 32] = key_chain.sk;
- let mut sk_bytes: Vec = Vec::new();
- sk.serialize_compressed(&mut sk_bytes).unwrap();
- let sk_js: JsValue = serde_wasm_bindgen::to_value(&sk_bytes).unwrap();
-
- let p_pub: [u8; 144] = key_chain.double_public;
- let mut p_pub_bytes: Vec = Vec::new();
- p_pub.serialize_compressed(&mut p_pub_bytes).unwrap();
+ let (p_pub, sk) = generate_keys::(seed);
+ // let mut sk_bytes: Vec = Vec::new();
+ // sk.serialize_compressed(&mut sk_bytes).unwrap();
+ let mut sk_js: JsValue =
+ serde_wasm_bindgen::to_value(sk.as_slice()).unwrap();
+ // let mut p_pub_bytes: Vec = Vec::new();
+ // p_pub.1.serialize_compressed(&mut p_pub_bytes).unwrap();
let p_pub_js: JsValue =
- serde_wasm_bindgen::to_value(&p_pub_bytes).unwrap();
+ serde_wasm_bindgen::to_value(&p_pub[48..]).unwrap();
let identity_js: JsValue =
serde_wasm_bindgen::to_value(&identity_vec).unwrap();
@@ -302,69 +332,88 @@ mod test {
let bad_sig_vec = vec![bad_sig];
bad_sig_vec.serialize_compressed(&mut sig_bytes).unwrap();
- //this portion (intentionally) messes up the decryption result for
+ // this portion (intentionally) corrupts the decryption result for
// early decryption
- let bad_seed_bytes = "bad".as_bytes();
- let bad_seed =
- serde_wasm_bindgen::to_value(bad_seed_bytes).unwrap();
- let bad_keys_js: JsValue = generate_keys(bad_seed).ok().unwrap();
- let bad_key_chain: KeyChain =
- serde_wasm_bindgen::from_value(bad_keys_js).unwrap();
- let bad_sk: [u8; 32] = bad_key_chain.sk;
- bad_sk.serialize_compressed(&mut sk_bytes).unwrap();
+ sk_js = serde_wasm_bindgen::to_value([1; 32].as_slice()).unwrap();
}
let sig_vec_js: JsValue =
serde_wasm_bindgen::to_value(&sig_bytes).unwrap();
if standard_tle {
- match tle(identity_js, message_js, sk_js, p_pub_js) {
+ match tle(identity_js, message_js, sk_js, p_pub_js, beacon.into()) {
Ok(ciphertext) => {
let ciphertext_clone = ciphertext.clone();
handler(TestStatusReport::EncryptSuccess { ciphertext });
- match tld(ciphertext_clone, sig_vec_js) {
- Ok(plaintext) =>
+ match tld(ciphertext_clone, sig_vec_js, beacon.into()) {
+ Ok(plaintext) => {
handler(TestStatusReport::DecryptSuccess {
plaintext,
- }),
- Err(error) =>
- handler(TestStatusReport::DecryptFailure { error }),
+ })
+ },
+ Err(error) => {
+ handler(TestStatusReport::DecryptFailure {
+ _error: error
+ })
+ },
}
},
- Err(error) =>
- handler(TestStatusReport::EncryptFailure { _error: error }),
+ Err(error) => {
+ handler(TestStatusReport::EncryptFailure { _error: error })
+ },
}
} else {
- match tle(identity_js, message_js, sk_js, p_pub_js) {
+ match tle(
+ identity_js,
+ message_js,
+ sk_js.clone(),
+ p_pub_js,
+ beacon.into(),
+ ) {
Ok(ciphertext) => {
- let sk_js_early: JsValue =
- serde_wasm_bindgen::to_value(&sk_bytes).unwrap();
+ // let sk_js_early: JsValue =
+ // serde_wasm_bindgen::to_value(&sk_bytes).unwrap();
let ciphertext_clone = ciphertext.clone();
handler(TestStatusReport::EncryptSuccess { ciphertext });
- match decrypt(ciphertext_clone, sk_js_early) {
- Ok(plaintext) =>
+ match decrypt(ciphertext_clone, sk_js, beacon.into()) {
+ Ok(plaintext) => {
handler(TestStatusReport::DecryptSuccess {
plaintext,
- }),
- Err(error) =>
- handler(TestStatusReport::DecryptFailure { error }),
+ })
+ },
+ Err(error) => {
+ handler(TestStatusReport::DecryptFailure {
+ _error: error
+ })
+ },
}
},
- Err(error) =>
- handler(TestStatusReport::EncryptFailure { _error: error }),
+ Err(error) => {
+ handler(TestStatusReport::EncryptFailure { _error: error })
+ },
}
}
}
#[wasm_bindgen_test]
- pub fn can_encrypt_decrypt() {
+ pub fn can_encrypt_decrypt_ideal() {
+ can_encrypt_decrypt::("ideal");
+ }
+
+ #[wasm_bindgen_test]
+ pub fn can_encrypt_decrypt_drand() {
+ can_encrypt_decrypt::("drand");
+ }
+
+ pub fn can_encrypt_decrypt(beacon_type: &str) {
let message: Vec = b"this is a test message".to_vec();
let id: Vec = b"testing purposes".to_vec();
- setup_test::(
+ setup_test::(
id,
message.clone(),
true,
true,
+ beacon_type,
&|status: TestStatusReport| match status {
TestStatusReport::EncryptSuccess { ciphertext } => {
let ciphertext_convert: Vec =
@@ -385,14 +434,24 @@ mod test {
}
#[wasm_bindgen_test]
- pub fn can_encrypt_decrypt_early() {
+ pub fn can_encrypt_decrypt_early_ideal() {
+ can_encrypt_decrypt_early::("ideal");
+ }
+
+ #[wasm_bindgen_test]
+ pub fn can_encrypt_decrypt_early_drand() {
+ can_encrypt_decrypt_early::("drand");
+ }
+
+ pub fn can_encrypt_decrypt_early(beacon_type: &str) {
let message: Vec = b"this is a test message".to_vec();
let id: Vec = b"testing purposes".to_vec();
- setup_test::(
+ setup_test::(
id,
message.clone(),
true,
false,
+ beacon_type.into(),
&|status: TestStatusReport| match status {
TestStatusReport::EncryptSuccess { ciphertext } => {
let ciphertext_convert: Vec =
@@ -411,42 +470,4 @@ mod test {
},
)
}
-
- #[wasm_bindgen_test]
- pub fn decrypt_failure_early() {
- let message: Vec = b"this is a test message".to_vec();
- let id: Vec = b"testing purposes".to_vec();
- setup_test::(
- id,
- message.clone(),
- false,
- false,
- &|status: TestStatusReport| {
- match status {
- TestStatusReport::EncryptSuccess { ciphertext } => {
- let ciphertext_convert: Vec =
- serde_wasm_bindgen::from_value(ciphertext.clone())
- .unwrap();
- assert!(ciphertext.is_truthy());
- assert_ne!(ciphertext_convert, message);
- },
- TestStatusReport::DecryptFailure { error } => {
- // This test needs to be updated. As of right now, there
- // doesn't seem to be a way to reliably compare errors
- // however the test will fail if no error is thrown from
- // decrypt. We just won't know if it was the decrypt
- // function failing. NOTE: TypeId comes from the
- // std library. A `TypeId` represents a globally
- // unique identifier for a type.
- let error_compare = JsError::new("this is irrelevant. We only check that it's a JsError (which it always is)");
- let type_id_compare = error_compare.type_id();
- let type_id = error.type_id();
-
- assert_eq!(type_id, type_id_compare);
- },
- _ => panic!("decrypt was successful"),
- }
- },
- )
- }
}
From e35b9a25e5018aa96a80dadae4e845d3df9af421 Mon Sep 17 00:00:00 2001
From: driemworks
Date: Tue, 17 Dec 2024 12:40:05 -0600
Subject: [PATCH 2/6] chore: update example and readme
---
examples/web/react-tlock-demo/src/App.js | 28 ++++++++++---------
ts/README.md | 35 ++++++++++++++----------
ts/src/timelock.ts | 8 ++----
3 files changed, 38 insertions(+), 33 deletions(-)
diff --git a/examples/web/react-tlock-demo/src/App.js b/examples/web/react-tlock-demo/src/App.js
index a2a7835..7157a4b 100644
--- a/examples/web/react-tlock-demo/src/App.js
+++ b/examples/web/react-tlock-demo/src/App.js
@@ -16,7 +16,7 @@
import './App.css'
import React, { useEffect, useState } from 'react'
-import { Timelock, IdealNetworkIdentityBuilder, DrandIdentityBuilder, SupportedCurve } from '@ideallabs/timelock.js'
+import { Timelock, IdealNetworkIdentityBuilder, DrandIdentityBuilder, SupportedCurve, u8a } from '@ideallabs/timelock.js'
import hkdf from 'js-crypto-hkdf'
function App() {
@@ -48,14 +48,16 @@ function App() {
const hash = 'SHA-256'
const length = 32
const esk = await hkdf.compute(seed, hash, length, '')
+ const key = Array.from(esk.key)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('')
// the message to encrypt for the future
const message = 'Hello, Timelock!'
const encodedMessage = new TextEncoder().encode(message)
// A randomness beacon public key (ex: IDN public key)
// We first get it as hex and then convert to a Uint8Array
- const pkHex =
+ const pubkey =
'83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a'
- const pubkey = fromHexString(pkHex)
// A future round number of the randomness beacon
const roundNumber = 1000
// 2. Encrypt the message
@@ -64,16 +66,16 @@ function App() {
roundNumber,
DrandIdentityBuilder,
pubkey,
- esk.key
+ key
)
console.log('Timelocked ciphertext: ' + JSON.stringify(ct))
// 3. Acquire a signature for decryption from he pulse output by the beacon at the given roundNumber
const sigHex =
'b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39'
- const sig = fromHexString(sigHex)
// Decrypt the ciphertext with the signature
- const plaintext = await timelockDrand.decrypt(ct, sig)
+ const plaintext = await timelockDrand.decrypt(ct, sigHex)
+ // console.log(plaintext)
console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`)
}
@@ -84,15 +86,16 @@ function App() {
const hash = 'SHA-256'
const length = 32
const esk = await hkdf.compute(seed, hash, length, '')
+ const key = Array.from(esk.key)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('')
// the message to encrypt for the future
const message = 'Hello, Timelock!'
const encodedMessage = new TextEncoder().encode(message)
// A randomness beacon public key (ex: IDN public key)
// We first get it as hex and then convert to a Uint8Array
- const pkHex =
- 'b68da85d953219f84d86c5167481f505edf04ab586f28aefd238475026f5f46ba707f41bd2702f3639a4eddff8cad50041dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100'
- // we only want the pubkey portion in G2, where we have [48-sig bytes][96 pk bytes]
- const pubkey = fromHexString(pkHex).slice(48)
+ const pubkey =
+ '41dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100'
// A future round number of the randomness beacon
const roundNumber = 10
@@ -102,15 +105,14 @@ function App() {
roundNumber,
IdealNetworkIdentityBuilder,
pubkey,
- esk.key
+ key
)
console.log('Timelocked ciphertext: ' + JSON.stringify(ct))
// 3. Acquire a signature for decryption from he pulse output by the beacon at the given roundNumber
- const sigHex =
+ const sig =
'e6cdf6c9d11c13e013b2c6cfd11dab46d8f1ace226ff845ffff4c7d6f64992892c54fb5d1f0f87dd300ce66f53598e01'
- const sig = fromHexString(sigHex)
// Decrypt the ciphertext with the signature
const plaintext = await timelockIdeal.decrypt(ct, sig)
console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`)
diff --git a/ts/README.md b/ts/README.md
index c53cd63..6eeabd3 100644
--- a/ts/README.md
+++ b/ts/README.md
@@ -57,30 +57,33 @@ import { Timelock, IdealNetworkIdentityHandler } from '@ideallabs/timelock.js'
import hkdf from 'js-crypto-hkdf'
// 1. Setup parameters for encryption
-// use an hkdf to securely generate an ephemeral secret key for AES-GCM encryption
-const seed = new TextEncoder().encode('password')
+// use an hkdf to generate an ephemeral secret key
+const seed = new TextEncoder().encode('my-secret-seed')
const hash = 'SHA-256'
const length = 32
const esk = await hkdf.compute(seed, hash, length, '')
+const key = Array.from(esk.key)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('')
// the message to encrypt for the future
const message = 'Hello, Timelock!'
const encodedMessage = new TextEncoder().encode(message)
// A randomness beacon public key (ex: IDN public key)
-const pkHex =
- 'b68da85d953219f84d86c5167481f505edf04ab586f28aefd238475026f5f46ba707f41bd2702f3639a4eddff8cad50041dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100'
-// Convert the hex string to a Uint8Array
-const pubkey = Uint8Array.from(pkHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
-// A FUTURE round number of the randomness beacon
+// We first get it as hex and then convert to a Uint8Array
+const pubkey =
+ '41dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100'
+// A future round number of the randomness beacon
const roundNumber = 10
// 2. Encrypt the message
-let ct = await timelock.encrypt(
+let ct = await timelockIdeal.encrypt(
encodedMessage,
roundNumber,
- IdealNetworkIdentityHandler,
+ IdealNetworkIdentityBuilder,
pubkey,
- esk.key,
+ key
)
+
console.log('Timelocked ciphertext: ' + JSON.stringify(ct))
```
@@ -96,12 +99,11 @@ Decrypt data using a beacon signature:
``` js
// Acquire a signature for decryption from he pulse output by the beacon at the given roundNumber
-const sigHex =
+ const sig =
'e6cdf6c9d11c13e013b2c6cfd11dab46d8f1ace226ff845ffff4c7d6f64992892c54fb5d1f0f87dd300ce66f53598e01'
-const sig = Uint8Array.from(sigHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))
// Decrypt the ciphertext with the signature
-const plaintext = await timelock.decrypt(ct, sig)
-console.log(`Recovered ${String.fromCharCode(...plaintext)}`)
+const plaintext = await timelockIdeal.decrypt(ct, sig)
+console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`)
```
### Force Decrypting a Message
@@ -114,7 +116,10 @@ const seed = new TextEncoder().encode('password')
const hash = 'SHA-256'
const length = 32
const esk = await hkdf.compute(seed, hash, length, '')
-const plaintext = await timelock.forceDecrypt(ciphertext, esk.key);
+const key = Array.from(esk.key)
+ .map((byte) => byte.toString(16).padStart(2, '0'))
+ .join('')
+const plaintext = await timelock.forceDecrypt(ciphertext, key);
console.log('Plaintext:', plaintext);
```
diff --git a/ts/src/timelock.ts b/ts/src/timelock.ts
index 87bd263..b367158 100644
--- a/ts/src/timelock.ts
+++ b/ts/src/timelock.ts
@@ -108,11 +108,9 @@ export class Timelock {
const beaconPublicKey = u8a(beaconPublicKeyHex)
const ephemeralSecretKey = u8a(ephemeralSecretKeyHex)
const id = await identityBuilder.build(roundNumber)
- return ok(
- new Uint8Array(
- tle(id, encodedMessage, ephemeralSecretKey, beaconPublicKey, this.curveId)
- )
- )
+ const ciphertext = tle(id, encodedMessage, ephemeralSecretKey, beaconPublicKey, this.curveId)
+ const result = new Uint8Array(ciphertext)
+ return ok(result)
} catch (err) {
return error(err.message)
}
From 4d323753899ba7d43cc551eb257f4cc67e3947a0 Mon Sep 17 00:00:00 2001
From: driemworks
Date: Tue, 17 Dec 2024 12:49:58 -0600
Subject: [PATCH 3/6] chore: remove unnecessary function
---
ts/src/interfaces/DrandIdentityBuilder.ts | 22 ++--------------------
1 file changed, 2 insertions(+), 20 deletions(-)
diff --git a/ts/src/interfaces/DrandIdentityBuilder.ts b/ts/src/interfaces/DrandIdentityBuilder.ts
index cc54dd9..23705f6 100644
--- a/ts/src/interfaces/DrandIdentityBuilder.ts
+++ b/ts/src/interfaces/DrandIdentityBuilder.ts
@@ -14,28 +14,10 @@
* limitations under the License.
*/
+import { u8a } from '../timelock';
import { IdentityBuilder } from './IIdentityBuilder'
import { Buffer } from "buffer";
-/**
- * Convert a hex encoded string to a uint8array
- * @param hexString The hex string (e.g. 0xabc123...)
- * @returns A Uint8Array
- */
-function hexToUint8Array(hexString) {
- if (hexString.length % 2 !== 0) {
- throw new Error("Hex string must have an even length.");
- }
-
- const uint8Array = new Uint8Array(hexString.length / 2);
-
- for (let i = 0; i < hexString.length; i += 2) {
- uint8Array[i / 2] = parseInt(hexString.substr(i, 2), 16);
- }
-
- return uint8Array;
-}
-
/**
* Compute the sha256 hash of the data
* @param data: Some Uint8Array
@@ -56,7 +38,7 @@ async function sha256(data: Uint8Array): Promise {
function generateMessage(round: number): Promise {
const buffer = Buffer.alloc(8);
buffer.writeBigUInt64BE(BigInt(round), 0);
- return sha256(buffer).then(result => hexToUint8Array(result))
+ return sha256(buffer).then(result => u8a(result))
}
/**
From 9123c77732745ecca76371758ae7a4a08e1730a3 Mon Sep 17 00:00:00 2001
From: Juan Girini
Date: Wed, 18 Dec 2024 11:59:35 +0100
Subject: [PATCH 4/6] Update wasm/src/js.rs
---
wasm/src/js.rs | 1 -
1 file changed, 1 deletion(-)
diff --git a/wasm/src/js.rs b/wasm/src/js.rs
index 16ab3f5..a6f9185 100644
--- a/wasm/src/js.rs
+++ b/wasm/src/js.rs
@@ -180,7 +180,6 @@ pub fn decrypt(
/// Bypass Tlock by attempting to decrypt the ciphertext with some secret key
/// under the stream cipher only
-// #[wasm_bindgen]
pub fn do_decrypt(
ciphertext_js: JsValue,
sk_vec_js: JsValue,
From 5ad69d3d9cda7138c343a3752c48d7f633d48a48 Mon Sep 17 00:00:00 2001
From: Juan Girini
Date: Wed, 18 Dec 2024 11:59:45 +0100
Subject: [PATCH 5/6] Update wasm/src/js.rs
---
wasm/src/js.rs | 4 ----
1 file changed, 4 deletions(-)
diff --git a/wasm/src/js.rs b/wasm/src/js.rs
index a6f9185..93b2bf9 100644
--- a/wasm/src/js.rs
+++ b/wasm/src/js.rs
@@ -300,12 +300,8 @@ mod test {
let seed = serde_wasm_bindgen::to_value(seed_bytes).unwrap();
let (p_pub, sk) = generate_keys::(seed);
- // let mut sk_bytes: Vec = Vec::new();
- // sk.serialize_compressed(&mut sk_bytes).unwrap();
let mut sk_js: JsValue =
serde_wasm_bindgen::to_value(sk.as_slice()).unwrap();
- // let mut p_pub_bytes: Vec = Vec::new();
- // p_pub.1.serialize_compressed(&mut p_pub_bytes).unwrap();
let p_pub_js: JsValue =
serde_wasm_bindgen::to_value(&p_pub[48..]).unwrap();
From c35b90c25b498a7ba4f280a8134670d70b96b05f Mon Sep 17 00:00:00 2001
From: Juan Girini
Date: Wed, 18 Dec 2024 11:59:52 +0100
Subject: [PATCH 6/6] Update wasm/src/js.rs
---
wasm/src/js.rs | 2 --
1 file changed, 2 deletions(-)
diff --git a/wasm/src/js.rs b/wasm/src/js.rs
index 93b2bf9..93fb970 100644
--- a/wasm/src/js.rs
+++ b/wasm/src/js.rs
@@ -366,8 +366,6 @@ mod test {
beacon.into(),
) {
Ok(ciphertext) => {
- // let sk_js_early: JsValue =
- // serde_wasm_bindgen::to_value(&sk_bytes).unwrap();
let ciphertext_clone = ciphertext.clone();
handler(TestStatusReport::EncryptSuccess { ciphertext });
match decrypt(ciphertext_clone, sk_js, beacon.into()) {