diff --git a/Cargo.toml b/Cargo.toml index 5ea2117..749cfd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,24 +2,28 @@ name = "x25519-chacha20poly1305" version = "0.1.0" edition = "2021" -authors = [ "John Sahhar " ] +authors = ["John Sahhar "] license = "MIT OR Apache-2.0" description = "x25519 & chacha20poly1305" repository = "https://github.com/entropyxyz/x25519-chacha20poly1305" [dependencies] hex = "*" -bip39 ={ git="https://github.com/infincia/bip39-rs.git", tag="v0.6.0-beta.1" } +bip39 = { git = "https://github.com/infincia/bip39-rs.git", tag = "v0.6.0-beta.1" } wasm-bindgen = "0.2.83" -x25519-dalek = { version = "2.0.0-rc.2", features = ["static_secrets"] } -serde ={ version="1.0", features=["derive"] } -serde_json ="1.0" -blake2 ="0.10.4" -chacha20poly1305="0.10.1" -sp-core ="6.0.0" -generic-array ="0.14.6" +x25519-dalek = { version = "2.0.0", features = ["static_secrets"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +blake2 = "0.10.4" +chacha20poly1305 = "0.10.1" +sp-core = "6.0.0" +generic-array = "0.14.6" +rand_core = { version = "0.6.4", features = ["getrandom"] } getrandom = { version = "0.2", features = ["js"] } schnorrkel = "0.9.1" +js-sys = "0.3.64" +thiserror = "1.0.47" +zeroize = "1.5.7" [lib] crate-type = ["cdylib", "rlib"] diff --git a/pkg/README.md b/pkg/README.md index e806851..04cd2b9 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -1,13 +1,49 @@ ### x25519-chacha20poly1305 +--- +**WARNING: This code has not been audited and is not yet suitable for production. Use at your own risk.** + +--- + +> NOTE: make sure you have your C archiver and compiler env vars set, or `secp256k1-sys` will fail to build +> +> e.g. +> `AR=/opt/homebrew/opt/llvm/bin/llvm-ar` +> `CC=/opt/homebrew/opt/llvm/bin/clang` + Nodejs package for x25519 key exchange and chacha20poly1305 encryption, written in Rust compiled to WASM. +## Development process + ### Install dependencies -Requires `wasm-pack` which can be installed with `cargo install wasm-pack`. +See `example/ci-test.sh` for example of how to install both rust and javascript dependencies. ### Compile +Compile a nodejs/wasm library from the Rust source. + ```sh make ``` + +### Link + +Link the nodejs/wasm library locally. + +```sh +make link +``` + +### Test + +After compiling and linking, run: + +```sh +ts-node example/test.ts +``` + +### NPM + +link to package on npm: https://www.npmjs.com/package/x25519 + diff --git a/pkg/x25519_chacha20poly1305.d.ts b/pkg/x25519_chacha20poly1305.d.ts index df443e5..b0c7af7 100644 --- a/pkg/x25519_chacha20poly1305.d.ts +++ b/pkg/x25519_chacha20poly1305.d.ts @@ -1,16 +1,20 @@ /* tslint:disable */ /* eslint-disable */ /** +* Convert a Vec to a hex encoded string * @param {Uint8Array} v * @returns {string} */ export function to_hex(v: Uint8Array): string; /** +* Convert a hex string to a Vec, ignoring 0x prefix * @param {string} v * @returns {Uint8Array} */ export function from_hex(v: string): Uint8Array; /** +* Derives a public DH key from a static DH secret. +* sk must be 64 bytes in length or an error will be returned. * @param {Uint8Array} sk * @returns {Uint8Array} */ diff --git a/pkg/x25519_chacha20poly1305.js b/pkg/x25519_chacha20poly1305.js index 9aff645..c56c6f2 100644 --- a/pkg/x25519_chacha20poly1305.js +++ b/pkg/x25519_chacha20poly1305.js @@ -3,7 +3,7 @@ imports['__wbindgen_placeholder__'] = module.exports; let wasm; const { TextDecoder, TextEncoder } = require(`util`); -const heap = new Array(32).fill(undefined); +const heap = new Array(128).fill(undefined); heap.push(undefined, null, true, false); @@ -12,7 +12,7 @@ function getObject(idx) { return heap[idx]; } let heap_next = heap.length; function dropObject(idx) { - if (idx < 36) return; + if (idx < 132) return; heap[idx] = heap_next; heap_next = idx; } @@ -27,16 +27,17 @@ let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true cachedTextDecoder.decode(); -let cachedUint8Memory0 = new Uint8Array(); +let cachedUint8Memory0 = null; function getUint8Memory0() { - if (cachedUint8Memory0.byteLength === 0) { + if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) { cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); } return cachedUint8Memory0; } function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); } @@ -52,25 +53,28 @@ function addHeapObject(obj) { let WASM_VECTOR_LEN = 0; function passArray8ToWasm0(arg, malloc) { - const ptr = malloc(arg.length * 1); + const ptr = malloc(arg.length * 1, 1) >>> 0; getUint8Memory0().set(arg, ptr / 1); WASM_VECTOR_LEN = arg.length; return ptr; } -let cachedInt32Memory0 = new Int32Array(); +let cachedInt32Memory0 = null; function getInt32Memory0() { - if (cachedInt32Memory0.byteLength === 0) { + if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) { cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); } return cachedInt32Memory0; } /** +* Convert a Vec to a hex encoded string * @param {Uint8Array} v * @returns {string} */ module.exports.to_hex = function(v) { + let deferred2_0; + let deferred2_1; try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); const ptr0 = passArray8ToWasm0(v, wasm.__wbindgen_malloc); @@ -78,10 +82,12 @@ module.exports.to_hex = function(v) { wasm.to_hex(retptr, ptr0, len0); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; + deferred2_0 = r0; + deferred2_1 = r1; return getStringFromWasm0(r0, r1); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_free(r0, r1); + wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); } }; @@ -104,14 +110,14 @@ function passStringToWasm0(arg, malloc, realloc) { if (realloc === undefined) { const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length); + const ptr = malloc(buf.length, 1) >>> 0; getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); WASM_VECTOR_LEN = buf.length; return ptr; } let len = arg.length; - let ptr = malloc(len); + let ptr = malloc(len, 1) >>> 0; const mem = getUint8Memory0(); @@ -127,7 +133,7 @@ function passStringToWasm0(arg, malloc, realloc) { if (offset !== 0) { arg = arg.slice(offset); } - ptr = realloc(ptr, len, len = offset + arg.length * 3); + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; const view = getUint8Memory0().subarray(ptr + offset, ptr + len); const ret = encodeString(arg, view); @@ -139,9 +145,11 @@ function passStringToWasm0(arg, malloc, realloc) { } function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); } /** +* Convert a hex string to a Vec, ignoring 0x prefix * @param {string} v * @returns {Uint8Array} */ @@ -153,15 +161,22 @@ module.exports.from_hex = function(v) { wasm.from_hex(retptr, ptr0, len0); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v1 = getArrayU8FromWasm0(r0, r1).slice(); + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + if (r3) { + throw takeObject(r2); + } + var v2 = getArrayU8FromWasm0(r0, r1).slice(); wasm.__wbindgen_free(r0, r1 * 1); - return v1; + return v2; } finally { wasm.__wbindgen_add_to_stack_pointer(16); } }; /** +* Derives a public DH key from a static DH secret. +* sk must be 64 bytes in length or an error will be returned. * @param {Uint8Array} sk * @returns {Uint8Array} */ @@ -173,9 +188,14 @@ module.exports.public_key_from_secret = function(sk) { wasm.public_key_from_secret(retptr, ptr0, len0); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v1 = getArrayU8FromWasm0(r0, r1).slice(); + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + if (r3) { + throw takeObject(r2); + } + var v2 = getArrayU8FromWasm0(r0, r1).slice(); wasm.__wbindgen_free(r0, r1 * 1); - return v1; + return v2; } finally { wasm.__wbindgen_add_to_stack_pointer(16); } @@ -193,9 +213,14 @@ module.exports.gen_signing_key = function() { wasm.gen_signing_key(retptr); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v0 = getArrayU8FromWasm0(r0, r1).slice(); + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + if (r3) { + throw takeObject(r2); + } + var v1 = getArrayU8FromWasm0(r0, r1).slice(); wasm.__wbindgen_free(r0, r1 * 1); - return v0; + return v1; } finally { wasm.__wbindgen_add_to_stack_pointer(16); } @@ -209,6 +234,8 @@ module.exports.gen_signing_key = function() { * @returns {string} */ module.exports.encrypt_and_sign = function(sk, msg, pk) { + let deferred5_0; + let deferred5_1; try { const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); const ptr0 = passArray8ToWasm0(sk, wasm.__wbindgen_malloc); @@ -220,10 +247,20 @@ module.exports.encrypt_and_sign = function(sk, msg, pk) { wasm.encrypt_and_sign(retptr, ptr0, len0, ptr1, len1, ptr2, len2); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - return getStringFromWasm0(r0, r1); + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + var ptr4 = r0; + var len4 = r1; + if (r3) { + ptr4 = 0; len4 = 0; + throw takeObject(r2); + } + deferred5_0 = ptr4; + deferred5_1 = len4; + return getStringFromWasm0(ptr4, len4); } finally { wasm.__wbindgen_add_to_stack_pointer(16); - wasm.__wbindgen_free(r0, r1); + wasm.__wbindgen_free(deferred5_0, deferred5_1, 1); } }; @@ -244,9 +281,14 @@ module.exports.decrypt_and_verify = function(sk, msg) { wasm.decrypt_and_verify(retptr, ptr0, len0, ptr1, len1); var r0 = getInt32Memory0()[retptr / 4 + 0]; var r1 = getInt32Memory0()[retptr / 4 + 1]; - var v2 = getArrayU8FromWasm0(r0, r1).slice(); + var r2 = getInt32Memory0()[retptr / 4 + 2]; + var r3 = getInt32Memory0()[retptr / 4 + 3]; + if (r3) { + throw takeObject(r2); + } + var v3 = getArrayU8FromWasm0(r0, r1).slice(); wasm.__wbindgen_free(r0, r1 * 1); - return v2; + return v3; } finally { wasm.__wbindgen_add_to_stack_pointer(16); } @@ -279,14 +321,6 @@ module.exports.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; -module.exports.__wbg_randomFillSync_85b3f4c52c56c313 = function(arg0, arg1, arg2) { - getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2)); -}; - -module.exports.__wbg_getRandomValues_cd175915511f705e = function(arg0, arg1) { - getObject(arg0).getRandomValues(getObject(arg1)); -}; - module.exports.__wbg_self_7eede1f4488bf346 = function() { return handleError(function () { const ret = self.self; return addHeapObject(ret); @@ -322,15 +356,15 @@ module.exports.__wbg_getRandomValues_307049345d0bd88c = function(arg0) { return addHeapObject(ret); }; -module.exports.__wbg_randomFillSync_6894564c2c334c42 = function() { return handleError(function (arg0, arg1, arg2) { - getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2)); -}, arguments) }; - -module.exports.__wbg_getRandomValues_805f1c3d65988a5a = function() { return handleError(function (arg0, arg1) { +module.exports.__wbg_getRandomValues_cd175915511f705e = function(arg0, arg1) { getObject(arg0).getRandomValues(getObject(arg1)); -}, arguments) }; +}; -module.exports.__wbg_crypto_e1d53a1d73fb10b8 = function(arg0) { +module.exports.__wbg_randomFillSync_85b3f4c52c56c313 = function(arg0, arg1, arg2) { + getObject(arg0).randomFillSync(getArrayU8FromWasm0(arg1, arg2)); +}; + +module.exports.__wbg_crypto_70a96de3b6b73dac = function(arg0) { const ret = getObject(arg0).crypto; return addHeapObject(ret); }; @@ -341,17 +375,17 @@ module.exports.__wbindgen_is_object = function(arg0) { return ret; }; -module.exports.__wbg_process_038c26bf42b093f8 = function(arg0) { +module.exports.__wbg_process_dd1577445152112e = function(arg0) { const ret = getObject(arg0).process; return addHeapObject(ret); }; -module.exports.__wbg_versions_ab37218d2f0b24a8 = function(arg0) { +module.exports.__wbg_versions_58036bec3add9e6f = function(arg0) { const ret = getObject(arg0).versions; return addHeapObject(ret); }; -module.exports.__wbg_node_080f4b19d15bc1fe = function(arg0) { +module.exports.__wbg_node_6a9d28205ed5b0d8 = function(arg0) { const ret = getObject(arg0).node; return addHeapObject(ret); }; @@ -361,7 +395,12 @@ module.exports.__wbindgen_is_string = function(arg0) { return ret; }; -module.exports.__wbg_require_78a3dcfbdba9cbce = function() { return handleError(function () { +module.exports.__wbg_msCrypto_adbc770ec9eca9c7 = function(arg0) { + const ret = getObject(arg0).msCrypto; + return addHeapObject(ret); +}; + +module.exports.__wbg_require_f05d779769764e82 = function() { return handleError(function () { const ret = module.require; return addHeapObject(ret); }, arguments) }; @@ -376,17 +415,20 @@ module.exports.__wbindgen_string_new = function(arg0, arg1) { return addHeapObject(ret); }; -module.exports.__wbg_msCrypto_6e7d3e1f92610cbb = function(arg0) { - const ret = getObject(arg0).msCrypto; - return addHeapObject(ret); -}; +module.exports.__wbg_getRandomValues_3774744e221a22ad = function() { return handleError(function (arg0, arg1) { + getObject(arg0).getRandomValues(getObject(arg1)); +}, arguments) }; + +module.exports.__wbg_randomFillSync_e950366c42764a07 = function() { return handleError(function (arg0, arg1) { + getObject(arg0).randomFillSync(takeObject(arg1)); +}, arguments) }; -module.exports.__wbg_newnoargs_b5b063fc6c2f0376 = function(arg0, arg1) { +module.exports.__wbg_newnoargs_581967eacc0e2604 = function(arg0, arg1) { const ret = new Function(getStringFromWasm0(arg0, arg1)); return addHeapObject(ret); }; -module.exports.__wbg_call_97ae9d8645dc388b = function() { return handleError(function (arg0, arg1) { +module.exports.__wbg_call_cb65541d95d71282 = function() { return handleError(function (arg0, arg1) { const ret = getObject(arg0).call(getObject(arg1)); return addHeapObject(ret); }, arguments) }; @@ -396,56 +438,66 @@ module.exports.__wbindgen_object_clone_ref = function(arg0) { return addHeapObject(ret); }; -module.exports.__wbg_self_6d479506f72c6a71 = function() { return handleError(function () { +module.exports.__wbg_self_1ff1d729e9aae938 = function() { return handleError(function () { const ret = self.self; return addHeapObject(ret); }, arguments) }; -module.exports.__wbg_window_f2557cc78490aceb = function() { return handleError(function () { +module.exports.__wbg_window_5f4faef6c12b79ec = function() { return handleError(function () { const ret = window.window; return addHeapObject(ret); }, arguments) }; -module.exports.__wbg_globalThis_7f206bda628d5286 = function() { return handleError(function () { +module.exports.__wbg_globalThis_1d39714405582d3c = function() { return handleError(function () { const ret = globalThis.globalThis; return addHeapObject(ret); }, arguments) }; -module.exports.__wbg_global_ba75c50d1cf384f4 = function() { return handleError(function () { +module.exports.__wbg_global_651f05c6a0944d1c = function() { return handleError(function () { const ret = global.global; return addHeapObject(ret); }, arguments) }; -module.exports.__wbg_call_168da88779e35f61 = function() { return handleError(function (arg0, arg1, arg2) { +module.exports.__wbg_new_d258248ed531ff54 = function(arg0, arg1) { + const ret = new Error(getStringFromWasm0(arg0, arg1)); + return addHeapObject(ret); +}; + +module.exports.__wbg_call_01734de55d61e11d = function() { return handleError(function (arg0, arg1, arg2) { const ret = getObject(arg0).call(getObject(arg1), getObject(arg2)); return addHeapObject(ret); }, arguments) }; -module.exports.__wbg_buffer_3f3d764d4747d564 = function(arg0) { +module.exports.__wbg_buffer_085ec1f694018c4f = function(arg0) { const ret = getObject(arg0).buffer; return addHeapObject(ret); }; -module.exports.__wbg_new_8c3f0052272a457a = function(arg0) { +module.exports.__wbg_newwithbyteoffsetandlength_6da8e527659b86aa = function(arg0, arg1, arg2) { + const ret = new Uint8Array(getObject(arg0), arg1 >>> 0, arg2 >>> 0); + return addHeapObject(ret); +}; + +module.exports.__wbg_new_8125e318e6245eed = function(arg0) { const ret = new Uint8Array(getObject(arg0)); return addHeapObject(ret); }; -module.exports.__wbg_set_83db9690f9353e79 = function(arg0, arg1, arg2) { +module.exports.__wbg_set_5cf90238115182c3 = function(arg0, arg1, arg2) { getObject(arg0).set(getObject(arg1), arg2 >>> 0); }; -module.exports.__wbg_length_9e1ae1900cb0fbd5 = function(arg0) { +module.exports.__wbg_length_72e2208bbc0efc61 = function(arg0) { const ret = getObject(arg0).length; return ret; }; -module.exports.__wbg_newwithlength_f5933855e4f48a19 = function(arg0) { +module.exports.__wbg_newwithlength_e5d69174d6984cd7 = function(arg0) { const ret = new Uint8Array(arg0 >>> 0); return addHeapObject(ret); }; -module.exports.__wbg_subarray_58ad4efbb5bcb886 = function(arg0, arg1, arg2) { +module.exports.__wbg_subarray_13db269f57aa838d = function(arg0, arg1, arg2) { const ret = getObject(arg0).subarray(arg1 >>> 0, arg2 >>> 0); return addHeapObject(ret); }; diff --git a/pkg/x25519_chacha20poly1305_bg.wasm b/pkg/x25519_chacha20poly1305_bg.wasm index 5501750..7a14361 100644 Binary files a/pkg/x25519_chacha20poly1305_bg.wasm and b/pkg/x25519_chacha20poly1305_bg.wasm differ diff --git a/pkg/x25519_chacha20poly1305_bg.wasm.d.ts b/pkg/x25519_chacha20poly1305_bg.wasm.d.ts index e8ec872..34758ab 100644 --- a/pkg/x25519_chacha20poly1305_bg.wasm.d.ts +++ b/pkg/x25519_chacha20poly1305_bg.wasm.d.ts @@ -13,7 +13,7 @@ export function rustsecp256k1_v0_4_1_context_destroy(a: number): void; export function rustsecp256k1_v0_4_1_default_illegal_callback_fn(a: number, b: number): void; export function rustsecp256k1_v0_4_1_default_error_callback_fn(a: number, b: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; -export function __wbindgen_malloc(a: number): number; -export function __wbindgen_free(a: number, b: number): void; -export function __wbindgen_realloc(a: number, b: number, c: number): number; +export function __wbindgen_malloc(a: number, b: number): number; +export function __wbindgen_free(a: number, b: number, c: number): void; +export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; export function __wbindgen_exn_store(a: number): void; diff --git a/src/lib.rs b/src/lib.rs index f67e233..eeef960 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,27 +1,32 @@ use bip39::Mnemonic; use blake2::{Blake2s256, Digest}; use chacha20poly1305::{ - aead::{Aead, Error, KeyInit}, + aead::{Aead, AeadCore, KeyInit}, ChaCha20Poly1305, }; -use generic_array::GenericArray; use hex; +use js_sys::Error; +use rand_core::OsRng; use schnorrkel::{MiniSecretKey, SecretKey}; use serde::{Deserialize, Serialize}; use serde_json::to_string; use sp_core::{crypto::AccountId32, sr25519, sr25519::Signature, Bytes, Pair}; +use thiserror::Error; use wasm_bindgen::prelude::*; use x25519_dalek::{PublicKey, StaticSecret}; +use zeroize::Zeroize; const HEX_PREFIX: [u8; 2] = [48, 120]; +/// Convert a Vec to a hex encoded string #[wasm_bindgen] pub fn to_hex(v: Vec) -> String { hex::encode(v) } +/// Convert a hex string to a Vec, ignoring 0x prefix #[wasm_bindgen] -pub fn from_hex(v: String) -> Vec { +pub fn from_hex(v: String) -> Result, Error> { let mut to_decode: String = v; if to_decode.len() >= 2 { let prefix = to_decode[0..2].as_bytes(); @@ -29,44 +34,46 @@ pub fn from_hex(v: String) -> Vec { to_decode = to_decode[2..].to_string(); } } - hex::decode(to_decode).unwrap() + Ok(hex::decode(to_decode).map_err(|err| Error::new(&err.to_string()))?) } #[wasm_bindgen] -// Derives a public DH key from a static DH secret. -// sk must be 64 bytes in length or an empty array will be returned. -// TODO: find better interface in WASM for throwing errors from rust to wasm. -pub fn public_key_from_secret(sk: Vec) -> Vec { +/// Derives a public DH key from a static DH secret. +/// sk must be 64 bytes in length or an error will be returned. +pub fn public_key_from_secret(sk: Vec) -> Result, Error> { if sk.len() != 64 { - return Vec::::new(); + return Err(Error::new("Secret key must be 64 bytes")); } - let sec_key = SecretKey::from_ed25519_bytes(sk.as_slice()).unwrap(); + let sec_key = + SecretKey::from_bytes(sk.as_slice()).map_err(|err| Error::new(&err.to_string()))?; let pair = sr25519::Pair::from(sec_key); let ss = derive_static_secret(&pair); - PublicKey::from(&ss).as_bytes().to_vec() + Ok(PublicKey::from(&ss).as_bytes().to_vec()) } -pub fn gen_msg_nonce() -> Vec { +// TODO i don't think this is needed as nonce generation is done internally +/// Generate a 12 byte random nonce +pub fn gen_msg_nonce() -> Result, Error> { let mut vec: Vec = vec![0; 12]; - getrandom::getrandom(&mut vec).unwrap(); - return vec; + getrandom::getrandom(&mut vec).map_err(|err| Error::new(&err.to_string()))?; + return Ok(vec); } #[wasm_bindgen] /// Generates a Ristretto Schnorr secret key. /// This method is used for testing, applications that implement this /// library should rely on user provided keys generated from substrate. -pub fn gen_signing_key() -> Vec { +pub fn gen_signing_key() -> Result, Error> { let mini_secret_key = MiniSecretKey::generate(); let secret_key: SecretKey = mini_secret_key.expand(MiniSecretKey::ED25519_MODE); let _sk: [u8; 64] = secret_key.to_bytes(); - let sk = SecretKey::from_bytes(&_sk).unwrap(); - sk.to_bytes().to_vec() + let sk = SecretKey::from_bytes(&_sk).map_err(|err| Error::new(&err.to_string()))?; + Ok(sk.to_bytes().to_vec()) } #[wasm_bindgen] /// Encrypts, signs, and serializes a SignedMessage to JSON. -pub fn encrypt_and_sign(sk: Vec, msg: Vec, pk: Vec) -> String { +pub fn encrypt_and_sign(sk: Vec, msg: Vec, pk: Vec) -> Result { let mut _raw_pk: [u8; 32] = [0; 32]; _raw_pk.copy_from_slice(&pk[0..32]); let _pk = PublicKey::from(_raw_pk); @@ -75,55 +82,36 @@ pub fn encrypt_and_sign(sk: Vec, msg: Vec, pk: Vec) -> String { let mut sk_buff: [u8; 64] = [0; 64]; sk_buff.copy_from_slice(&sk[0..64]); if sk.len() != 64 { - return "bad key length".to_string(); + return Err(Error::new("Secret key must be 64 bytes")); } - let sec_key = SecretKey::from_ed25519_bytes(sk.as_slice()); - match sec_key { - Err(v) => { - return v.to_string(); - } - Ok(v) => { - let pair = sr25519::Pair::from(v); - let sm = SignedMessage::new(&pair, &_msg, &_pk).unwrap(); - return sm.to_json(); - } - } + let sec_key = + SecretKey::from_bytes(sk.as_slice()).map_err(|err| Error::new(&err.to_string()))?; + let pair = sr25519::Pair::from(sec_key); + let signed_message = + SignedMessage::new(&pair, &_msg, &_pk).map_err(|err| Error::new(&err.to_string()))?; + Ok(signed_message + .to_json() + .map_err(|err| Error::new(&err.to_string()))?) } #[wasm_bindgen] /// Deserializes, verifies and decrypts a json encoded `SignedMessage`. /// Returns the plaintext. -pub fn decrypt_and_verify(sk: Vec, msg: String) -> Vec { - let _sm = serde_json::from_str(msg.as_str()); - if _sm.is_err() { - return "error deserializing".to_string().as_bytes().to_vec(); - } - - let sm: SignedMessage = _sm.unwrap(); +pub fn decrypt_and_verify(sk: Vec, msg: String) -> Result, Error> { + let sm: SignedMessage = + serde_json::from_str(msg.as_str()).map_err(|err| Error::new(&err.to_string()))?; if !sm.verify() { - return "failed to verify signature".to_string().as_bytes().to_vec(); + return Err(Error::new("Failed to verify signature")); } - let sec_key = SecretKey::from_ed25519_bytes(sk.as_slice()); - match sec_key { - Err(v) => { - return v.to_string().as_bytes().to_vec(); - } - Ok(v) => { - let pair = sr25519::Pair::from(v); - let res = sm.decrypt(&pair); - match res { - Err(_v) => { - return "failed".to_string().as_bytes().to_vec(); - } - Ok(v) => { - return v.clone(); - } - } - } - } + let sec_key = + SecretKey::from_bytes(sk.as_slice()).map_err(|err| Error::new(&err.to_string()))?; + let pair = sr25519::Pair::from(sec_key); + Ok(sm + .decrypt(&pair) + .map_err(|err| Error::new(&err.to_string()))?) } /// Constant time not-equal compare for two equal sized byte vectors. @@ -143,6 +131,7 @@ pub fn constant_time_eq(a: Vec, b: Vec) -> bool { a.len() == b.len() && constant_time_ne(&a, &b) == 0 } +/// Given a sr25519 secret signing key, generate an x25519 secret encryption key pub fn derive_static_secret(sk: &sr25519::Pair) -> StaticSecret { let mut buffer: [u8; 32] = [0; 32]; let mut hasher = Blake2s256::new(); @@ -150,6 +139,7 @@ pub fn derive_static_secret(sk: &sr25519::Pair) -> StaticSecret { let hash = hasher.finalize().to_vec(); buffer.copy_from_slice(&hash); let result = StaticSecret::from(buffer); + buffer.zeroize(); result } @@ -179,15 +169,23 @@ impl SignedMessage { /// via Diffie-Hellman for encryption. /// msg is the plaintext message to encrypt and sign /// recip is the public Diffie-Hellman parameter of the recipient. - pub fn new(sk: &sr25519::Pair, msg: &Bytes, recip: &PublicKey) -> Result { - let s = derive_static_secret(sk); - let a = PublicKey::from(&s); + pub fn new( + sk: &sr25519::Pair, + msg: &Bytes, + recip: &PublicKey, + ) -> Result { + let mut s = derive_static_secret(sk); + let a = x25519_dalek::PublicKey::from(&s); let shared_secret = s.diffie_hellman(recip); + s.zeroize(); + let msg_nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng); // 96-bits; unique per message + let cipher = ChaCha20Poly1305::new_from_slice(shared_secret.as_bytes()) + .map_err(|e| ValidationErr::Conversion(e.to_string()))?; + let ciphertext = cipher + .encrypt(&msg_nonce, msg.0.as_slice()) + .map_err(|e| ValidationErr::Encryption(e.to_string()))?; let mut static_nonce: [u8; 12] = [0; 12]; - getrandom::getrandom(&mut static_nonce).unwrap(); - let nonce = GenericArray::from_slice(&static_nonce); - let cipher = ChaCha20Poly1305::new_from_slice(shared_secret.as_bytes()).unwrap(); - let ciphertext = cipher.encrypt(nonce, msg.0.as_slice())?; + static_nonce.copy_from_slice(&msg_nonce); let mut hasher = Blake2s256::new(); hasher.update(&ciphertext); @@ -204,17 +202,18 @@ impl SignedMessage { } /// Decrypts the message and returns the plaintext. - pub fn decrypt(&self, sk: &sr25519::Pair) -> Result, Error> { - if !self.verify() { - return Err(Error); - } - let static_secret = derive_static_secret(sk); + pub fn decrypt(&self, sk: &sr25519::Pair) -> Result, ValidationErr> { + let mut static_secret = derive_static_secret(sk); let shared_secret = static_secret.diffie_hellman(&PublicKey::from(self.a)); - let cipher = ChaCha20Poly1305::new_from_slice(shared_secret.as_bytes()).unwrap(); - cipher.decrypt( - &generic_array::GenericArray::from(self.nonce), - self.msg.0.as_slice(), - ) + static_secret.zeroize(); + let cipher = ChaCha20Poly1305::new_from_slice(shared_secret.as_bytes()) + .map_err(|e| ValidationErr::Conversion(e.to_string()))? + .decrypt( + &generic_array::GenericArray::from(self.nonce), + self.msg.0.as_slice(), + ) + .map_err(|e| ValidationErr::Decryption(e.to_string()))?; + Ok(cipher) } /// Returns the AccountId32 of the message signer. @@ -222,7 +221,12 @@ impl SignedMessage { AccountId32::new(self.pk) } - /// Returns the public key of the message signer. + /// Returns the public DH parameter of the message sender. + pub fn sender(&self) -> x25519_dalek::PublicKey { + x25519_dalek::PublicKey::from(self.a) + } + + /// Returns the sr25519 public key of the message signer. pub fn pk(&self) -> sr25519::Public { sr25519::Public::from_raw(self.pk) } @@ -243,8 +247,8 @@ impl SignedMessage { } /// Returns a serialized json string of self. - pub fn to_json(&self) -> String { - to_string(self).unwrap() + pub fn to_json(&self) -> Result { + Ok(to_string(self)?) } } @@ -254,10 +258,28 @@ pub fn new_mnemonic() -> Mnemonic { } /// Derives a sr25519::Pair from a Mnemonic -pub fn mnemonic_to_pair(m: &Mnemonic) -> sr25519::Pair { - ::from_phrase(m.phrase(), None) - .unwrap() - .0 +pub fn mnemonic_to_pair(m: &Mnemonic) -> Result { + Ok(::from_phrase(m.phrase(), None) + .map_err(|_| ValidationErr::SecretString("Secret String Error"))? + .0) +} + +#[derive(Debug, Error)] +pub enum ValidationErr { + #[error("ChaCha20 decryption error: {0}")] + Decryption(String), + #[error("ChaCha20 Encryption error: {0}")] + Encryption(String), + #[error("ChaCha20 Conversion error: {0}")] + Conversion(String), + #[error("Secret String failure: {0:?}")] + SecretString(&'static str), + #[error("Message is too old")] + StaleMessage, + #[error("Time subtraction error: {0}")] + SystemTime(#[from] std::time::SystemTimeError), + #[error("JSON serialization error: {0}")] + Json(#[from] serde_json::Error), } #[cfg(test)] @@ -268,11 +290,11 @@ mod tests { fn test_bad_signatures_fails() { let plaintext = Bytes(vec![69, 42, 0]); - let alice = mnemonic_to_pair(&new_mnemonic()); + let alice = mnemonic_to_pair(&new_mnemonic()).unwrap(); let alice_secret = derive_static_secret(&alice); let alice_public_key = PublicKey::from(&alice_secret); - let bob = mnemonic_to_pair(&new_mnemonic()); + let bob = mnemonic_to_pair(&new_mnemonic()).unwrap(); let bob_secret = derive_static_secret(&bob); let bob_public_key = PublicKey::from(&bob_secret); @@ -292,10 +314,10 @@ mod tests { fn test_sign_and_encrypt() { let plaintext = Bytes(vec![69, 42, 0]); - let alice = mnemonic_to_pair(&new_mnemonic()); - let alice_secret = derive_static_secret(&alice); + let alice = mnemonic_to_pair(&new_mnemonic()).unwrap(); + let _alice_secret = derive_static_secret(&alice); - let bob = mnemonic_to_pair(&new_mnemonic()); + let bob = mnemonic_to_pair(&new_mnemonic()).unwrap(); let bob_secret = derive_static_secret(&bob); let bob_public_key = PublicKey::from(&bob_secret); @@ -320,4 +342,33 @@ mod tests { // Check the encrypted message != the plaintext. assert_ne!(encrypted_message.msg, plaintext); } + + /// This checks that a SignedMessage created with SignedMessage::new and one created with + /// encrypt_and_sign both have the same public key + #[test] + fn test_keypair_parsing() { + let plaintext_vec = vec![68, 42, 0]; + + let alice = mnemonic_to_pair(&new_mnemonic()).unwrap(); + let _alice_secret = derive_static_secret(&alice); + + let bob = mnemonic_to_pair(&new_mnemonic()).unwrap(); + let bob_secret = derive_static_secret(&bob); + let bob_public_key = PublicKey::from(&bob_secret); + + // Test encryption & signing. + let signed_message = + SignedMessage::new(&alice, &Bytes(plaintext_vec.clone()), &bob_public_key).unwrap(); + + let signed_message_wasm_json = encrypt_and_sign( + alice.to_raw_vec(), + plaintext_vec, + bob_public_key.to_bytes().to_vec(), + ) + .unwrap(); + + let signed_message_wasm: SignedMessage = + serde_json::from_str(&signed_message_wasm_json).unwrap(); + assert_eq!(signed_message_wasm.pk, signed_message.pk); + } }