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 e9724e3..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": { - "@ideallabs/timelock.js": "^1.0.0", "@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/examples/web/react-tlock-demo/src/App.js b/examples/web/react-tlock-demo/src/App.js new file mode 100644 index 0000000..7157a4b --- /dev/null +++ b/examples/web/react-tlock-demo/src/App.js @@ -0,0 +1,134 @@ +/* + * 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 './App.css' +import React, { useEffect, useState } from 'react' +import { Timelock, IdealNetworkIdentityBuilder, DrandIdentityBuilder, SupportedCurve, u8a } from '@ideallabs/timelock.js' +import hkdf from 'js-crypto-hkdf' + +function App() { + + const [timelockDrand, setTimelockDrand] = useState(null) + const [timelockIdeal, setTimelockIdeal] = useState(null) + + useEffect(() => { + Timelock.build(SupportedCurve.BLS12_381).then((tlock) => { + setTimelockDrand(tlock) + }) + + Timelock.build(SupportedCurve.BLS12_377).then((tlock) => { + setTimelockIdeal(tlock) + }) + + }, []) + + const fromHexString = (hexString) => + Uint8Array.from( + hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)) + ) + + // 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') + 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 pubkey = + '83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a' + // A future round number of the randomness beacon + const roundNumber = 1000 + // 2. Encrypt the message + let ct = await timelockDrand.encrypt( + encodedMessage, + roundNumber, + DrandIdentityBuilder, + pubkey, + 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' + // Decrypt the ciphertext with the signature + const plaintext = await timelockDrand.decrypt(ct, sigHex) + // console.log(plaintext) + 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, '') + 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 pubkey = + '41dc53da3d3617a189c85c8cb51a5f4fdfcebda05c50e81595f69e178d240fce3acdafd97b5fd204553e685836393a00b112f5cd78477d79ac8094c608d35bb42bd5091c5bbedd881e2ee0e8492a4361c69bf15250d75aee44035bc5b7553100' + // A future round number of the randomness beacon + const roundNumber = 10 + + // 2. Encrypt the message + let ct = await timelockIdeal.encrypt( + encodedMessage, + roundNumber, + IdealNetworkIdentityBuilder, + pubkey, + 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 sig = + 'e6cdf6c9d11c13e013b2c6cfd11dab46d8f1ace226ff845ffff4c7d6f64992892c54fb5d1f0f87dd300ce66f53598e01' + // Decrypt the ciphertext with the signature + const plaintext = await timelockIdeal.decrypt(ct, sig) + console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`) + } + + return ( +
+

Timelock Encryption Demo

+

+ Open the developer console (F12) and then click the button below to + execute the demo. +

+ + +
+ ) +} + +export default App 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..6eeabd3 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,66 +10,100 @@ 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 -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)) ``` +#### 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: ``` 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 @@ -82,26 +116,13 @@ 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); ``` -## 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/App.js b/ts/examples/react-tlock-demo/src/App.js deleted file mode 100644 index 6a3afa9..0000000 --- a/ts/examples/react-tlock-demo/src/App.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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 './App.css' -import React, { useEffect, useState } from 'react' -import { Timelock, IdealNetworkIdentityBuilder } from '@driemworks/timelock.js' -import hkdf from 'js-crypto-hkdf' - -function App() { - const [timelock, setTimelock] = useState(null) - - useEffect(() => { - Timelock.build().then((tlock) => { - setTimelock(tlock) - console.log('timelock wasm ready') - }) - }, []) - - const fromHexString = (hexString) => - Uint8Array.from( - hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)) - ) - - const runDemo = 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' - const pubkey = fromHexString(pkHex) - // A future round number of the randomness beacon - const roundNumber = 10 - - // 2. Encrypt the message - let ct = await timelock.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 - const sigHex = - 'e6cdf6c9d11c13e013b2c6cfd11dab46d8f1ace226ff845ffff4c7d6f64992892c54fb5d1f0f87dd300ce66f53598e01' - const sig = fromHexString(sigHex) - // Decrypt the ciphertext with the signature - const plaintext = await timelock.decrypt(ct, sig) - console.log(`Recovered ${String.fromCharCode(...plaintext)}, Expected ${message}`) - } - - return ( -
-

Timelock Encryption Demo

-

- Open the developer console (F12) and then click the button below to - execute the demo. -

- -
- ) -} - -export default App 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..23705f6 --- /dev/null +++ b/ts/src/interfaces/DrandIdentityBuilder.ts @@ -0,0 +1,49 @@ +/* + * 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 { u8a } from '../timelock'; +import { IdentityBuilder } from './IIdentityBuilder' +import { Buffer } from "buffer"; + +/** + * 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 => u8a(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..b367158 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,28 @@ 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) + const ciphertext = tle(id, encodedMessage, ephemeralSecretKey, beaconPublicKey, this.curveId) + const result = new Uint8Array(ciphertext) + return ok(result) + } catch (err) { + return error(err.message) + } } /** @@ -87,10 +125,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 +145,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..93fb970 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,72 @@ 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 +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 +199,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 +221,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 +245,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 +293,17 @@ 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_js: JsValue = + serde_wasm_bindgen::to_value(sk.as_slice()).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 +327,86 @@ 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 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 +427,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 +463,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"), - } - }, - ) - } }