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 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
+
1684
1870
/// Shut down the `OlmMachine`.
1685
1871
///
1686
1872
/// The `OlmMachine` cannot be used after this method has been called.
0 commit comments