Skip to content

feat(crypto-js): Implement OlmMachine.export_room_keys and .import_room_keys #1059

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

Merged
merged 7 commits into from
Sep 27, 2022
106 changes: 104 additions & 2 deletions bindings/matrix-sdk-crypto-js/src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

use std::collections::BTreeMap;

use js_sys::{Array, Map, Promise, Set};
use js_sys::{Array, Function, Map, Promise, Set};
use ruma::{serde::Raw, DeviceKeyAlgorithm, OwnedTransactionId, UInt};
use serde_json::Value as JsonValue;
use serde_json::{json, Value as JsonValue};
use wasm_bindgen::prelude::*;

use crate::{
Expand Down Expand Up @@ -626,4 +626,106 @@ impl OlmMachine {
.map(|_| JsValue::UNDEFINED)?)
}))
}

/// Export the keys that match the given predicate.
///
/// `predicate` is a closure that will be called for every known
/// `InboundGroupSession`, which represents a room key. If the closure
/// returns `true`, the `InboundGroupSession` will be included in the
/// export, otherwise it won't.
#[wasm_bindgen(js_name = "exportRoomKeys")]
pub fn export_room_keys(&self, predicate: Function) -> Promise {
let me = self.inner.clone();

future_to_promise(async move {
Ok(serde_json::to_string(
&me.export_room_keys(|session| {
let session = session.clone();

predicate
.call1(&JsValue::NULL, &olm::InboundGroupSession::from(session).into())
.expect("Predicate function passed to `export_room_keys` failed")
.as_bool()
.unwrap_or(false)
})
.await?,
)?)
})
}

/// Import the given room keys into our store.
///
/// `exported_keys` is a list of previously exported keys that should be
/// imported into our store. If we already have a better version of a key,
/// the key will _not_ be imported.
///
/// `progress_listener` is a closure that takes 2 arguments: `progress` and
/// `total`, and returns nothing.
#[wasm_bindgen(js_name = "importRoomKeys")]
pub fn import_room_keys(
&self,
exported_room_keys: &str,
progress_listener: Function,
) -> Result<Promise, JsError> {
let me = self.inner.clone();
let exported_room_keys: Vec<matrix_sdk_crypto::olm::ExportedRoomKey> =
serde_json::from_str(exported_room_keys)?;

Ok(future_to_promise(async move {
let matrix_sdk_crypto::RoomKeyImportResult { imported_count, total_count, keys } = me
.import_room_keys(exported_room_keys, false, |progress, total| {
let progress: u64 = progress.try_into().unwrap();
let total: u64 = total.try_into().unwrap();

progress_listener
.call2(&JsValue::NULL, &JsValue::from(progress), &JsValue::from(total))
.expect("Progress listener passed to `import_room_keys` failed");
})
.await?;

Ok(serde_json::to_string(&json!({
"imported_count": imported_count,
"total_count": total_count,
"keys": keys,
}))?)
}))
}

/// Encrypt the list of exported room keys using the given passphrase.
///
/// `exported_room_keys` is a list of sessions that should be encrypted
/// (it's generally returned by `export_room_keys`). `passphrase` is the
/// passphrase that will be used to encrypt the exported room keys. And
/// `rounds` is the number of rounds that should be used for the key
/// derivation when the passphrase gets turned into an AES key. More rounds
/// are increasingly computationnally intensive and as such help against
/// brute-force attacks. Should be at least `10_000`, while values in the
/// `100_000` ranges should be preferred.
#[wasm_bindgen(js_name = "encryptExportedRoomKeys")]
pub fn encrypt_exported_room_keys(
exported_room_keys: &str,
passphrase: &str,
rounds: u32,
) -> Result<String, JsError> {
let exported_room_keys: Vec<matrix_sdk_crypto::olm::ExportedRoomKey> =
serde_json::from_str(exported_room_keys)?;

Ok(matrix_sdk_crypto::encrypt_room_key_export(&exported_room_keys, passphrase, rounds)?)
}

/// Try to decrypt a reader into a list of exported room keys.
///
/// `encrypted_exported_room_keys` is the result from
/// `encrypt_exported_room_keys`. `passphrase` is the passphrase that was
/// used when calling `encrypt_exported_room_keys`.
#[wasm_bindgen(js_name = "decryptExportedRoomKeys")]
pub fn decrypt_exported_room_keys(
encrypted_exported_room_keys: &str,
passphrase: &str,
) -> Result<String, JsError> {
Ok(serde_json::to_string(&matrix_sdk_crypto::decrypt_room_key_export(
encrypted_exported_room_keys.as_bytes(),
passphrase,
)?)?)
}
}
36 changes: 35 additions & 1 deletion bindings/matrix-sdk-crypto-js/src/olm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use wasm_bindgen::prelude::*;

use crate::impl_from_to_inner;
use crate::{identifiers, impl_from_to_inner};

/// Struct representing the state of our private cross signing keys,
/// it shows which private cross signing keys we have locally stored.
Expand Down Expand Up @@ -36,3 +36,37 @@ impl CrossSigningStatus {
self.inner.has_user_signing
}
}

/// Inbound group session.
///
/// Inbound group sessions are used to exchange room messages between a group of
/// participants. Inbound group sessions are used to decrypt the room messages.
#[wasm_bindgen]
#[derive(Debug)]
pub struct InboundGroupSession {
inner: matrix_sdk_crypto::olm::InboundGroupSession,
}

impl_from_to_inner!(matrix_sdk_crypto::olm::InboundGroupSession => InboundGroupSession);

#[wasm_bindgen]
impl InboundGroupSession {
/// The room where this session is used in.
#[wasm_bindgen(getter, js_name = "roomId")]
pub fn room_id(&self) -> identifiers::RoomId {
self.inner.room_id().to_owned().into()
}

/// Returns the unique identifier for this session.
#[wasm_bindgen(getter, js_name = "sessionId")]
pub fn session_id(&self) -> String {
self.inner.session_id().to_owned()
}

/// Has the session been imported from a file or server-side backup? As
/// opposed to being directly received as an `m.room_key` event.
#[wasm_bindgen(js_name = "hasBeenImported")]
pub fn has_been_imported(&self) -> bool {
self.inner.has_been_imported()
}
}
1 change: 0 additions & 1 deletion bindings/matrix-sdk-crypto-js/tests/device.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const {
QrCode,
QrCodeScan,
} = require('../pkg/matrix_sdk_crypto_js');
const { LoggerLevel, Tracing } = require('../pkg/matrix_sdk_crypto_js');
const { zip, addMachineToMachine } = require('./helper');

describe('LocalTrust', () => {
Expand Down
75 changes: 75 additions & 0 deletions bindings/matrix-sdk-crypto-js/tests/machine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
DeviceKeyId,
DeviceLists,
EncryptionSettings,
InboundGroupSession,
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
Expand All @@ -19,6 +20,7 @@ const {
VerificationRequest,
VerificationState,
} = require('../pkg/matrix_sdk_crypto_js');
const { addMachineToMachine } = require('./helper');
require('fake-indexeddb/auto');

describe(OlmMachine.name, () => {
Expand Down Expand Up @@ -491,4 +493,77 @@ describe(OlmMachine.name, () => {

expect(isTrusted).toStrictEqual(false);
});

describe('can export/import room keys', () => {
let m;
let exportedRoomKeys;

test('can export room keys', async () => {
m = await machine();
await m.shareRoomKey(room, [new UserId('@bob:example.org')], new EncryptionSettings());

exportedRoomKeys = await m.exportRoomKeys(session => {
expect(session).toBeInstanceOf(InboundGroupSession);
expect(session.roomId.toString()).toStrictEqual(room.toString());
expect(session.sessionId).toBeDefined();
expect(session.hasBeenImported()).toStrictEqual(false);

return true;
});

const roomKeys = JSON.parse(exportedRoomKeys);
expect(roomKeys).toHaveLength(1);
expect(roomKeys[0]).toMatchObject({
algorithm: expect.any(String),
room_id: room.toString(),
sender_key: expect.any(String),
session_id: expect.any(String),
session_key: expect.any(String),
sender_claimed_keys: {
ed25519: expect.any(String),
},
forwarding_curve25519_key_chain: [],
});
});

let encryptedExportedRoomKeys;
let encryptionPassphrase = 'Hello, Matrix!';

test('can encrypt the exported room keys', () => {
encryptedExportedRoomKeys = OlmMachine.encryptExportedRoomKeys(
exportedRoomKeys,
encryptionPassphrase,
100_000,
);

expect(encryptedExportedRoomKeys).toMatch(/^-----BEGIN MEGOLM SESSION DATA-----/);
});

test('can decrypt the exported room keys', () => {
const decryptedExportedRoomKeys = OlmMachine.decryptExportedRoomKeys(
encryptedExportedRoomKeys,
encryptionPassphrase,
);

expect(decryptedExportedRoomKeys).toStrictEqual(exportedRoomKeys);
});

test('can import room keys', async () => {
const progressListener = (progress, total) => {
expect(progress).toBeLessThan(total);

// Since it's called only once, let's be crazy.
expect(progress).toStrictEqual(0n);
expect(total).toStrictEqual(1n);
};

const result = JSON.parse(await m.importRoomKeys(exportedRoomKeys, progressListener));

expect(result).toMatchObject({
imported_count: expect.any(Number),
total_count: expect.any(Number),
keys: expect.any(Object),
});
});
});
});