diff --git a/protocols/secio/Cargo.toml b/protocols/secio/Cargo.toml index 1b97fc4f..9a69de6d 100644 --- a/protocols/secio/Cargo.toml +++ b/protocols/secio/Cargo.toml @@ -28,12 +28,16 @@ tokio-io = "0.1.0" sha2 = "0.8.0" hmac = "0.7.0" -[target.'cfg(not(any(target_os = "emscripten", target_os = "unknown")))'.dependencies] +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] ring = { version = "0.14", features = ["use_heap"], default-features = false } untrusted = { version = "0.6" } -[target.'cfg(any(target_os = "emscripten", target_os = "unknown"))'.dependencies] -stdweb = { version = "0.4", default-features = false } +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = "0.3.10" +send_wrapper = "0.2" +wasm-bindgen = "0.2.33" +wasm-bindgen-futures = "0.3.10" +web-sys = { version = "0.3.10", features = ["Crypto", "CryptoKey", "SubtleCrypto", "Window"] } [features] default = ["secp256k1"] diff --git a/protocols/secio/src/exchange/impl_webcrypto.rs b/protocols/secio/src/exchange/impl_webcrypto.rs index faa7444d..2b5be088 100644 --- a/protocols/secio/src/exchange/impl_webcrypto.rs +++ b/protocols/secio/src/exchange/impl_webcrypto.rs @@ -22,115 +22,164 @@ use crate::{KeyAgreement, SecioError}; use futures::prelude::*; -use futures::sync::oneshot; -use stdweb::{self, Reference, web::ArrayBuffer, web::TypedArray}; +use send_wrapper::SendWrapper; +use std::io; +use wasm_bindgen::prelude::*; -/// Opaque private key type. -pub type AgreementPrivateKey = Reference; +/// Opaque private key type. Contains the private key and the `SubtleCrypto` object. +pub type AgreementPrivateKey = SendSyncHack<(JsValue, web_sys::SubtleCrypto)>; + +/// We use a `SendWrapper` from the `send_wrapper` crate around our JS data type. JavaScript data +/// types are not `Send`/`Sync`, but since WASM is single-threaded we know that we're only ever +/// going to access them from the same thread. +pub struct SendSyncHack(SendWrapper); + +impl Future for SendSyncHack +where T: Future { + type Item = T::Item; + type Error = T::Error; + + fn poll(&mut self) -> Poll { + self.0.poll() + } +} /// Generates a new key pair as part of the exchange. /// /// Returns the opaque private key and the corresponding public key. -pub fn generate_agreement(algorithm: KeyAgreement) -> impl Future), Error = SecioError> { - // Making sure we are initialized before we dial. Initialization is protected by a simple - // boolean static variable, so it's not a problem to call it multiple times and the cost - // is negligible. - stdweb::initialize(); +pub fn generate_agreement(algorithm: KeyAgreement) + -> impl Future), Error = SecioError> +{ + // First step is to create the `SubtleCrypto` object. + let crypto = build_crypto_future(); - let (tx, rx) = oneshot::channel(); - let mut tx = Some(tx); + // We then generate the ephemeral key. + let key_promise = crypto.and_then(move |crypto| { + let crypto = crypto.clone(); + let obj = build_curve_obj(algorithm); - let curve = match algorithm { - KeyAgreement::EcdhP256 => "P-256", - KeyAgreement::EcdhP384 => "P-384", - }; + let usages = js_sys::Array::new(); + usages.push(&JsValue::from_str("deriveKey")); + usages.push(&JsValue::from_str("deriveBits")); - let send = move |private, public| { - let _ = tx.take() - .expect("JavaScript promise has been resolved twice") // TODO: prove - .send((private, public)); - }; + crypto.generate_key_with_object(&obj, true, usages.as_ref()) + .map(wasm_bindgen_futures::JsFuture::from) + .into_future() + .flatten() + .map(|key_pair| (key_pair, crypto)) + }); - js!{ - var send = @{send}; + // WebCrypto has generated a key-pair. Let's split this key pair into a private key and a + // public key. + let split_key = key_promise.and_then(move |(key_pair, crypto)| { + let private = js_sys::Reflect::get(&key_pair, &JsValue::from_str("privateKey")); + let public = js_sys::Reflect::get(&key_pair, &JsValue::from_str("publicKey")); + match (private, public) { + (Ok(pr), Ok(pu)) => Ok((pr, pu, crypto)), + (Err(err), _) => Err(err), + (_, Err(err)) => Err(err), + } + }); - let obj = { - name : "ECDH", - namedCurve: @{curve}, - }; + // Then we turn the public key into an `ArrayBuffer`. + let export_key = split_key.and_then(move |(private, public, crypto)| { + crypto.export_key("raw", &public.into()) + .map(wasm_bindgen_futures::JsFuture::from) + .into_future() + .flatten() + .map(|public| ((private, crypto), public)) + }); - window.crypto.subtle - .generateKey("ECDH", true, ["deriveKey", "deriveBits"]) - .then(function(key) { - window.crypto.subtle.exportKey("raw", key.publicKey) - .then(function(pubkey) { send(key.privateKey, pubkey) }) - }); - }; + // And finally we convert this `ArrayBuffer` into a `Vec`. + let future = export_key + .map(|((private, crypto), public)| { + let public = js_sys::Uint8Array::new(&public); + let mut public_buf = vec![0; public.length() as usize]; + public.copy_to(&mut public_buf); + (SendSyncHack(SendWrapper::new((private, crypto))), public_buf) + }); - rx - .map(move |(private, public): (AgreementPrivateKey, Reference)| { - // TODO: is this actually true? the WebCrypto specs are blurry - let array = public.downcast::() - .expect("The output of crypto.subtle.exportKey is always an ArrayBuffer"); - (private, Vec::::from(array)) - }) - .map_err(|_| unreachable!()) + SendSyncHack(SendWrapper::new(future.map_err(|err| { + SecioError::IoError(io::Error::new(io::ErrorKind::Other, format!("{:?}", err))) + }))) } /// Finish the agreement. On success, returns the shared key that both remote agreed upon. pub fn agree(algorithm: KeyAgreement, key: AgreementPrivateKey, other_public_key: &[u8], out_size: usize) -> impl Future, Error = SecioError> { - let (tx, rx) = oneshot::channel(); - let mut tx = Some(tx); + let (private_key, crypto) = key.0.take(); - let curve = match algorithm { - KeyAgreement::EcdhP256 => "P-256", - KeyAgreement::EcdhP384 => "P-384", - }; - - let other_public_key = TypedArray::from(other_public_key).buffer(); - - let out_size = out_size as u32; - - let send = move |out: Reference| { - let _ = tx.take() - .expect("JavaScript promise has been resolved twice") // TODO: prove - .send(out); - }; - - js!{ - var key = @{key}; - var other_public_key = @{other_public_key}; - var send = @{send}; - var curve = @{curve}; - var out_size = @{out_size}; - - let import_params = { - name : "ECDH", - namedCurve: curve, + // We start by importing the remote's public key into the WebCrypto world. + let import_promise = { + let other_public_key = { + // This unsafe is here because the lifetime of `other_public_key` must not outlive the + // `tmp_view`. This is guaranteed by the fact that we clone this array right below. + // See also https://github.com/rustwasm/wasm-bindgen/issues/1303 + let tmp_view = unsafe { js_sys::Uint8Array::view(other_public_key) }; + js_sys::Uint8Array::new(tmp_view.as_ref()) }; - window.crypto.subtle.importKey("raw", other_public_key, import_params, false, ["deriveBits"]) - .then(function(public_key) { - let derive_params = { - name : "ECDH", - namedCurve: curve, - public: public_key, - }; - - window.crypto.subtle.deriveBits(derive_params, key, out_size) - }) - .then(function(bits) { - send(new Uint8Array(bits)); - }); + // Note: contrary to what one might think, we shouldn't add the "deriveBits" usage. + crypto + .import_key_with_object( + "raw", &js_sys::Object::from(other_public_key.buffer()), + &build_curve_obj(algorithm), false, &js_sys::Array::new() + ) + .into_future() + .map(wasm_bindgen_futures::JsFuture::from) + .flatten() }; - rx - .map(move |buffer| { - Vec::::from(buffer.downcast::(). - expect("We put the bits into a Uint8Array, which can be casted into \ - an ArrayBuffer")) + // We then derive the final private key. + let derive = import_promise.and_then({ + let crypto = crypto.clone(); + move |public_key| { + let derive_params = build_curve_obj(algorithm); + let _ = js_sys::Reflect::set(derive_params.as_ref(), &JsValue::from_str("public"), &public_key); + crypto + .derive_bits_with_object( + &derive_params, + &web_sys::CryptoKey::from(private_key), + 8 * out_size as u32 + ) + .into_future() + .map(wasm_bindgen_futures::JsFuture::from) + .flatten() + } + }); + + let future = derive + .map(|bytes| { + let bytes = js_sys::Uint8Array::new(&bytes); + let mut buf = vec![0; bytes.length() as usize]; + bytes.copy_to(&mut buf); + buf }) - .map_err(|_| unreachable!()) + .map_err(|err| { + SecioError::IoError(io::Error::new(io::ErrorKind::Other, format!("{:?}", err))) + }); + + SendSyncHack(SendWrapper::new(future)) +} + +/// Builds a future that returns the `SubtleCrypto` object. +fn build_crypto_future() -> impl Future { + web_sys::window() + .ok_or_else(|| JsValue::from_str("Window object not available")) + .and_then(|window| window.crypto()) + .map(|crypto| crypto.subtle()) + .into_future() +} + +/// Builds a `EcKeyGenParams` object. +/// See https://developer.mozilla.org/en-US/docs/Web/API/EcKeyGenParams +fn build_curve_obj(algorithm: KeyAgreement) -> js_sys::Object { + let obj = js_sys::Object::new(); + let _ = js_sys::Reflect::set(obj.as_ref(), &JsValue::from_str("name"), &JsValue::from_str("ECDH")); + let _ = js_sys::Reflect::set(obj.as_ref(), &JsValue::from_str("namedCurve"), &JsValue::from_str(match algorithm { + KeyAgreement::EcdhP256 => "P-256", + KeyAgreement::EcdhP384 => "P-384", + })); + obj } diff --git a/protocols/secio/src/lib.rs b/protocols/secio/src/lib.rs index 5bc6b161..ed220fec 100644 --- a/protocols/secio/src/lib.rs +++ b/protocols/secio/src/lib.rs @@ -71,14 +71,6 @@ //! `SecioMiddleware` that implements `Sink` and `Stream` and can be used to send packets of data. //! -#![recursion_limit = "128"] - -// TODO: unfortunately the `js!` macro of stdweb depends on tons of "private" macros, which we -// don't want to import manually -#[cfg(any(target_os = "emscripten", target_os = "unknown"))] -#[macro_use] -extern crate stdweb; - pub use self::error::SecioError; use bytes::BytesMut;