Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite the WebCrypto ECDH using wasm-bindgen #980

Merged
merged 3 commits into from
Apr 10, 2019
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 7 additions & 3 deletions protocols/secio/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
239 changes: 144 additions & 95 deletions protocols/secio/src/exchange/impl_webcrypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,115 +22,164 @@

use crate::{KeyAgreement, SecioError};
use futures::prelude::*;
use futures::sync::oneshot;
use stdweb::{self, Reference, web::ArrayBuffer, web::TypedArray};

/// Opaque private key type.
pub type AgreementPrivateKey = Reference;
use send_wrapper::SendWrapper;
use std::io;
use wasm_bindgen::prelude::*;

/// 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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Unless the Threads feature gets implemented eventually. I think Chrome versions >= 70 already support it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, I don't know how to solve that and suggest opening an issue instead of blocking this PR. It is not a safety problem, only a panicking problem.

pub struct SendSyncHack<T>(SendWrapper<T>);

impl<T> Future for SendSyncHack<T>
where T: Future {
type Item = T::Item;
type Error = T::Error;

fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
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<Item = (AgreementPrivateKey, Vec<u8>), 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();

let (tx, rx) = oneshot::channel();
let mut tx = Some(tx);

let curve = match algorithm {
KeyAgreement::EcdhP256 => "P-256",
KeyAgreement::EcdhP384 => "P-384",
};

let send = move |private, public| {
let _ = tx.take()
.expect("JavaScript promise has been resolved twice") // TODO: prove
.send((private, public));
};

js!{
var send = @{send};

let obj = {
name : "ECDH",
namedCurve: @{curve},
};

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) })
});
};

rx
.map(move |(private, public): (AgreementPrivateKey, Reference)| {
// TODO: is this actually true? the WebCrypto specs are blurry
let array = public.downcast::<ArrayBuffer>()
.expect("The output of crypto.subtle.exportKey is always an ArrayBuffer");
(private, Vec::<u8>::from(array))
})
.map_err(|_| unreachable!())
pub fn generate_agreement(algorithm: KeyAgreement)
-> impl Future<Item = (AgreementPrivateKey, Vec<u8>), Error = SecioError>
{
// First step is to create the `SubtleCrypto` object.
let crypto = build_crypto_future();

// 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 usages = js_sys::Array::new();
usages.push(&JsValue::from_str("deriveKey"));
usages.push(&JsValue::from_str("deriveBits"));

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))
});

// 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),
}
});

// 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))
});

// And finally we convert this `ArrayBuffer` into a `Vec<u8>`.
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)
});

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<Item = Vec<u8>, Error = SecioError>
{
let (tx, rx) = oneshot::channel();
let mut tx = Some(tx);

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 (private_key, crypto) = key.0.take();

// 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.
tomaka marked this conversation as resolved.
Show resolved Hide resolved
// 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())
};

let send = move |out: Reference| {
let _ = tx.take()
.expect("JavaScript promise has been resolved twice") // TODO: prove
.send(out);
// Note: contrary to what one might think, we shouldn't add the "deriveBits" usage.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain that bit?

Copy link
Member Author

Choose a reason for hiding this comment

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

The importKey function takes a list of "usages": https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
For me it was obvious that one should pass the "deriveBits" usage, but if I remember correctly in practice both Firefox and Chrome produce an error if I do that.

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()
};

js!{
var key = @{key};
var other_public_key = @{other_public_key};
var send = @{send};
var curve = @{curve};
var out_size = @{out_size};
// 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(|err| {
SecioError::IoError(io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))
});

let import_params = {
name : "ECDH",
namedCurve: curve,
};
SendSyncHack(SendWrapper::new(future))
}

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));
});
};
/// Builds a future that returns the `SubtleCrypto` object.
fn build_crypto_future() -> impl Future<Item = web_sys::SubtleCrypto, Error = JsValue> {
web_sys::window()
.ok_or_else(|| JsValue::from_str("Window object not available"))
.and_then(|window| window.crypto())
.map(|crypto| crypto.subtle())
.into_future()
}

rx
.map(move |buffer| {
Vec::<u8>::from(buffer.downcast::<ArrayBuffer>().
expect("We put the bits into a Uint8Array, which can be casted into \
an ArrayBuffer"))
})
.map_err(|_| unreachable!())
/// 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
}
8 changes: 0 additions & 8 deletions protocols/secio/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down