feat(crypto-js): Implement `OlmMachine.export_room_keys` and `.import_room_keys`
feat(crypto-js): Implement `OlmMachine.export_room_keys` and `.import_room_keys`pull/1064/head
commit
f8b02d1a11
|
@ -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::{
|
||||
|
@ -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,
|
||||
)?)?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -5,6 +5,7 @@ const {
|
|||
DeviceKeyId,
|
||||
DeviceLists,
|
||||
EncryptionSettings,
|
||||
InboundGroupSession,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
|
@ -19,6 +20,7 @@ const {
|
|||
VerificationRequest,
|
||||
VerificationState,
|
||||
} = require('../pkg/matrix_sdk_crypto_js');
|
||||
const { addMachineToMachine } = require('./helper');
|
||||
require('fake-indexeddb/auto');
|
||||
|
||||
describe(OlmMachine.name, () => {
|
||||
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue