Skip to content
This repository has been archived by the owner on Feb 26, 2020. It is now read-only.

Introduce new JS Api #4

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "schnorrkel-js"
version = "0.1.2"
version = "0.1.3"
authors = ["kianenigma <[email protected]>"]
edition = "2018"

Expand Down
5 changes: 2 additions & 3 deletions node-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ function hexlify(arr) {
let hex = ""
for (let byte of arr) {
let _byte = byte.toString(16).padStart(2, "0")
console.log(byte, _byte) ;
hex += _byte
}
return "0x" + hex
Expand All @@ -24,13 +23,13 @@ String.prototype.getBytes = function () {
return bytes;
};

let schnorrkel = require('./schnorrkel_js')
let schnorrkel = require('./pkg/schnorrkel.node')
let msg = "SUBSTRATE".getBytes()
let seed = new Uint8Array(32)
let kp = schnorrkel.keypair_from_seed(seed)
let secret = kp.slice(0, 64)
let public = kp.slice(64, 96)
let sig = schnorrkel.sign(public, secret, msg)
let sig = schnorrkel.sign(secret, msg)
console.log(`++ used seed ${hexlify(seed)}`)
console.log(`++ public => ${hexlify(public)}`)
console.log(`++ secret => ${hexlify(secret)}`)
Expand Down
16 changes: 7 additions & 9 deletions pack-node.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
#!/usr/bin/env node

console.log('+++ Fixing nodejs imports.\n');

const fs = require('fs');
const buffer = fs.readFileSync('./pkg/schnorrkel_js_bg.wasm');

fs.writeFileSync('./pkg/schnorrkel_js_bg.js', `
const imports = {};
imports['./schnorrkel_js'] = require('./schnorrkel_js');
const bytes = Buffer.from('${buffer.toString('base64')}', 'base64');
console.log('\t+++ Fixing node imports.');
fs.writeFileSync('./pkg/schnorrkel.node.bg.js', `
const path = require('path').join(__dirname, 'schnorrkel_js_bg.wasm');
const bytes = require('fs').readFileSync(path);
let imports = {};
imports['./schnorrkel_js'] = require('./schnorrkel.node');

const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);

module.exports = wasmInstance.exports;
`);
15 changes: 15 additions & 0 deletions pack-wp-raw.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
console.log('\t+++ Fixing webpack imports.');
const fs = require('fs');
const buffer = fs.readFileSync('./pkg/schnorrkel_js_bg.wasm');

fs.writeFileSync('./pkg/schnorrkel.wp.bg.js', `
const imports = {};
imports['./schnorrkel_js'] = require('./schnorrkel.wp');

const bytes = Buffer.from('${buffer.toString('base64')}', 'base64');

const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
module.exports = wasmInstance.exports;
`);
11 changes: 11 additions & 0 deletions pack-wp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
console.log('\t+++ Fixing webpack imports.');
const fs = require('fs');

fs.writeFileSync('./pkg/schnorrkel.wp.bg.js', `const imports = {};
imports['./schnorrkel_js'] = require('./schnorrkel.wp');
WebAssembly.instantiateStreaming(fetch('./schnorrkel_js_bg.wasm'), imports)
.then(results => {
console.info('++ schnorrkel-js wasm blob loaded.')
module.exports = results.instance.exports
});`);
31 changes: 31 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@parity/schnorrkel-js",
"description": "a wasm port of the schnorrkel crypto library.",
"repository": {
"type": "git",
"url": "git+https://github.com/paritytech/schnorrkel-js"
},
"author": "Parity Technologies <[email protected]> (https://parity.io/)",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/paritytech/schnorrkel-js/issues"
},
"homepage": "https://github.com/paritytech/schnorrkel-js#readme",

"collaborators": [
"kianenigma <[email protected]>"
],
"version": "0.1.5",
"files": [
"schnorrkel_js_bg.wasm",

"schnorrkel.wp.js",
"schnorrkel.node.js",

"schnorrkel_js_bg.js",
"schnorrkel_js.d.ts"
],
"module": "schnorrkel.wp.js",
"types": "schnorrkel_js.d.ts",
"sideEffects": "false"
}
28 changes: 22 additions & 6 deletions pre-publish.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
#!/usr/bin/env bash

# Compile the latest version; update ./pkg and readme files in it.
wasm-pack build --target nodejs
wasm-pack build --target nodejs --release
mv ./pkg/schnorrkel_js.js ./pkg/schnorrkel.node.js
mv ./pkg/schnorrkel_js_bg.js ./pkg/schnorrkel.node.bg.js
sed -i -e 's/schnorrkel_js_bg/schnorrkel.node.bg/g' ./pkg/schnorrkel.node.js
./pack-node.sh

# Fix the name
sed -i -e 's/schnorrkel-js/@parity\/schnorrkel-js/g' pkg/package.json
wasm-pack build --target browser --release
mv ./pkg/schnorrkel_js.js ./pkg/schnorrkel.wp.js
sed -i -e 's/schnorrkel_js_bg/schnorrkel.wp.bg.js/g' ./pkg/schnorrkel.wp.js
./pack-wp.sh
# will most likely cause an error saying wasm blobl is larger than 4KB
# ./pack-wp-raw.sh

# Run the script to fix node/browser import support
./pack-node.sh
# Replace package.json file.
cp ./package.json ./pkg/package.json

# publish
# wasm-pack publish
# wasm-pack publish

# replace this at the top of the .wp.js file
# import get_wasm from './schnorrkel.wp.bg.js';
# var wasm = get_wasm()
# // this line is a gamble...
# setTimeout(() => wasm = get_wasm(), 500)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller needs to wait for the actual promise result to ensure it has been added.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed but then the export functions will be broken (export can only be used in the top level code.). Webpack's async import() function should be a proper fix for this I believe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will defintaly not merge this branch until at least this ugly thing is gone but it is good to have it pushed in here so that it can be tested without the need to place this horrble piece of code in npm :D

# setTimeout(() => wasm = get_wasm(), 1000)
# setTimeout(() => wasm = get_wasm(), 1500)
53 changes: 40 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;

extern crate schnorrkel;
extern crate sha2;

mod wrapper;
use wrapper::*;
Expand All @@ -17,8 +18,11 @@ use wrapper::*;
///
/// * returned vector is the signature consisting of 64 bytes.
#[wasm_bindgen]
pub fn sign(public: &[u8], private: &[u8], message: &[u8]) -> Vec<u8> {
__sign(public, private, message).to_vec()
pub fn sign(secret: &[u8], message: &[u8]) -> Result<Vec<u8>, JsValue> {
match __sign(secret, message) {
Ok(some_sig) => Ok(some_sig.to_vec()),
Err(err) => Err(JsValue::from_str(&format!("{}", err)))
}
}

/// Verify a message and its corresponding against a public key;
Expand All @@ -31,14 +35,25 @@ pub fn verify(signature: &[u8], message: &[u8], pubkey: &[u8]) -> bool {
__verify(signature, message, pubkey)
}

#[wasm_bindgen]
pub fn expand_to_public(secret: &[u8]) -> Result<Vec<u8>, JsValue> {
match __expand_to_public(secret) {
Ok(some_public) => Ok(some_public.to_vec()),
Err(err) => Err(JsValue::from_str(&format!("{}", err)))
}
}

/// Generate a secret key (aka. private key) from a seed phrase.
///
/// * seed: UIntArray with 32 element
///
/// returned vector is the private key consisting of 64 bytes.
#[wasm_bindgen]
pub fn secret_from_seed(seed: &[u8]) -> Vec<u8> {
__secret_from_seed(seed).to_vec()
pub fn secret_from_seed(seed: &[u8]) -> Result<Vec<u8>, JsValue> {
match __secret_from_seed(seed) {
Ok(some_seed) => Ok(some_seed.to_vec()),
Err(err) => Err(JsValue::from_str(&format!("{}", err)))
}
}

/// Generate a key pair. .
Expand All @@ -48,8 +63,11 @@ pub fn secret_from_seed(seed: &[u8]) -> Vec<u8> {
/// returned vector is the concatenation of first the private key (64 bytes)
/// followed by the public key (32) bytes.
#[wasm_bindgen]
pub fn keypair_from_seed(seed: &[u8]) -> Vec<u8> {
__keypair_from_seed(seed).to_vec()
pub fn keypair_from_seed(seed: &[u8]) -> Result<Vec<u8>, JsValue> {
match __keypair_from_seed(seed) {
Ok(some_kp) => Ok(some_kp.to_vec()),
Err(err) => Err(JsValue::from_str(&format!("{}", err)))
}
}

#[cfg(test)]
Expand All @@ -73,36 +91,45 @@ pub mod tests {
#[wasm_bindgen_test]
fn can_create_keypair() {
let seed = generate_random_seed();
let keypair = keypair_from_seed(seed.as_slice());
let keypair = keypair_from_seed(seed.as_slice()).unwrap();
assert!(keypair.len() == KEYPAIR_LENGTH);
}

#[wasm_bindgen_test]
fn can_create_secret() {
let seed = generate_random_seed();
let secret = secret_from_seed(seed.as_slice());
let secret = secret_from_seed(seed.as_slice()).unwrap();
assert!(secret.len() == SECRET_KEY_LENGTH);
}

#[wasm_bindgen_test]
fn can_sign_message() {
let seed = generate_random_seed();
let keypair = keypair_from_seed(seed.as_slice());
let keypair = keypair_from_seed(seed.as_slice()).unwrap();
let private = &keypair[0..SECRET_KEY_LENGTH];
let public = &keypair[SECRET_KEY_LENGTH..KEYPAIR_LENGTH];
let message = b"this is a message";
let signature = sign(public, private, message);
let signature = sign(private, message).unwrap();
assert!(signature.len() == SIGNATURE_LENGTH);
}

#[wasm_bindgen_test]
fn can_verify_message() {
let seed = generate_random_seed();
let keypair = keypair_from_seed(seed.as_slice());
let keypair = keypair_from_seed(seed.as_slice()).unwrap();
let private = &keypair[0..SECRET_KEY_LENGTH];
let public = &keypair[SECRET_KEY_LENGTH..KEYPAIR_LENGTH];
let message = b"this is a message";
let signature = sign(public, private, message);
let signature = sign(private, message).unwrap();
assert!(verify(&signature[..], message, public));
}

#[wasm_bindgen_test]
fn can_extract_public_from_secret() {
let seed = generate_random_seed();
let keypair = keypair_from_seed(seed.as_ref()).unwrap();
let private = &keypair[0..SECRET_KEY_LENGTH];
let known_public = &keypair[SECRET_KEY_LENGTH..KEYPAIR_LENGTH];
let inferred_public = expand_to_public(private).unwrap();
assert!(known_public.to_vec() == inferred_public);
}
}
72 changes: 38 additions & 34 deletions src/wrapper.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@

use schnorrkel::keys::*;
use schnorrkel::context::{signing_context};
use schnorrkel::sign::{Signature,SIGNATURE_LENGTH};
use schnorrkel::SignatureError;

use sha2::Sha512;

// We must make sure that this is the same as declared in the substrate source code.
const SIGNING_CTX: &'static [u8] = b"substrate transaction";

/// Private helper function.
fn keypair_from_seed(seed: &[u8]) -> Keypair {
let mini_key: MiniSecretKey = MiniSecretKey::from_bytes(seed)
.expect("32 bytes can always build a key; qed");
mini_key.expand_to_keypair::<Sha512>()
}

pub fn __keypair_from_seed(seed: &[u8]) -> [u8; KEYPAIR_LENGTH] {
let keypair = keypair_from_seed(seed).to_bytes();
let mut kp = [0u8; KEYPAIR_LENGTH];
kp.copy_from_slice(&keypair);
kp
}

pub fn __sign(secret: &[u8], message: &[u8]) -> Result<[u8; SIGNATURE_LENGTH], SignatureError> {
let secret_key = SecretKey::from_bytes(secret)?;
// TODO: This can be used once the new version of schnorrkel is out on crates.io
// let kp = secret_key.to_keypair();
// let context = signing_context(SIGNING_CTX);
// Ok(kp.sign(context.bytes(message)).to_bytes())

// Temporary replacement method:
let public = secret_key.to_public();
let kp = Keypair {
public: public,
secret: secret_key
};

pub fn __secret_from_seed(seed: &[u8]) -> [u8; SECRET_KEY_LENGTH] {
let secret = keypair_from_seed(seed).secret.to_bytes();
let mut s = [0u8; SECRET_KEY_LENGTH];
s.copy_from_slice(&secret);
s
let context = signing_context(SIGNING_CTX);
Ok(kp.sign(context.bytes(message)).to_bytes())
}

pub fn __verify(signature: &[u8], message: &[u8], pubkey: &[u8]) -> bool {
Expand All @@ -42,18 +38,26 @@ pub fn __verify(signature: &[u8], message: &[u8], pubkey: &[u8]) -> bool {
pk.verify_simple(SIGNING_CTX, message, &sig)
}

pub fn __sign(public: &[u8], private: &[u8], message: &[u8]) -> [u8; SIGNATURE_LENGTH] {
// despite being a method of KeyPair, only the secret is used for signing.
let secret = match SecretKey::from_bytes(private) {
Ok(some_secret) => some_secret,
Err(_) => panic!("Provided private key is invalid.")
};
/// Private helper function.
fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, SignatureError> {
let mini_key: MiniSecretKey = MiniSecretKey::from_bytes(seed)?;
Ok(mini_key.expand_to_keypair::<Sha512>())
}

let public = match PublicKey::from_bytes(public) {
Ok(some_public) => some_public,
Err(_) => panic!("Provided public key is invalid.")
};

let context = signing_context(SIGNING_CTX);
secret.sign(context.bytes(message), &public).to_bytes()
}
pub fn __keypair_from_seed(seed: &[u8]) -> Result<[u8; KEYPAIR_LENGTH], SignatureError> {
let keypair = keypair_from_seed(seed)?.to_bytes();
let mut kp = [0u8; KEYPAIR_LENGTH];
kp.copy_from_slice(&keypair);
Ok(kp)
}

pub fn __secret_from_seed(seed: &[u8]) -> Result<[u8; SECRET_KEY_LENGTH], SignatureError> {
let secret = keypair_from_seed(seed)?.secret.to_bytes();
let mut s = [0u8; SECRET_KEY_LENGTH];
s.copy_from_slice(&secret);
Ok(s)
}

pub fn __expand_to_public(secret: &[u8]) -> Result<[u8; PUBLIC_KEY_LENGTH], SignatureError> {
Ok(SecretKey::from_bytes(secret)?.to_public().to_bytes())
}