Skip to content

Commit 09f02f3

Browse files
committed
Expose the support to import and export a secrets bundle
1 parent ea173ca commit 09f02f3

File tree

4 files changed

+180
-2
lines changed

4 files changed

+180
-2
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# UNRELEASED
22

3+
- Add `OlmMachine.importSecretsBundle()` and `OlmMachine.exportSecretsBundle()`
4+
methods as well as the `SecretsBundle` class to import end-to-end encryption
5+
secrets in a bundled manner.
6+
37
- Expose the vodozemac ECIES support, which can be used to establish the secure
48
channel required for QR code login described in [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108).
59

src/machine.rs

+43
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,49 @@ impl OlmMachine {
534534
})
535535
}
536536

537+
/// Export all the secrets we have in the store into a [`SecretsBundle`].
538+
///
539+
/// This method will export all the private cross-signing keys and, if
540+
/// available, the private part of a backup key and its accompanying
541+
/// version.
542+
///
543+
/// The method will fail if we don't have all three private cross-signing
544+
/// keys available.
545+
///
546+
/// **Warning**: Only export this and share it with a trusted recipient,
547+
/// i.e. if an existing device is sharing this with a new device.
548+
#[wasm_bindgen(js_name = "exportSecretsBundle")]
549+
pub fn export_secrets_bundle(&self) -> Promise {
550+
let me = self.inner.clone();
551+
552+
future_to_promise(async move {
553+
Ok(me.store().export_secrets_bundle().await.map(store::SecretsBundle::from)?)
554+
})
555+
}
556+
557+
/// Import and persists secrets from a [`SecretsBundle`].
558+
///
559+
/// This method will import all the private cross-signing keys and, if
560+
/// available, the private part of a backup key and its accompanying
561+
/// version into the store.
562+
///
563+
/// **Warning**: Only import this from a trusted source, i.e. if an existing
564+
/// device is sharing this with a new device. The imported cross-signing
565+
/// keys will create a [`OwnUserIdentity`] and mark it as verified.
566+
///
567+
/// The backup key will be persisted in the store and can be enabled using
568+
/// the [`BackupMachine`].
569+
#[wasm_bindgen(js_name = "importSecretsBundle")]
570+
pub fn import_secrets_bundle(&self, bundle: store::SecretsBundle) -> Promise {
571+
let me = self.inner.clone();
572+
573+
future_to_promise(async move {
574+
me.store().import_secrets_bundle(&bundle.inner).await?;
575+
576+
Ok(JsValue::null())
577+
})
578+
}
579+
537580
/// Export all the private cross signing keys we have.
538581
///
539582
/// The export will contain the seeds for the ed25519 keys as

src/store.rs

+70-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
33
use std::sync::Arc;
44

5-
use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore, MemoryStore};
5+
use matrix_sdk_crypto::{
6+
store::{DynCryptoStore, IntoCryptoStore, MemoryStore},
7+
types::BackupSecrets,
8+
};
69
use wasm_bindgen::prelude::*;
710

811
use crate::{
@@ -170,3 +173,69 @@ impl RoomKeyInfo {
170173
self.inner.session_id.clone()
171174
}
172175
}
176+
177+
/// Struct containing the bundle of secrets to fully activate a new devices for
178+
/// end-to-end encryption.
179+
#[derive(Debug)]
180+
#[wasm_bindgen]
181+
pub struct SecretsBundle {
182+
pub(super) inner: matrix_sdk_crypto::types::SecretsBundle,
183+
}
184+
185+
/// The backup-specific parts of a secrets bundle.
186+
#[derive(Debug)]
187+
#[wasm_bindgen(getter_with_clone)]
188+
pub struct BackupSecretsBundle {
189+
/// The backup decryption key.
190+
pub key: String,
191+
/// The backup version which this backup decryption key is used with.
192+
pub backup_version: String,
193+
}
194+
195+
#[wasm_bindgen]
196+
impl SecretsBundle {
197+
/// The seed of the master key encoded as unpadded base64.
198+
#[wasm_bindgen(getter, js_name = "masterKey")]
199+
pub fn master_key(&self) -> String {
200+
self.inner.cross_signing.master_key.clone()
201+
}
202+
203+
/// The seed of the self signing key encoded as unpadded base64.
204+
#[wasm_bindgen(getter, js_name = "selfSigningKey")]
205+
pub fn self_signing_key(&self) -> String {
206+
self.inner.cross_signing.self_signing_key.clone()
207+
}
208+
209+
/// The seed of the user signing key encoded as unpadded base64.
210+
#[wasm_bindgen(getter, js_name = "userSigningKey")]
211+
pub fn user_signing_key(&self) -> String {
212+
self.inner.cross_signing.user_signing_key.clone()
213+
}
214+
215+
/// The bundle of the backup decryption key and backup version if any.
216+
#[wasm_bindgen(getter, js_name = "backupBundle")]
217+
pub fn backup_bundle(&self) -> Option<BackupSecretsBundle> {
218+
if let Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(backup)) = &self.inner.backup {
219+
Some(BackupSecretsBundle {
220+
key: backup.key.to_base64(),
221+
backup_version: backup.backup_version.clone(),
222+
})
223+
} else {
224+
None
225+
}
226+
}
227+
228+
/// Serialize the [`SecretsBundle`] to a JSON object.
229+
pub fn to_json(&self) -> Result<JsValue, JsError> {
230+
Ok(serde_wasm_bindgen::to_value(&self.inner)?)
231+
}
232+
233+
/// Deserialize the [`SecretsBundle`] from a JSON object.
234+
pub fn from_json(json: JsValue) -> Result<SecretsBundle, JsError> {
235+
let bundle = serde_wasm_bindgen::from_value(json)?;
236+
237+
Ok(Self { inner: bundle })
238+
}
239+
}
240+
241+
impl_from_to_inner!(matrix_sdk_crypto::types::SecretsBundle => SecretsBundle);

tests/ecies.test.ts

+63-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { Ecies } = require("../pkg/matrix_sdk_crypto_wasm");
1+
const { Ecies, SecretsBundle, UserId, DeviceId, OlmMachine, RequestType } = require("../pkg/matrix_sdk_crypto_wasm");
22

33
describe(Ecies.name, () => {
44
test("can establish a channel and decrypt the initial message", () => {
@@ -26,3 +26,65 @@ describe(Ecies.name, () => {
2626
expect(second_plaintext).toStrictEqual("Other message");
2727
});
2828
});
29+
30+
describe(SecretsBundle.name, () => {
31+
test("can deserialize a secrets bundle", async () => {
32+
const json = {
33+
type: "m.login.secrets",
34+
cross_signing: {
35+
master_key: "bMnVpkHI4S2wXRxy+IpaKM5PIAUUkl6DE+n0YLIW/qs",
36+
user_signing_key: "8tlgLjUrrb/zGJo4YKGhDTIDCEjtJTAS/Sh2AGNLuIo",
37+
self_signing_key: "pfDknmP5a0fVVRE54zhkUgJfzbNmvKcNfIWEW796bQs",
38+
},
39+
backup: {
40+
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
41+
key: "bYYv3aFLQ49jMNcOjuTtBY9EKDby2x1m3gfX81nIKRQ",
42+
backup_version: "9",
43+
},
44+
};
45+
46+
const cycle = JSON.parse(JSON.stringify(json));
47+
const bundle = SecretsBundle.from_json(cycle);
48+
49+
expect(bundle.masterKey).toStrictEqual("bMnVpkHI4S2wXRxy+IpaKM5PIAUUkl6DE+n0YLIW/qs");
50+
});
51+
52+
test("can import a secrets bundle", async () => {
53+
const userId = new UserId("@alice:example.org");
54+
const firstDevice = new DeviceId("ABCDEF");
55+
const secondDevice = new DeviceId("DEVICE2");
56+
57+
const firstMachine = await OlmMachine.initialize(userId, firstDevice);
58+
const secondMachine = await OlmMachine.initialize(userId, secondDevice);
59+
60+
await firstMachine.bootstrapCrossSigning(false);
61+
const bundle = await firstMachine.exportSecretsBundle();
62+
63+
const alice = new Ecies();
64+
const bob = new Ecies();
65+
66+
const json_bundle = bundle.to_json();
67+
68+
const { initial_message } = alice.establish_outbound_channel(bob.public_key(), JSON.stringify(json_bundle));
69+
const { message } = bob.establish_inbound_channel(initial_message);
70+
71+
const deserialize_message = JSON.parse(message);
72+
const received_bundle = SecretsBundle.from_json(deserialize_message);
73+
74+
await secondMachine.importSecretsBundle(received_bundle);
75+
76+
const crossSigningStatus = await secondMachine.crossSigningStatus();
77+
expect(crossSigningStatus.hasMaster).toStrictEqual(true);
78+
expect(crossSigningStatus.hasSelfSigning).toStrictEqual(true);
79+
expect(crossSigningStatus.hasUserSigning).toStrictEqual(true);
80+
81+
const exported_bundle = await secondMachine.exportSecretsBundle();
82+
83+
expect(exported_bundle.masterKey).toStrictEqual(bundle.masterKey);
84+
expect(exported_bundle.selfSigningKey).toStrictEqual(bundle.selfSigningKey);
85+
expect(exported_bundle.userSigningKey).toStrictEqual(bundle.userSigningKey);
86+
87+
const identity = await secondMachine.getIdentity(userId);
88+
expect(identity.isVerified).toBeTruthy();
89+
});
90+
});

0 commit comments

Comments
 (0)