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

WebR Expose matrix-sdk-crypto-js bindings for KeyBackup #2196

Closed
wants to merge 9 commits into from
Closed
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
8 changes: 7 additions & 1 deletion bindings/matrix-sdk-crypto-js/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,19 @@ tracing = []
anyhow = { workspace = true }
console_error_panic_hook = "0.1.7"
futures-util = "0.3.27"
hmac = "0.12.1"
http = { workspace = true }
pbkdf2 = "0.11.0"
rand = "0.8.5"
js-sys = "0.3.49"
matrix-sdk-common = { version = "0.6.0", path = "../../crates/matrix-sdk-common", features = ["js"] }
matrix-sdk-indexeddb = { version = "0.2.0", path = "../../crates/matrix-sdk-indexeddb" }
matrix-sdk-qrcode = { version = "0.4.0", path = "../../crates/matrix-sdk-qrcode", optional = true }
ruma = { workspace = true, features = ["js", "rand"] }
serde = { workspace = true }
serde_json = { workspace = true }
serde-wasm-bindgen = "0.5.0"
sha2 = "0.10.2"
tracing = { workspace = true }
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std"] }
vodozemac = { workspace = true, features = ["js"] }
Expand All @@ -56,4 +62,4 @@ zeroize = { workspace = true }
path = "../../crates/matrix-sdk-crypto"
version = "0.6.0"
default_features = false
features = ["js", "automatic-room-key-forwarding"]
features = ["js", "backups_v1", "automatic-room-key-forwarding"]
176 changes: 176 additions & 0 deletions bindings/matrix-sdk-crypto-js/src/backup_recovery_key.rs
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()
Copy link
Contributor

Choose a reason for hiding this comment

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

Left an unwrap here.

Copy link
Member

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.

}
}

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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())
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

}
}
1 change: 1 addition & 0 deletions bindings/matrix-sdk-crypto-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#![allow(clippy::drop_non_drop)]

pub mod attachment;
pub mod backup_recovery_key;
pub mod device;
pub mod encryption;
pub mod events;
Expand Down
155 changes: 154 additions & 1 deletion bindings/matrix-sdk-crypto-js/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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
Expand Down Expand Up @@ -763,6 +767,155 @@ impl OlmMachine {
}))
}

/// Store the recovery key in the crypto store.
Copy link
Member

Choose a reason for hiding this comment

The 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())
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Down
Loading