Skip to content

Commit

Permalink
matrix-sdk-crypto-wasm bindings for KeyBackup
Browse files Browse the repository at this point in the history
Import Valere's changes from
matrix-org/matrix-rust-sdk#2196, excluding the fixes to
MemoryStore.
  • Loading branch information
richvdh committed Jul 19, 2023
1 parent 4721f8d commit cb06c51
Show file tree
Hide file tree
Showing 7 changed files with 638 additions and 3 deletions.
8 changes: 7 additions & 1 deletion 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 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()
}
}

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 {
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())
}
}
1 change: 1 addition & 0 deletions 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 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.
///
/// 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())
})
}

/// 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

0 comments on commit cb06c51

Please sign in to comment.