Skip to content

Commit 28b69c9

Browse files
committed
Expose encrypted-history sharing
1 parent 1b59dda commit 28b69c9

File tree

5 files changed

+471
-12
lines changed

5 files changed

+471
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Add support for received room key bundle data, as required by encrypted history sharing ((MSC4268)[https://github.com/matrix-org/matrix-spec-proposals/pull/4268)). ([#5276](https://github.com/matrix-org/matrix-rust-sdk/pull/5276))
66

7+
- Expose experimental new functionality for sharing encrypted room history, per [MSC4268](https://github.com/matrix-org/matrix-spec-proposals/pull/4268). ([#250](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/250))
8+
79
- Extend `OlmMachine` constructor functions to accept an optional `logger` instance. This logger is associated with the machine for its lifetime, and used instead of the global `console` to write any log messages from the underlying rust library. Similarly, the legacy store migration functions (in the `Migration` class), and the `StoreHandle` open functions are extended to accept an optional logger, to be used for the duration of that operation. ([#251](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/251))
810

911
- Add a new error code for `MegolmDecryptionError`, `DecryptionErrorCode::MismatchedSender`, indicating that the sender of the event does not match the owner of the device that established the Megolm session. ([#248](https://github.com/matrix-org/matrix-rust-sdk-crypto-wasm/pull/248))

src/machine.rs

Lines changed: 192 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::{
44
collections::{BTreeMap, HashSet},
5+
io::{Cursor, Read},
56
iter,
67
ops::Deref,
78
pin::{pin, Pin},
@@ -11,23 +12,30 @@ use std::{
1112
use futures_util::{pin_mut, Stream, StreamExt};
1213
use js_sys::{Array, Function, JsString, Map, Promise, Set};
1314
use matrix_sdk_common::ruma::{
14-
self, events::secret::request::SecretName, serde::Raw, OneTimeKeyAlgorithm, OwnedDeviceId,
15-
OwnedTransactionId, OwnedUserId, UInt,
15+
self,
16+
events::{
17+
room::{EncryptedFile, EncryptedFileInit},
18+
secret::request::SecretName,
19+
},
20+
serde::Raw,
21+
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedTransactionId, OwnedUserId, UInt,
1622
};
1723
use matrix_sdk_crypto::{
1824
backups::MegolmV1BackupKey,
1925
olm::{BackedUpRoomKey, ExportedRoomKey},
2026
store::types::{DeviceChanges, IdentityChanges},
21-
types::RoomKeyBackupInfo,
22-
CryptoStoreError, EncryptionSyncChanges, GossippedSecret,
27+
types::{events::room_key_bundle::RoomKeyBundleContent, RoomKeyBackupInfo},
28+
CryptoStoreError, EncryptionSyncChanges, GossippedSecret, MediaEncryptionInfo,
2329
};
2430
use serde::{ser::SerializeSeq, Serialize, Serializer};
2531
use serde_json::json;
26-
use tracing::{dispatcher, instrument::WithSubscriber, warn, Dispatch};
32+
use tracing::{dispatcher, info, instrument::WithSubscriber, warn, Dispatch};
2733
use wasm_bindgen::{convert::TryFromJsValue, prelude::*};
2834
use wasm_bindgen_futures::{spawn_local, JsFuture};
35+
use zeroize::Zeroizing;
2936

3037
use crate::{
38+
attachment,
3139
backup::{BackupDecryptionKey, BackupKeys, RoomKeyCounts},
3240
dehydrated_devices::DehydratedDevices,
3341
device, encryption,
@@ -42,7 +50,7 @@ use crate::{
4250
tracing::{logger_to_dispatcher, JsLogger},
4351
types::{
4452
self, processed_to_device_event_to_js_value, RoomKeyImportResult, RoomSettings,
45-
SignatureVerification,
53+
SignatureVerification, StoredRoomKeyBundleData,
4654
},
4755
verification, vodozemac,
4856
};
@@ -1681,6 +1689,184 @@ impl OlmMachine {
16811689
self.inner.dehydrated_devices().into()
16821690
}
16831691

1692+
/// Assemble, and encrypt, a room key bundle for sharing encrypted history,
1693+
/// as per {@link MSC4268 | https://github.com/matrix-org/matrix-spec-proposals/pull/4268}.
1694+
///
1695+
/// Returns `undefined` if there are no keys to share in the given room,
1696+
/// otherwise an {@link EncryptedAttachment}.
1697+
///
1698+
/// The data should be uploaded to the media server, and the details then
1699+
/// passed to {@link shareRoomKeyBundleData}.
1700+
///
1701+
/// @experimental
1702+
#[wasm_bindgen(
1703+
js_name = "buildRoomKeyBundle",
1704+
unchecked_return_type = "Promise<EncryptedAttachment | undefined>"
1705+
)]
1706+
pub fn build_room_key_bundle(&self, room_id: &identifiers::RoomId) -> Promise {
1707+
let _guard = dispatcher::set_default(&self.tracing_subscriber);
1708+
let me = self.inner.clone();
1709+
let room_id = room_id.inner.clone();
1710+
1711+
future_to_promise(async move {
1712+
let bundle = me.store().build_room_key_bundle(&room_id).await?;
1713+
1714+
if bundle.is_empty() {
1715+
info!("No keys to share");
1716+
return Ok(None);
1717+
}
1718+
1719+
// Remember to zeroize the json as soon as we're done with it
1720+
let json = Zeroizing::new(serde_json::to_vec(&bundle)?);
1721+
1722+
Ok(Some(attachment::Attachment::encrypt(json.as_slice())?))
1723+
})
1724+
}
1725+
1726+
/// Collect the devices belonging to the given user, and send the details
1727+
/// of a room key bundle to those devices.
1728+
///
1729+
/// Returns a list of to-device requests which must be sent.
1730+
///
1731+
/// @experimental
1732+
#[wasm_bindgen(
1733+
js_name = "shareRoomKeyBundleData",
1734+
unchecked_return_type = "Promise<ToDeviceRequest[]>"
1735+
)]
1736+
pub fn share_room_key_bundle_data(
1737+
&self,
1738+
user: &identifiers::UserId,
1739+
room: &identifiers::RoomId,
1740+
url: &str,
1741+
media_encryption_info: Option<String>,
1742+
sharing_strategy: encryption::CollectStrategy,
1743+
) -> Result<Promise, JsError> {
1744+
let _guard = dispatcher::set_default(&self.tracing_subscriber);
1745+
let me = self.inner.clone();
1746+
let user_id = user.inner.clone();
1747+
1748+
let media_encryption_info = media_encryption_info.ok_or_else(|| {
1749+
// We accept Option<String> to save a typescript assertion on the application
1750+
// side. In practice `build_room_key_bundle` should never return an
1751+
// EncryptedAttachment with nullish encryption_info.
1752+
JsError::new("shareRoomKeyBundleData: nullish encryption info")
1753+
})?;
1754+
1755+
let media_encryption_info: MediaEncryptionInfo =
1756+
serde_json::from_str(&media_encryption_info).map_err(|e| {
1757+
JsError::new(&format!("Unable to validate room key media encryption info: {e}"))
1758+
})?;
1759+
1760+
let url = url.try_into().map_err(|e| JsError::new(&format!("Invalid media url: {e}")))?;
1761+
1762+
let bundle_data = RoomKeyBundleContent {
1763+
room_id: room.inner.clone(),
1764+
file: EncryptedFile::from(EncryptedFileInit {
1765+
url,
1766+
key: media_encryption_info.key,
1767+
iv: media_encryption_info.iv,
1768+
hashes: media_encryption_info.hashes,
1769+
v: media_encryption_info.version,
1770+
}),
1771+
};
1772+
1773+
Ok(future_to_promise(async move {
1774+
let to_device_requests = me
1775+
.share_room_key_bundle_data(&user_id, &sharing_strategy.into(), bundle_data)
1776+
.await?;
1777+
1778+
// convert each request to our own ToDeviceRequest struct, and then wrap it in a
1779+
// JsValue.
1780+
//
1781+
// Then collect the results into a javascript Array, throwing any errors into
1782+
// the promise.
1783+
let result = to_device_requests
1784+
.into_iter()
1785+
.map(|td| ToDeviceRequest::try_from(&td).map(JsValue::from))
1786+
.collect::<Result<Array, _>>()?;
1787+
1788+
Ok(result)
1789+
}))
1790+
}
1791+
1792+
/// See if we have received an {@link MSC4268 | https://github.com/matrix-org/matrix-spec-proposals/pull/4268}
1793+
/// room key bundle for the given room from the given user.
1794+
///
1795+
/// Returns either `undefined` if no suitable bundle has been received,
1796+
/// or an {@link StoredRoomKeyBundleData}, in which case, the bundle
1797+
/// should be downloaded, and then passed to {@link
1798+
/// receiveRoomKeyBundle}.
1799+
///
1800+
/// @experimental
1801+
#[wasm_bindgen(
1802+
js_name = "getReceivedRoomKeyBundleData",
1803+
unchecked_return_type = "Promise<StoredRoomKeyBundleData | undefined>"
1804+
)]
1805+
pub fn get_received_room_key_bundle_data(
1806+
&self,
1807+
room_id: &identifiers::RoomId,
1808+
inviter: &identifiers::UserId,
1809+
) -> Promise {
1810+
let _guard = dispatcher::set_default(&self.tracing_subscriber);
1811+
let me = self.inner.clone();
1812+
let room_id = room_id.inner.clone();
1813+
let inviter = inviter.inner.clone();
1814+
1815+
future_to_promise(async move {
1816+
let result = me
1817+
.store()
1818+
.get_received_room_key_bundle_data(&room_id, &inviter)
1819+
.await?
1820+
.map(types::StoredRoomKeyBundleData::from);
1821+
Ok(result)
1822+
})
1823+
}
1824+
1825+
/// Import the message keys from a downloaded room key bundle.
1826+
///
1827+
/// After {@link getReceivedRoomKeyBundleData} returns a truthy result, the
1828+
/// media file should be downloaded and then passed into this method to
1829+
/// actually do the import.
1830+
///
1831+
/// @experimental
1832+
#[wasm_bindgen(js_name = "receiveRoomKeyBundle", unchecked_return_type = "Promise<undefined>")]
1833+
pub fn receive_room_key_bundle(
1834+
&self,
1835+
bundle_data: &StoredRoomKeyBundleData,
1836+
encrypted_bundle: Vec<u8>,
1837+
) -> Result<Promise, JsError> {
1838+
let _guard = dispatcher::set_default(&self.tracing_subscriber);
1839+
1840+
let deserialized_bundle = {
1841+
let mut cursor = Cursor::new(encrypted_bundle.as_slice());
1842+
let mut decryptor = matrix_sdk_crypto::AttachmentDecryptor::new(
1843+
&mut cursor,
1844+
serde_json::from_str(&bundle_data.encryption_info)?,
1845+
)?;
1846+
1847+
let mut decrypted_bundle = Zeroizing::new(Vec::new());
1848+
decryptor.read_to_end(&mut decrypted_bundle)?;
1849+
1850+
serde_json::from_slice(&decrypted_bundle)?
1851+
};
1852+
1853+
let me = self.inner.clone();
1854+
let bundle_data = bundle_data.clone();
1855+
Ok(future_to_promise(async move {
1856+
me.store()
1857+
.receive_room_key_bundle(
1858+
&bundle_data.room_id.inner,
1859+
&bundle_data.sender_user.inner,
1860+
&bundle_data.sender_data,
1861+
deserialized_bundle,
1862+
/* TODO: Use the progress listener and expose an argument for it. */
1863+
|_, _| {},
1864+
)
1865+
.await?;
1866+
Ok(JsValue::UNDEFINED)
1867+
}))
1868+
}
1869+
16841870
/// Shut down the `OlmMachine`.
16851871
///
16861872
/// The `OlmMachine` cannot be used after this method has been called.

src/types.rs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ use std::{
77

88
use js_sys::{Array, JsString, Map, Set};
99
use matrix_sdk_common::ruma::OwnedRoomId;
10-
use matrix_sdk_crypto::backups::{
11-
SignatureState as InnerSignatureState, SignatureVerification as InnerSignatureVerification,
10+
use matrix_sdk_crypto::{
11+
backups::{
12+
SignatureState as InnerSignatureState, SignatureVerification as InnerSignatureVerification,
13+
},
14+
olm::SenderData,
15+
MediaEncryptionInfo,
1216
};
1317
use tracing::warn;
1418
use wasm_bindgen::prelude::*;
1519

1620
use crate::{
1721
encryption::EncryptionAlgorithm,
18-
identifiers::{DeviceKeyId, UserId},
22+
identifiers::{DeviceKeyId, RoomId, UserId},
1923
impl_from_to_inner,
2024
responses::ToDeviceEncryptionInfo,
2125
vodozemac::Ed25519Signature,
@@ -528,3 +532,47 @@ pub fn processed_to_device_event_to_js_value(
528532
};
529533
Some(result)
530534
}
535+
536+
/// Information on a stored room key bundle data event.
537+
#[wasm_bindgen]
538+
#[derive(Debug, Clone)]
539+
pub struct StoredRoomKeyBundleData {
540+
/// The user that sent us this data.
541+
#[wasm_bindgen(readonly, getter_with_clone, js_name = "senderUser")]
542+
pub sender_user: UserId,
543+
544+
/// Information about the sender of this data and how much we trust that
545+
/// information.
546+
///
547+
/// This is held here because we need the `SenderData` back again in
548+
/// [`OlmMachine::receive_room_key_bundle`]. It's not exposed over the wasm
549+
/// boundary because `SenderData` doesn't implement `Into<JsValue>`.
550+
pub(crate) sender_data: SenderData,
551+
552+
/// The room that these keys are for.
553+
#[wasm_bindgen(readonly, getter_with_clone, js_name = "roomId")]
554+
pub room_id: RoomId,
555+
556+
/// The location of the bundle.
557+
#[wasm_bindgen(readonly, getter_with_clone)]
558+
pub url: String,
559+
560+
/// The JSON-encoded encryption info for the key bundle.
561+
#[wasm_bindgen(readonly, getter_with_clone, js_name = "encryptionInfo")]
562+
pub encryption_info: String,
563+
}
564+
565+
impl From<matrix_sdk_crypto::store::types::StoredRoomKeyBundleData> for StoredRoomKeyBundleData {
566+
fn from(value: matrix_sdk_crypto::store::types::StoredRoomKeyBundleData) -> Self {
567+
let url = value.bundle_data.file.url.to_string();
568+
let encryption_info =
569+
serde_json::to_string(&MediaEncryptionInfo::from(value.bundle_data.file)).unwrap();
570+
Self {
571+
sender_user: value.sender_user.into(),
572+
sender_data: value.sender_data,
573+
room_id: value.bundle_data.room_id.into(),
574+
url,
575+
encryption_info,
576+
}
577+
}
578+
}

0 commit comments

Comments
 (0)