2
2
3
3
use std::{
4
4
collections::{BTreeMap, HashSet},
5
+ io::{Cursor, Read},
5
6
iter,
6
7
ops::Deref,
7
8
pin::{pin, Pin},
@@ -11,23 +12,30 @@ use std::{
11
12
use futures_util::{pin_mut, Stream, StreamExt};
12
13
use js_sys::{Array, Function, JsString, Map, Promise, Set};
13
14
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,
16
22
};
17
23
use matrix_sdk_crypto::{
18
24
backups::MegolmV1BackupKey,
19
25
olm::{BackedUpRoomKey, ExportedRoomKey},
20
26
store::types::{DeviceChanges, IdentityChanges},
21
- types::RoomKeyBackupInfo,
22
- CryptoStoreError, EncryptionSyncChanges, GossippedSecret,
27
+ types::{events::room_key_bundle::RoomKeyBundleContent, RoomKeyBackupInfo} ,
28
+ CryptoStoreError, EncryptionSyncChanges, GossippedSecret, MediaEncryptionInfo,
23
29
};
24
30
use serde::{ser::SerializeSeq, Serialize, Serializer};
25
31
use serde_json::json;
26
- use tracing::{dispatcher, instrument::WithSubscriber, warn, Dispatch};
32
+ use tracing::{dispatcher, info, instrument::WithSubscriber, warn, Dispatch};
27
33
use wasm_bindgen::{convert::TryFromJsValue, prelude::*};
28
34
use wasm_bindgen_futures::{spawn_local, JsFuture};
35
+ use zeroize::Zeroizing;
29
36
30
37
use crate::{
38
+ attachment,
31
39
backup::{BackupDecryptionKey, BackupKeys, RoomKeyCounts},
32
40
dehydrated_devices::DehydratedDevices,
33
41
device, encryption,
@@ -42,7 +50,7 @@ use crate::{
42
50
tracing::{logger_to_dispatcher, JsLogger},
43
51
types::{
44
52
self, processed_to_device_event_to_js_value, RoomKeyImportResult, RoomSettings,
45
- SignatureVerification,
53
+ SignatureVerification, StoredRoomKeyBundleData,
46
54
},
47
55
verification, vodozemac,
48
56
};
@@ -1681,6 +1689,184 @@ impl OlmMachine {
1681
1689
self.inner.dehydrated_devices().into()
1682
1690
}
1683
1691
1692
+ /// Assemble, and encrypt, a room key bundle for sharing encrypted history,
1693
+ /// as per {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4268|MSC4268}.
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 https://github.com/matrix-org/matrix-spec-proposals/pull/4268|MSC4268}
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
+
1684
1870
/// Shut down the `OlmMachine`.
1685
1871
///
1686
1872
/// The `OlmMachine` cannot be used after this method has been called.
0 commit comments