mirror of
https://github.com/fluencelabs/rust-libp2p
synced 2025-06-10 16:41:21 +00:00
Rewrite the WebCrypto ECDH using wasm-bindgen (#980)
* Rewrite the WebCrypto ECDH * Add comment about the unsafe
This commit is contained in:
@ -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"]
|
||||
|
@ -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<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();
|
||||
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();
|
||||
|
||||
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<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)
|
||||
});
|
||||
|
||||
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!())
|
||||
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 (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::<u8>::from(buffer.downcast::<ArrayBuffer>().
|
||||
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<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()
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user