-
Notifications
You must be signed in to change notification settings - Fork 252
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
WebR Expose matrix-sdk-crypto-js bindings for KeyBackup #2196
Changes from all commits
640ea63
09b8139
5c89b6b
2d962bf
b9d038f
17477be
fbb9941
dca398e
d6b30ae
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 |
---|---|---|
@@ -0,0 +1,176 @@ | ||
//! Megolm backup types | ||
|
||
use std::{collections::HashMap, iter, ops::DerefMut}; | ||
|
||
use hmac::Hmac; | ||
use js_sys::{JsString, JSON}; | ||
use matrix_sdk_crypto::{backups::MegolmV1BackupKey as InnerMegolmV1BackupKey, store::RecoveryKey}; | ||
use pbkdf2::pbkdf2; | ||
use rand::{distributions::Alphanumeric, thread_rng, Rng}; | ||
use serde_wasm_bindgen; | ||
use sha2::Sha512; | ||
use wasm_bindgen::prelude::*; | ||
use zeroize::Zeroize; | ||
|
||
/// The private part of the backup key, the one used for recovery. | ||
#[derive(Debug)] | ||
#[wasm_bindgen] | ||
pub struct BackupRecoveryKey { | ||
pub(crate) inner: RecoveryKey, | ||
pub(crate) passphrase_info: Option<PassphraseInfo>, | ||
} | ||
|
||
/// Struct containing info about the way the backup key got derived from a | ||
/// passphrase. | ||
#[derive(Debug, Clone)] | ||
#[wasm_bindgen] | ||
pub struct PassphraseInfo { | ||
/// The salt that was used during key derivation. | ||
#[wasm_bindgen(getter_with_clone)] | ||
pub private_key_salt: JsString, | ||
/// The number of PBKDF rounds that were used for key derivation. | ||
pub private_key_iterations: i32, | ||
} | ||
|
||
/// The public part of the backup key. | ||
#[derive(Debug, Clone)] | ||
#[wasm_bindgen] | ||
pub struct MegolmV1BackupKey { | ||
inner: InnerMegolmV1BackupKey, | ||
passphrase_info: Option<PassphraseInfo>, | ||
} | ||
|
||
#[wasm_bindgen] | ||
impl MegolmV1BackupKey { | ||
/// The actual base64 encoded public key. | ||
#[wasm_bindgen(getter, js_name = "publicKeyBase64")] | ||
pub fn public_key(&self) -> JsString { | ||
self.inner.to_base64().into() | ||
} | ||
|
||
/// The passphrase info, if the key was derived from one. | ||
#[wasm_bindgen(getter, js_name = "passphraseInfo")] | ||
pub fn passphrase_info(&self) -> Option<PassphraseInfo> { | ||
self.passphrase_info.clone() | ||
} | ||
|
||
/// Get the full name of the backup algorithm this backup key supports. | ||
#[wasm_bindgen(getter, js_name = "algorithm")] | ||
pub fn backup_algorithm(&self) -> JsString { | ||
self.inner.backup_algorithm().into() | ||
} | ||
|
||
/// Signatures that have signed our backup key. | ||
/// map of userId to map of deviceOrKeyId to signature | ||
#[wasm_bindgen(getter, js_name = "signatures")] | ||
pub fn signatures(&self) -> JsValue { | ||
let signatures: HashMap<String, HashMap<String, String>> = self | ||
.inner | ||
.signatures() | ||
.into_iter() | ||
.map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect())) | ||
.collect(); | ||
|
||
serde_wasm_bindgen::to_value(&signatures).unwrap() | ||
} | ||
} | ||
|
||
impl BackupRecoveryKey { | ||
const KEY_SIZE: usize = 32; | ||
const SALT_SIZE: usize = 32; | ||
const PBKDF_ROUNDS: i32 = 500_000; | ||
} | ||
|
||
#[wasm_bindgen] | ||
impl BackupRecoveryKey { | ||
/// Create a new random [`BackupRecoveryKey`]. | ||
#[wasm_bindgen(js_name = "createRandomKey")] | ||
pub fn create_random_key() -> BackupRecoveryKey { | ||
BackupRecoveryKey { | ||
inner: RecoveryKey::new() | ||
.expect("Can't gather enough randomness to create a recovery key"), | ||
passphrase_info: None, | ||
} | ||
} | ||
|
||
/// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string. | ||
#[wasm_bindgen(js_name = "fromBase64")] | ||
pub fn from_base64(key: String) -> Result<BackupRecoveryKey, JsError> { | ||
Ok(Self { inner: RecoveryKey::from_base64(&key)?, passphrase_info: None }) | ||
} | ||
|
||
/// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string. | ||
#[wasm_bindgen(js_name = "fromBase58")] | ||
pub fn from_base58(key: String) -> Result<BackupRecoveryKey, JsError> { | ||
Ok(Self { inner: RecoveryKey::from_base58(&key)?, passphrase_info: None }) | ||
} | ||
|
||
/// Create a new [`BackupRecoveryKey`] from the given passphrase. | ||
#[wasm_bindgen(js_name = "newFromPassphrase")] | ||
pub fn new_from_passphrase(passphrase: String) -> BackupRecoveryKey { | ||
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. Are we certain that EW needs this? If so perhaps we should give in and put it into the crypto crate instead of having it twice in the bindings. 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. I didn't see it in web code, we can probably reove |
||
let mut rng = thread_rng(); | ||
let salt: String = iter::repeat(()) | ||
.map(|()| rng.sample(Alphanumeric)) | ||
.map(char::from) | ||
.take(Self::SALT_SIZE) | ||
.collect(); | ||
|
||
BackupRecoveryKey::from_passphrase(passphrase, salt, Self::PBKDF_ROUNDS) | ||
} | ||
|
||
/// Restore a [`BackupRecoveryKey`] from the given passphrase. | ||
#[wasm_bindgen(js_name = "fromPassphrase")] | ||
pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Self { | ||
let mut key = Box::new([0u8; Self::KEY_SIZE]); | ||
let rounds = rounds as u32; | ||
|
||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), salt.as_bytes(), rounds, key.deref_mut()); | ||
|
||
let recovery_key = RecoveryKey::from_bytes(&key); | ||
|
||
key.zeroize(); | ||
|
||
Self { | ||
inner: recovery_key, | ||
passphrase_info: Some(PassphraseInfo { | ||
private_key_salt: salt.into(), | ||
private_key_iterations: rounds as i32, | ||
}), | ||
} | ||
} | ||
|
||
/// Convert the recovery key to a base 58 encoded string. | ||
#[wasm_bindgen(js_name = "toBase58")] | ||
pub fn to_base58(&self) -> JsString { | ||
self.inner.to_base58().into() | ||
} | ||
|
||
/// Convert the recovery key to a base 64 encoded string. | ||
#[wasm_bindgen(js_name = "toBase64")] | ||
pub fn to_base64(&self) -> JsString { | ||
self.inner.to_base64().into() | ||
} | ||
|
||
/// Get the public part of the backup key. | ||
#[wasm_bindgen(getter, js_name = "megolmV1PublicKey")] | ||
pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey { | ||
let public_key = self.inner.megolm_v1_public_key(); | ||
|
||
MegolmV1BackupKey { inner: public_key, passphrase_info: self.passphrase_info.clone() } | ||
} | ||
|
||
/// Try to decrypt a message that was encrypted using the public part of the | ||
/// backup key. | ||
#[wasm_bindgen(js_name = "decryptV1")] | ||
pub fn decrypt_v1( | ||
&self, | ||
ephemeral_key: String, | ||
mac: String, | ||
ciphertext: String, | ||
) -> Result<JsValue, JsError> { | ||
self.inner | ||
.decrypt_v1(&ephemeral_key, &mac, &ciphertext) | ||
.map_err(|e| e.into()) | ||
.map(|r| JSON::parse(&r).unwrap()) | ||
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. Another one. Hmm, this now assumes that whatever you are decrypting is JSON, I guess that's currently true but worries me slightly. 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. let's just return the JSON. The javascript side can deserialise it more easily anyway. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,10 @@ use std::{collections::BTreeMap, ops::Deref, time::Duration}; | |
|
||
use futures_util::StreamExt; | ||
use js_sys::{Array, Function, Map, Promise, Set}; | ||
use matrix_sdk_crypto::{backups::MegolmV1BackupKey, store::RecoveryKey, types::RoomKeyBackupInfo}; | ||
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt}; | ||
use serde_json::{json, Value as JsonValue}; | ||
use serde_wasm_bindgen; | ||
use tracing::warn; | ||
use wasm_bindgen::prelude::*; | ||
use wasm_bindgen_futures::{spawn_local, JsFuture}; | ||
|
@@ -20,7 +22,9 @@ use crate::{ | |
responses::{self, response_from_string}, | ||
store, | ||
store::RoomKeyInfo, | ||
sync_events, types, verification, vodozemac, | ||
sync_events, | ||
types::{self, BackupKeys, RoomKeyCounts, SignatureVerification}, | ||
verification, vodozemac, | ||
}; | ||
|
||
/// State machine implementation of the Olm/Megolm encryption protocol | ||
|
@@ -763,6 +767,155 @@ impl OlmMachine { | |
})) | ||
} | ||
|
||
/// Store the recovery key in the crypto store. | ||
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. IMHO we should not use the term "recovery key" here: matrix-org/matrix-spec#1571 |
||
/// | ||
/// This is useful if the client wants to support gossiping of the backup | ||
/// key. | ||
#[wasm_bindgen(js_name = "saveBackupRecoveryKey")] | ||
pub fn save_recovery_key( | ||
&self, | ||
recovery_key_base_58: String, | ||
version: String, | ||
) -> Result<Promise, JsError> { | ||
let key = RecoveryKey::from_base58(&recovery_key_base_58)?; | ||
|
||
let me = self.inner.clone(); | ||
|
||
Ok(future_to_promise(async move { | ||
me.backup_machine().save_recovery_key(Some(key), Some(version)).await?; | ||
Ok(JsValue::NULL) | ||
})) | ||
} | ||
|
||
/// Get the backup keys we have saved in our crypto store. | ||
/// Returns a json object {recoveryKeyBase58: "", backupVersion: ""} | ||
#[wasm_bindgen(js_name = "getBackupKeys")] | ||
pub fn get_backup_keys(&self) -> Promise { | ||
let me = self.inner.clone(); | ||
|
||
future_to_promise(async move { | ||
let inner = me.backup_machine().get_backup_keys().await?; | ||
let backup_keys = BackupKeys { | ||
recovery_key: inner.recovery_key.map(|k| k.to_base58()), | ||
backup_version: inner.backup_version, | ||
}; | ||
Ok(serde_wasm_bindgen::to_value(&backup_keys).unwrap()) | ||
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. More unwraps. |
||
}) | ||
} | ||
|
||
/// Check if the given backup has been verified by us or by another of our | ||
/// devices that we trust. | ||
/// | ||
/// The `backup_info` should be a JSON object with the following | ||
/// format: | ||
/// | ||
/// ```json | ||
/// { | ||
/// "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", | ||
/// "auth_data": { | ||
/// "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM", | ||
/// "signatures": {} | ||
/// } | ||
/// } | ||
/// ``` | ||
/// Returns a SignatureVerification object | ||
#[wasm_bindgen(js_name = "verifyBackup")] | ||
pub fn verify_backup(&self, backup_info: JsValue) -> Result<Promise, JsError> { | ||
let backup_info: RoomKeyBackupInfo = serde_wasm_bindgen::from_value(backup_info)?; | ||
|
||
let me = self.inner.clone(); | ||
|
||
Ok(future_to_promise(async move { | ||
let result = me.backup_machine().verify_backup(backup_info, false).await?; | ||
Ok(SignatureVerification { inner: result }) | ||
})) | ||
} | ||
|
||
/// Activate the given backup key to be used with the given backup version. | ||
/// | ||
/// **Warning**: The caller needs to make sure that the given `BackupKey` is | ||
/// trusted, otherwise we might be encrypting room keys that a malicious | ||
/// party could decrypt. | ||
/// | ||
/// The [`OlmMachine::verify_backup`] method can be used to so. | ||
#[wasm_bindgen(js_name = "enableBackupV1")] | ||
pub fn enable_backup_v1( | ||
&self, | ||
public_key_base_64: String, | ||
version: String, | ||
) -> Result<Promise, JsError> { | ||
let backup_key = MegolmV1BackupKey::from_base64(&public_key_base_64)?; | ||
backup_key.set_version(version); | ||
|
||
let me = self.inner.clone(); | ||
|
||
Ok(future_to_promise(async move { | ||
me.backup_machine().enable_backup_v1(backup_key).await?; | ||
Ok(JsValue::NULL) | ||
})) | ||
} | ||
|
||
/// Are we able to encrypt room keys. | ||
/// | ||
/// This returns true if we have an active `BackupKey` and backup version | ||
/// registered with the state machine. | ||
#[wasm_bindgen(js_name = "isBackupEnabled")] | ||
pub fn backup_enabled(&self) -> Promise { | ||
let me = self.inner.clone(); | ||
|
||
future_to_promise(async move { | ||
let enabled = me.backup_machine().enabled().await; | ||
Ok(JsValue::from_bool(enabled)) | ||
}) | ||
} | ||
|
||
/// Disable and reset our backup state. | ||
/// | ||
/// This will remove any pending backup request, remove the backup key and | ||
/// reset the backup state of each room key we have. | ||
#[wasm_bindgen(js_name = "disabledBackup")] | ||
pub fn disable_backup(&self) -> Promise { | ||
let me = self.inner.clone(); | ||
|
||
future_to_promise(async move { | ||
me.backup_machine().disable_backup().await?; | ||
Ok(JsValue::NULL) | ||
}) | ||
} | ||
|
||
/// Encrypt a batch of room keys and return a request that needs to be sent | ||
/// out to backup the room keys. | ||
/// This returns an optional `JsValue` representing a `KeysBackupRequest`. | ||
#[wasm_bindgen(js_name = "backupRoomKeys")] | ||
pub fn backup_room_keys(&self) -> Promise { | ||
let me = self.inner.clone(); | ||
|
||
future_to_promise(async move { | ||
let request = me.backup_machine().backup().await?.map(OutgoingRequest); | ||
|
||
match request { | ||
None => Ok(JsValue::NULL), | ||
Some(r) => Ok(JsValue::try_from(r)?), | ||
} | ||
}) | ||
} | ||
|
||
/// Get the number of backed up room keys and the total number of room keys. | ||
/// Returns a {"total":1,"backedUp":0} json object | ||
#[wasm_bindgen(js_name = "roomKeyCounts")] | ||
pub fn room_key_counts(&self) -> Promise { | ||
let me = self.inner.clone(); | ||
future_to_promise(async move { | ||
let inner = me.backup_machine().room_key_counts().await?; | ||
let count = RoomKeyCounts { | ||
total: inner.total.try_into()?, | ||
backed_up: inner.backed_up.try_into()?, | ||
}; | ||
let js_value = serde_wasm_bindgen::to_value(&count).unwrap(); | ||
Ok(js_value) | ||
}) | ||
} | ||
|
||
/// Encrypt the list of exported room keys using the given passphrase. | ||
/// | ||
/// `exported_room_keys` is a list of sessions that should be encrypted | ||
|
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.
Left an unwrap here.
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.
I'm far from convinced that we need this method anyway. What is the application supposed to do with a list of signatures once it has one? It should be up to the rust side to validate the signatures.
Will remove this for now.