Skip to content

Commit 114ef63

Browse files
committed
Expose encrypted-history sharing
1 parent aaacc35 commit 114ef63

File tree

6 files changed

+428
-9
lines changed

6 files changed

+428
-9
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
- 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))
810

911
# matrix-sdk-crypto-wasm v15.0.0

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ thiserror = "2.0.12"
7474
tracing = { version = "0.1.36", default-features = false, features = ["std"] }
7575
tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std", "ansi"] }
7676
url = "2.5.0"
77-
wasm-bindgen = "0.2.89"
77+
wasm-bindgen = "0.2.100"
7878
wasm-bindgen-futures = "0.4.33"
7979
zeroize = "1.6.0"
8080
wasm-bindgen-test = "0.3.37"

src/machine.rs

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ use matrix_sdk_crypto::{
2323
};
2424
use serde::{ser::SerializeSeq, Serialize, Serializer};
2525
use serde_json::json;
26-
use tracing::warn;
26+
use tracing::{info, warn};
2727
use wasm_bindgen::{convert::TryFromJsValue, prelude::*};
2828
use wasm_bindgen_futures::{spawn_local, JsFuture};
29+
use zeroize::Zeroizing;
2930

3031
use crate::{
32+
attachment,
3133
backup::{BackupDecryptionKey, BackupKeys, RoomKeyCounts},
3234
dehydrated_devices::DehydratedDevices,
3335
device, encryption,
@@ -41,7 +43,7 @@ use crate::{
4143
sync_events,
4244
types::{
4345
self, processed_to_device_event_to_js_value, RoomKeyImportResult, RoomSettings,
44-
SignatureVerification,
46+
SignatureVerification, StoredRoomKeyBundleData,
4547
},
4648
verification, vodozemac,
4749
};
@@ -1584,6 +1586,146 @@ impl OlmMachine {
15841586
self.inner.dehydrated_devices().into()
15851587
}
15861588

1589+
/// Assemble, and encrypt, a room key bundle for sharing encrypted history,
1590+
/// as per {@link MSC4268 | https://github.com/matrix-org/matrix-spec-proposals/pull/4268}.
1591+
///
1592+
/// Returns `undefined` if there are no keys to share in the given room,
1593+
/// otherwise an {@link EncryptedAttachment}.
1594+
///
1595+
/// The data should be uploaded to the media server, and the details then
1596+
/// passed to {@link shareRoomKeyBundleData}.
1597+
///
1598+
/// @experimental
1599+
#[wasm_bindgen(
1600+
js_name = "buildRoomKeyBundle",
1601+
unchecked_return_type = "Promise<EncryptedAttachment | undefined>"
1602+
)]
1603+
pub fn build_room_key_bundle(&self, room_id: &identifiers::RoomId) -> Promise {
1604+
let me = self.inner.clone();
1605+
let room_id = room_id.inner.clone();
1606+
1607+
future_to_promise(async move {
1608+
let bundle = me.store().build_room_key_bundle(&room_id).await?;
1609+
1610+
if bundle.is_empty() {
1611+
info!("No keys to share");
1612+
return Ok(None);
1613+
}
1614+
1615+
// Remember to zeroize the json as soon as we're done with it
1616+
let json = Zeroizing::new(serde_json::to_vec(&bundle)?);
1617+
1618+
Ok(Some(attachment::Attachment::encrypt(json.as_slice())?))
1619+
})
1620+
}
1621+
1622+
/// Collect the devices belonging to the given user, and send the details
1623+
/// of a room key bundle to those devices.
1624+
///
1625+
/// Returns a list of to-device requests which must be sent.
1626+
///
1627+
/// @experimental
1628+
#[wasm_bindgen(
1629+
js_name = "shareRoomKeyBundleData",
1630+
unchecked_return_type = "Promise<ToDeviceRequest[]>"
1631+
)]
1632+
pub fn share_room_key_bundle_data(
1633+
&self,
1634+
user: &identifiers::UserId,
1635+
sharing_strategy: encryption::CollectStrategy,
1636+
content: JsValue,
1637+
) -> Result<Promise, JsError> {
1638+
let me = self.inner.clone();
1639+
let user_id = user.inner.clone();
1640+
let bundle_data = serde_wasm_bindgen::from_value(content).map_err(|e| {
1641+
JsError::new(&format!("Unable to validate room key bundle content: {e}"))
1642+
})?;
1643+
1644+
Ok(future_to_promise(async move {
1645+
let to_device_requests = me
1646+
.share_room_key_bundle_data(&user_id, &sharing_strategy.into(), bundle_data)
1647+
.await?;
1648+
1649+
// convert each request to our own ToDeviceRequest struct, and then wrap it in a
1650+
// JsValue.
1651+
//
1652+
// Then collect the results into a javascript Array, throwing any errors into
1653+
// the promise.
1654+
let result = to_device_requests
1655+
.into_iter()
1656+
.map(|td| ToDeviceRequest::try_from(&td).map(JsValue::from))
1657+
.collect::<Result<Array, _>>()?;
1658+
1659+
Ok(result)
1660+
}))
1661+
}
1662+
1663+
/// See if we have received an {@link MSC4268 | https://github.com/matrix-org/matrix-spec-proposals/pull/4268}
1664+
/// room key bundle for the given room from the given user.
1665+
///
1666+
/// Returns either `undefined` if no suitable bundle has been received,
1667+
/// or an {@link StoredRoomKeyBundleData}, in which case, the bundle
1668+
/// should be downloaded, and then passed to {@link
1669+
/// receiveRoomKeyBundle}.
1670+
///
1671+
/// @experimental
1672+
#[wasm_bindgen(
1673+
js_name = "getReceivedRoomKeyBundleData",
1674+
unchecked_return_type = "Promise<StoredRoomKeyBundleData | undefined>"
1675+
)]
1676+
pub fn get_received_room_key_bundle_data(
1677+
&self,
1678+
room_id: &identifiers::RoomId,
1679+
inviter: &identifiers::UserId,
1680+
) -> Promise {
1681+
let me = self.inner.clone();
1682+
let room_id = room_id.inner.clone();
1683+
let inviter = inviter.inner.clone();
1684+
1685+
future_to_promise(async move {
1686+
let result = me
1687+
.store()
1688+
.get_received_room_key_bundle_data(&room_id, &inviter)
1689+
.await?
1690+
.map(types::StoredRoomKeyBundleData::from);
1691+
Ok(result)
1692+
})
1693+
}
1694+
1695+
/// Import the message keys from a downloaded room key bundle.
1696+
///
1697+
/// After {@link getReceivedRoomKeyBundleData} returns a truthy result, the
1698+
/// media file should be downloaded and then passed into this method to
1699+
/// actually do the import.
1700+
///
1701+
/// @experimental
1702+
#[wasm_bindgen(js_name = "receiveRoomKeyBundle", unchecked_return_type = "Promise<undefined>")]
1703+
pub fn receive_room_key_bundle(
1704+
&self,
1705+
bundle_data: &StoredRoomKeyBundleData,
1706+
mut bundle: attachment::EncryptedAttachment,
1707+
) -> Result<Promise, JsError> {
1708+
let me = self.inner.clone();
1709+
let bundle_data = bundle_data.clone();
1710+
1711+
let decrypted_bundle = attachment::Attachment::decrypt(&mut bundle)?;
1712+
let deserialized_bundle = serde_json::from_slice(&decrypted_bundle)?;
1713+
1714+
Ok(future_to_promise(async move {
1715+
me.store()
1716+
.receive_room_key_bundle(
1717+
&bundle_data.room_id.inner,
1718+
&bundle_data.sender_user.inner,
1719+
&bundle_data.sender_data,
1720+
deserialized_bundle,
1721+
/* TODO: Use the progress listener and expose an argument for it. */
1722+
|_, _| {},
1723+
)
1724+
.await?;
1725+
Ok(JsValue::UNDEFINED)
1726+
}))
1727+
}
1728+
15871729
/// Shut down the `OlmMachine`.
15881730
///
15891731
/// 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+
}

tests/helper.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ limitations under the License.
1616

1717
import {
1818
DeviceLists,
19-
RequestType,
20-
KeysUploadRequest,
2119
KeysQueryRequest,
22-
ToDeviceRequest,
20+
KeysUploadRequest,
2321
OlmMachine,
22+
RequestType,
23+
RoomId,
24+
ToDeviceRequest,
2425
UserId,
2526
} from "@matrix-org/matrix-sdk-crypto-wasm";
2627

@@ -88,6 +89,7 @@ export async function addMachineToMachine(machineToAdd: OlmMachine, machine: Olm
8889

8990
let bootstrapCrossSigningResult = await machineToAdd.bootstrapCrossSigning(true);
9091
let signingKeysUploadRequest = bootstrapCrossSigningResult.uploadSigningKeysRequest;
92+
const uploadSignaturesRequestBody = JSON.parse(bootstrapCrossSigningResult.uploadSignaturesRequest.body);
9193

9294
// Let's forge a `KeysQuery`'s response.
9395
let keyQueryResponse = {
@@ -101,6 +103,13 @@ export async function addMachineToMachine(machineToAdd: OlmMachine, machine: Olm
101103
keyQueryResponse.device_keys[userId] = {};
102104
keyQueryResponse.device_keys[userId][deviceId] = JSON.parse(keysUploadRequest.body).device_keys;
103105

106+
// Hack in the cross-signing signature on the device from `bootstrapCrossSigning`
107+
expect(uploadSignaturesRequestBody[userId][deviceId]).toBeDefined();
108+
Object.assign(
109+
keyQueryResponse.device_keys[userId][deviceId].signatures[userId],
110+
uploadSignaturesRequestBody[userId][deviceId].signatures[userId],
111+
);
112+
104113
const keys = JSON.parse(signingKeysUploadRequest.body);
105114
keyQueryResponse.master_keys[userId] = keys.master_key;
106115
keyQueryResponse.self_signing_keys[userId] = keys.self_signing_key;
@@ -156,3 +165,55 @@ export async function sendToDeviceMessageIntoMachine(
156165
undefined,
157166
);
158167
}
168+
169+
/**
170+
* Have `senderMachine` claim one of `receiverMachine`'s OTKs, to establish an olm session.
171+
*
172+
* Requires that `senderMachine` already know about `receiverMachine`, normally via
173+
* `addMachineToMachine(receiverMachine, senderMachine)`.
174+
*/
175+
export async function establishOlmSession(senderMachine: OlmMachine, receiverMachine: OlmMachine) {
176+
const keysClaimRequest = await senderMachine.getMissingSessions([receiverMachine.userId]);
177+
const keysClaimRequestBody = JSON.parse(keysClaimRequest!.body);
178+
expect(keysClaimRequestBody.one_time_keys).toEqual({
179+
[receiverMachine.userId.toString()]: { [receiverMachine.deviceId.toString()]: "signed_curve25519" },
180+
});
181+
182+
const outgoing = await receiverMachine.outgoingRequests();
183+
const keysUploadRequest = outgoing.find((req) => req instanceof KeysUploadRequest);
184+
expect(keysUploadRequest).toBeDefined();
185+
const keysUploadRequestBody = JSON.parse(keysUploadRequest!.body);
186+
const [otk_id, otk] = Object.entries(keysUploadRequestBody.one_time_keys)[0];
187+
188+
const keysClaimResponse = {
189+
one_time_keys: {
190+
[receiverMachine.userId.toString()]: { [receiverMachine.deviceId.toString()]: { [otk_id]: otk } },
191+
},
192+
};
193+
194+
await senderMachine.markRequestAsSent(
195+
keysClaimRequest!.id,
196+
RequestType.KeysClaim,
197+
JSON.stringify(keysClaimResponse),
198+
);
199+
}
200+
201+
/**
202+
* Encrypt an event using the given OlmMachine. Returns the JSON for the encrypted event.
203+
*
204+
* Assumes that a megolm session has already been established.
205+
*/
206+
export async function encryptEvent(
207+
senderMachine: OlmMachine,
208+
room: RoomId,
209+
eventType: string,
210+
eventContent: string,
211+
): Promise<string> {
212+
return JSON.stringify({
213+
sender: senderMachine.userId.toString(),
214+
event_id: `\$${Date.now()}:example.com`,
215+
type: "m.room.encrypted",
216+
origin_server_ts: Date.now(),
217+
content: JSON.parse(await senderMachine.encryptRoomEvent(room, eventType, eventContent)),
218+
});
219+
}

0 commit comments

Comments
 (0)