Skip to content

Expose functionality for encrypted history sharing #250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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

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

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

- 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))
Expand Down
198 changes: 192 additions & 6 deletions src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::{
collections::{BTreeMap, HashSet},
io::{Cursor, Read},
iter,
ops::Deref,
pin::{pin, Pin},
Expand All @@ -11,23 +12,30 @@ use std::{
use futures_util::{pin_mut, Stream, StreamExt};
use js_sys::{Array, Function, JsString, Map, Promise, Set};
use matrix_sdk_common::ruma::{
self, events::secret::request::SecretName, serde::Raw, OneTimeKeyAlgorithm, OwnedDeviceId,
OwnedTransactionId, OwnedUserId, UInt,
self,
events::{
room::{EncryptedFile, EncryptedFileInit},
secret::request::SecretName,
},
serde::Raw,
OneTimeKeyAlgorithm, OwnedDeviceId, OwnedTransactionId, OwnedUserId, UInt,
};
use matrix_sdk_crypto::{
backups::MegolmV1BackupKey,
olm::{BackedUpRoomKey, ExportedRoomKey},
store::types::{DeviceChanges, IdentityChanges},
types::RoomKeyBackupInfo,
CryptoStoreError, EncryptionSyncChanges, GossippedSecret,
types::{events::room_key_bundle::RoomKeyBundleContent, RoomKeyBackupInfo},
CryptoStoreError, EncryptionSyncChanges, GossippedSecret, MediaEncryptionInfo,
};
use serde::{ser::SerializeSeq, Serialize, Serializer};
use serde_json::json;
use tracing::{dispatcher, instrument::WithSubscriber, warn, Dispatch};
use tracing::{dispatcher, info, instrument::WithSubscriber, warn, Dispatch};
use wasm_bindgen::{convert::TryFromJsValue, prelude::*};
use wasm_bindgen_futures::{spawn_local, JsFuture};
use zeroize::Zeroizing;

use crate::{
attachment,
backup::{BackupDecryptionKey, BackupKeys, RoomKeyCounts},
dehydrated_devices::DehydratedDevices,
device, encryption,
Expand All @@ -42,7 +50,7 @@ use crate::{
tracing::{logger_to_dispatcher, JsLogger},
types::{
self, processed_to_device_event_to_js_value, RoomKeyImportResult, RoomSettings,
SignatureVerification,
SignatureVerification, StoredRoomKeyBundleData,
},
verification, vodozemac,
};
Expand Down Expand Up @@ -1681,6 +1689,184 @@ impl OlmMachine {
self.inner.dehydrated_devices().into()
}

/// Assemble, and encrypt, a room key bundle for sharing encrypted history,
/// as per {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4268|MSC4268}.
///
/// Returns `undefined` if there are no keys to share in the given room,
/// otherwise an {@link EncryptedAttachment}.
///
/// The data should be uploaded to the media server, and the details then
/// passed to {@link shareRoomKeyBundleData}.
///
/// @experimental
#[wasm_bindgen(
js_name = "buildRoomKeyBundle",
unchecked_return_type = "Promise<EncryptedAttachment | undefined>"
)]
pub fn build_room_key_bundle(&self, room_id: &identifiers::RoomId) -> Promise {
let _guard = dispatcher::set_default(&self.tracing_subscriber);
let me = self.inner.clone();
let room_id = room_id.inner.clone();

future_to_promise(async move {
let bundle = me.store().build_room_key_bundle(&room_id).await?;

if bundle.is_empty() {
info!("No keys to share");
return Ok(None);
}

// Remember to zeroize the json as soon as we're done with it
let json = Zeroizing::new(serde_json::to_vec(&bundle)?);

Ok(Some(attachment::Attachment::encrypt(json.as_slice())?))
})
}

/// Collect the devices belonging to the given user, and send the details
/// of a room key bundle to those devices.
///
/// Returns a list of to-device requests which must be sent.
///
/// @experimental
#[wasm_bindgen(
js_name = "shareRoomKeyBundleData",
unchecked_return_type = "Promise<ToDeviceRequest[]>"
)]
pub fn share_room_key_bundle_data(
&self,
user: &identifiers::UserId,
room: &identifiers::RoomId,
url: &str,
media_encryption_info: Option<String>,
sharing_strategy: encryption::CollectStrategy,
) -> Result<Promise, JsError> {
let _guard = dispatcher::set_default(&self.tracing_subscriber);
let me = self.inner.clone();
let user_id = user.inner.clone();

let media_encryption_info = media_encryption_info.ok_or_else(|| {
// We accept Option<String> to save a typescript assertion on the application
// side. In practice `build_room_key_bundle` should never return an
// EncryptedAttachment with nullish encryption_info.
JsError::new("shareRoomKeyBundleData: nullish encryption info")
})?;

let media_encryption_info: MediaEncryptionInfo =
serde_json::from_str(&media_encryption_info).map_err(|e| {
JsError::new(&format!("Unable to validate room key media encryption info: {e}"))
})?;

let url = url.try_into().map_err(|e| JsError::new(&format!("Invalid media url: {e}")))?;

let bundle_data = RoomKeyBundleContent {
room_id: room.inner.clone(),
file: EncryptedFile::from(EncryptedFileInit {
url,
key: media_encryption_info.key,
iv: media_encryption_info.iv,
hashes: media_encryption_info.hashes,
v: media_encryption_info.version,
}),
};

Ok(future_to_promise(async move {
let to_device_requests = me
.share_room_key_bundle_data(&user_id, &sharing_strategy.into(), bundle_data)
.await?;

// convert each request to our own ToDeviceRequest struct, and then wrap it in a
// JsValue.
//
// Then collect the results into a javascript Array, throwing any errors into
// the promise.
let result = to_device_requests
.into_iter()
.map(|td| ToDeviceRequest::try_from(&td).map(JsValue::from))
.collect::<Result<Array, _>>()?;

Ok(result)
}))
}

/// See if we have received an {@link https://github.com/matrix-org/matrix-spec-proposals/pull/4268|MSC4268}
/// room key bundle for the given room from the given user.
///
/// Returns either `undefined` if no suitable bundle has been received,
/// or an {@link StoredRoomKeyBundleData}, in which case, the bundle
/// should be downloaded, and then passed to {@link
/// receiveRoomKeyBundle}.
///
/// @experimental
#[wasm_bindgen(
js_name = "getReceivedRoomKeyBundleData",
unchecked_return_type = "Promise<StoredRoomKeyBundleData | undefined>"
)]
pub fn get_received_room_key_bundle_data(
&self,
room_id: &identifiers::RoomId,
inviter: &identifiers::UserId,
) -> Promise {
let _guard = dispatcher::set_default(&self.tracing_subscriber);
let me = self.inner.clone();
let room_id = room_id.inner.clone();
let inviter = inviter.inner.clone();

future_to_promise(async move {
let result = me
.store()
.get_received_room_key_bundle_data(&room_id, &inviter)
.await?
.map(types::StoredRoomKeyBundleData::from);
Ok(result)
})
}

/// Import the message keys from a downloaded room key bundle.
///
/// After {@link getReceivedRoomKeyBundleData} returns a truthy result, the
/// media file should be downloaded and then passed into this method to
/// actually do the import.
///
/// @experimental
#[wasm_bindgen(js_name = "receiveRoomKeyBundle", unchecked_return_type = "Promise<undefined>")]
pub fn receive_room_key_bundle(
&self,
bundle_data: &StoredRoomKeyBundleData,
encrypted_bundle: Vec<u8>,
) -> Result<Promise, JsError> {
let _guard = dispatcher::set_default(&self.tracing_subscriber);

let deserialized_bundle = {
let mut cursor = Cursor::new(encrypted_bundle.as_slice());
let mut decryptor = matrix_sdk_crypto::AttachmentDecryptor::new(
&mut cursor,
serde_json::from_str(&bundle_data.encryption_info)?,
)?;

let mut decrypted_bundle = Zeroizing::new(Vec::new());
decryptor.read_to_end(&mut decrypted_bundle)?;

serde_json::from_slice(&decrypted_bundle)?
};

let me = self.inner.clone();
let bundle_data = bundle_data.clone();
Ok(future_to_promise(async move {
me.store()
.receive_room_key_bundle(
&bundle_data.room_id.inner,
&bundle_data.sender_user.inner,
&bundle_data.sender_data,
deserialized_bundle,
/* TODO: Use the progress listener and expose an argument for it. */
|_, _| {},
)
.await?;
Ok(JsValue::UNDEFINED)
}))
}

/// Shut down the `OlmMachine`.
///
/// The `OlmMachine` cannot be used after this method has been called.
Expand Down
54 changes: 51 additions & 3 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ use std::{

use js_sys::{Array, JsString, Map, Set};
use matrix_sdk_common::ruma::OwnedRoomId;
use matrix_sdk_crypto::backups::{
SignatureState as InnerSignatureState, SignatureVerification as InnerSignatureVerification,
use matrix_sdk_crypto::{
backups::{
SignatureState as InnerSignatureState, SignatureVerification as InnerSignatureVerification,
},
olm::SenderData,
MediaEncryptionInfo,
};
use tracing::warn;
use wasm_bindgen::prelude::*;

use crate::{
encryption::EncryptionAlgorithm,
identifiers::{DeviceKeyId, UserId},
identifiers::{DeviceKeyId, RoomId, UserId},
impl_from_to_inner,
responses::ToDeviceEncryptionInfo,
vodozemac::Ed25519Signature,
Expand Down Expand Up @@ -528,3 +532,47 @@ pub fn processed_to_device_event_to_js_value(
};
Some(result)
}

/// Information on a stored room key bundle data event.
#[wasm_bindgen]
#[derive(Debug, Clone)]
pub struct StoredRoomKeyBundleData {
/// The user that sent us this data.
#[wasm_bindgen(readonly, getter_with_clone, js_name = "senderUser")]
pub sender_user: UserId,

/// Information about the sender of this data and how much we trust that
/// information.
///
/// This is held here because we need the `SenderData` back again in
/// [`OlmMachine::receive_room_key_bundle`]. It's not exposed over the wasm
/// boundary because `SenderData` doesn't implement `Into<JsValue>`.
pub(crate) sender_data: SenderData,

/// The room that these keys are for.
#[wasm_bindgen(readonly, getter_with_clone, js_name = "roomId")]
pub room_id: RoomId,

/// The location of the bundle.
#[wasm_bindgen(readonly, getter_with_clone)]
pub url: String,

/// The JSON-encoded encryption info for the key bundle.
#[wasm_bindgen(readonly, getter_with_clone, js_name = "encryptionInfo")]
pub encryption_info: String,
}

impl From<matrix_sdk_crypto::store::types::StoredRoomKeyBundleData> for StoredRoomKeyBundleData {
fn from(value: matrix_sdk_crypto::store::types::StoredRoomKeyBundleData) -> Self {
let url = value.bundle_data.file.url.to_string();
let encryption_info =
serde_json::to_string(&MediaEncryptionInfo::from(value.bundle_data.file)).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would perhaps be nicer to expect() here.

Self {
sender_user: value.sender_user.into(),
sender_data: value.sender_data,
room_id: value.bundle_data.room_id.into(),
url,
encryption_info,
}
}
}
35 changes: 1 addition & 34 deletions tests/device.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const {
QrCode,
QrCodeScan,
} = require("@matrix-org/matrix-sdk-crypto-wasm");
const { zip, addMachineToMachine } = require("./helper");
const { zip, addMachineToMachine, forwardToDeviceMessage } = require("./helper");
const { VerificationRequestPhase, QrState } = require("@matrix-org/matrix-sdk-crypto-wasm");

// Uncomment to enable debug logging for tests
Expand Down Expand Up @@ -1122,36 +1122,3 @@ describe("VerificationMethod", () => {
expect(VerificationMethod.ReciprocateV1).toStrictEqual(3);
});
});

/**
* Forward an outgoing to-device message returned by one OlmMachine into another OlmMachine.
*/
async function forwardToDeviceMessage(sendingUser, recipientMachine, outgoingVerificationRequest) {
expect(outgoingVerificationRequest).toBeInstanceOf(ToDeviceRequest);
await sendToDeviceMessageIntoMachine(
sendingUser,
outgoingVerificationRequest.event_type,
JSON.parse(outgoingVerificationRequest.body).messages[recipientMachine.userId.toString()][
recipientMachine.deviceId.toString()
],
recipientMachine,
);
}

/**
* Send a to-device message into an OlmMachine.
*/
async function sendToDeviceMessageIntoMachine(sendingUser, eventType, content, recipientMachine) {
await recipientMachine.receiveSyncChanges(
JSON.stringify([
{
sender: sendingUser.toString(),
type: eventType,
content: content,
},
]),
new DeviceLists(),
new Map(),
undefined,
);
}
Loading
Loading