-
Notifications
You must be signed in to change notification settings - Fork 999
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you explain that bit? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
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 | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.