Skip to content

Commit

Permalink
Expose the support to import and export a secrets bundle
Browse files Browse the repository at this point in the history
  • Loading branch information
poljar committed May 21, 2024
1 parent c8540ca commit 3c707d0
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# UNRELEASED

- Add `OlmMachine.importSecretsBundle()` and `OlmMachine.exportSecretsBundle()`
methods as well as the `SecretsBundle` class to import end-to-end encryption
secrets in a bundled manner.

- Expose the vodozemac ECIES support, which can be used to establish the secure
channel required for QR code login described in [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108).

Expand Down
43 changes: 43 additions & 0 deletions src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,49 @@ impl OlmMachine {
})
}

/// Export all the secrets we have in the store into a [`SecretsBundle`].
///
/// This method will export all the private cross-signing keys and, if
/// available, the private part of a backup key and its accompanying
/// version.
///
/// The method will fail if we don't have all three private cross-signing
/// keys available.
///
/// **Warning**: Only export this and share it with a trusted recipient,
/// i.e. if an existing device is sharing this with a new device.
#[wasm_bindgen(js_name = "exportSecretsBundle")]
pub fn export_secrets_bundle(&self) -> Promise {
let me = self.inner.clone();

future_to_promise(async move {
Ok(me.store().export_secrets_bundle().await.map(store::SecretsBundle::from)?)
})
}

/// Import and persists secrets from a [`SecretsBundle`].
///
/// This method will import all the private cross-signing keys and, if
/// available, the private part of a backup key and its accompanying
/// version into the store.
///
/// **Warning**: Only import this from a trusted source, i.e. if an existing
/// device is sharing this with a new device. The imported cross-signing
/// keys will create a [`OwnUserIdentity`] and mark it as verified.
///
/// The backup key will be persisted in the store and can be enabled using
/// the [`BackupMachine`].
#[wasm_bindgen(js_name = "importSecretsBundle")]
pub fn import_secrets_bundle(&self, bundle: store::SecretsBundle) -> Promise {
let me = self.inner.clone();

future_to_promise(async move {
me.store().import_secrets_bundle(&bundle.inner).await?;

Ok(JsValue::null())
})
}

/// Export all the private cross signing keys we have.
///
/// The export will contain the seeds for the ed25519 keys as
Expand Down
71 changes: 70 additions & 1 deletion src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

use std::sync::Arc;

use matrix_sdk_crypto::store::{DynCryptoStore, IntoCryptoStore, MemoryStore};
use matrix_sdk_crypto::{
store::{DynCryptoStore, IntoCryptoStore, MemoryStore},
types::BackupSecrets,
};
use wasm_bindgen::prelude::*;

use crate::{
Expand Down Expand Up @@ -170,3 +173,69 @@ impl RoomKeyInfo {
self.inner.session_id.clone()
}
}

/// Struct containing the bundle of secrets to fully activate a new device for
/// end-to-end encryption.
#[derive(Debug)]
#[wasm_bindgen]
pub struct SecretsBundle {
pub(super) inner: matrix_sdk_crypto::types::SecretsBundle,
}

/// The backup-specific parts of a secrets bundle.
#[derive(Debug)]
#[wasm_bindgen(getter_with_clone)]
pub struct BackupSecretsBundle {
/// The backup decryption key, encoded as unpadded base64.
pub key: String,
/// The backup version which this backup decryption key is used with.
pub backup_version: String,
}

#[wasm_bindgen]
impl SecretsBundle {
/// The seed of the master key encoded as unpadded base64.
#[wasm_bindgen(getter, js_name = "masterKey")]
pub fn master_key(&self) -> String {
self.inner.cross_signing.master_key.clone()
}

/// The seed of the self signing key encoded as unpadded base64.
#[wasm_bindgen(getter, js_name = "selfSigningKey")]
pub fn self_signing_key(&self) -> String {
self.inner.cross_signing.self_signing_key.clone()
}

/// The seed of the user signing key encoded as unpadded base64.
#[wasm_bindgen(getter, js_name = "userSigningKey")]
pub fn user_signing_key(&self) -> String {
self.inner.cross_signing.user_signing_key.clone()
}

/// The bundle of the backup decryption key and backup version if any.
#[wasm_bindgen(getter, js_name = "backupBundle")]
pub fn backup_bundle(&self) -> Option<BackupSecretsBundle> {
if let Some(BackupSecrets::MegolmBackupV1Curve25519AesSha2(backup)) = &self.inner.backup {
Some(BackupSecretsBundle {
key: backup.key.to_base64(),
backup_version: backup.backup_version.clone(),
})
} else {
None
}
}

/// Serialize the [`SecretsBundle`] to a JSON object.
pub fn to_json(&self) -> Result<JsValue, JsError> {
Ok(serde_wasm_bindgen::to_value(&self.inner)?)
}

/// Deserialize the [`SecretsBundle`] from a JSON object.
pub fn from_json(json: JsValue) -> Result<SecretsBundle, JsError> {
let bundle = serde_wasm_bindgen::from_value(json)?;

Ok(Self { inner: bundle })
}
}

impl_from_to_inner!(matrix_sdk_crypto::types::SecretsBundle => SecretsBundle);
64 changes: 63 additions & 1 deletion tests/ecies.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Ecies } = require("../pkg/matrix_sdk_crypto_wasm");
const { Ecies, SecretsBundle, UserId, DeviceId, OlmMachine, RequestType } = require("../pkg/matrix_sdk_crypto_wasm");

describe(Ecies.name, () => {
test("can establish a channel and decrypt the initial message", () => {
Expand Down Expand Up @@ -26,3 +26,65 @@ describe(Ecies.name, () => {
expect(second_plaintext).toStrictEqual("Other message");
});
});

describe(SecretsBundle.name, () => {
test("can deserialize a secrets bundle", async () => {
const json = {
type: "m.login.secrets",
cross_signing: {
master_key: "bMnVpkHI4S2wXRxy+IpaKM5PIAUUkl6DE+n0YLIW/qs",
user_signing_key: "8tlgLjUrrb/zGJo4YKGhDTIDCEjtJTAS/Sh2AGNLuIo",
self_signing_key: "pfDknmP5a0fVVRE54zhkUgJfzbNmvKcNfIWEW796bQs",
},
backup: {
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
key: "bYYv3aFLQ49jMNcOjuTtBY9EKDby2x1m3gfX81nIKRQ",
backup_version: "9",
},
};

const cycle = JSON.parse(JSON.stringify(json));
const bundle = SecretsBundle.from_json(cycle);

expect(bundle.masterKey).toStrictEqual("bMnVpkHI4S2wXRxy+IpaKM5PIAUUkl6DE+n0YLIW/qs");
});

test("can import a secrets bundle", async () => {
const userId = new UserId("@alice:example.org");
const firstDevice = new DeviceId("ABCDEF");
const secondDevice = new DeviceId("DEVICE2");

const firstMachine = await OlmMachine.initialize(userId, firstDevice);
const secondMachine = await OlmMachine.initialize(userId, secondDevice);

await firstMachine.bootstrapCrossSigning(false);
const bundle = await firstMachine.exportSecretsBundle();

const alice = new Ecies();
const bob = new Ecies();

const json_bundle = bundle.to_json();

const { initial_message } = alice.establish_outbound_channel(bob.public_key(), JSON.stringify(json_bundle));
const { message } = bob.establish_inbound_channel(initial_message);

const deserialize_message = JSON.parse(message);
const received_bundle = SecretsBundle.from_json(deserialize_message);

await secondMachine.importSecretsBundle(received_bundle);

const crossSigningStatus = await secondMachine.crossSigningStatus();
expect(crossSigningStatus.hasMaster).toStrictEqual(true);
expect(crossSigningStatus.hasSelfSigning).toStrictEqual(true);
expect(crossSigningStatus.hasUserSigning).toStrictEqual(true);

const exported_bundle = await secondMachine.exportSecretsBundle();

expect(exported_bundle.masterKey).toStrictEqual(bundle.masterKey);
expect(exported_bundle.selfSigningKey).toStrictEqual(bundle.selfSigningKey);
expect(exported_bundle.userSigningKey).toStrictEqual(bundle.userSigningKey);

const identity = await secondMachine.getIdentity(userId);
expect(identity.isVerified).toBeTruthy();
});
});

0 comments on commit 3c707d0

Please sign in to comment.