From 0589fd6720de46446c20eedf7dded584222946f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 15 May 2024 15:11:06 +0200 Subject: [PATCH 1/3] Expose the QR code parsing structs used by the QR login mechanism --- CHANGELOG.md | 4 ++ Cargo.lock | 1 + Cargo.toml | 1 + src/lib.rs | 1 + src/qr_login.rs | 141 ++++++++++++++++++++++++++++++++++++++++++ tests/qr_code.test.js | 36 +++++++++++ 6 files changed, 184 insertions(+) create mode 100644 src/qr_login.rs create mode 100644 tests/qr_code.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index cf4081754..e23e672a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # UNRELEASED +- Add `QrCodeData` and `QrCodeMode` classes which can be used to parse or + generate QR codes intended for the QR code login mechanism described in + [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108). + - Update matrix-rust-sdk to `35173347f`, which includes: - Add data types to parse the QR code data for the QR code login defined in diff --git a/Cargo.lock b/Cargo.lock index 05ff6f67e..dbbe31bf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -924,6 +924,7 @@ dependencies = [ "serde_json", "tracing", "tracing-subscriber", + "url", "vergen", "wasm-bindgen", "wasm-bindgen-futures", diff --git a/Cargo.toml b/Cargo.toml index a72bf9dfe..761e4d42b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ serde_json = "1.0.91" serde-wasm-bindgen = "0.5.0" tracing = { version = "0.1.36", default-features = false, features = ["std"] } tracing-subscriber = { version = "0.3.14", default-features = false, features = ["registry", "std", "ansi"] } +url = "2.5.0" wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.33" zeroize = "1.6.0" diff --git a/src/lib.rs b/src/lib.rs index 1a2afa434..c0aed2441 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub mod libolm_migration; pub mod machine; mod macros; pub mod olm; +pub mod qr_login; pub mod requests; pub mod responses; pub mod store; diff --git a/src/qr_login.rs b/src/qr_login.rs new file mode 100644 index 000000000..4bd8f2679 --- /dev/null +++ b/src/qr_login.rs @@ -0,0 +1,141 @@ +//! Types for QR code login + +use matrix_sdk_crypto::types::qr_login; +use url::Url; +use wasm_bindgen::prelude::*; + +use crate::vodozemac::Curve25519PublicKey; + +/// The mode of the QR code login. +/// +/// The QR code login mechanism supports both, the new device, as well as the +/// existing device to display the QR code. +/// +/// The different modes have an explicit one-byte identifier which gets added to +/// the QR code data. +#[wasm_bindgen] +#[derive(Debug)] +pub enum QrCodeMode { + /// The new device is displaying the QR code. + Login, + /// The existing device is displaying the QR code. + Reciprocate, +} + +impl From for QrCodeMode { + fn from(value: qr_login::QrCodeMode) -> Self { + match value { + qr_login::QrCodeMode::Login => Self::Login, + qr_login::QrCodeMode::Reciprocate => Self::Reciprocate, + } + } +} + +/// Data for the QR code login mechanism. +/// +/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be +/// decoded from a QR code. +#[wasm_bindgen] +#[derive(Debug)] +pub struct QrCodeData { + inner: qr_login::QrCodeData, +} + +#[wasm_bindgen] +impl QrCodeData { + /// Create new [`QrCodeData`] from a given public key, a rendezvous URL and, + /// optionally, a homeserver URL. + /// + /// If a homeserver URL is given, then the [`QrCodeData`] mode will be + /// [`QrCodeMode::Reciprocate`], i.e. the QR code will contain data for the + /// existing device to display the QR code. + /// + /// If no homeserver is given, the [`QrCodeData`] mode will be + /// [`QrCodeMode::Login`], i.e. the QR code will contain data for the + /// new device to display the QR code. + #[wasm_bindgen(constructor)] + pub fn new( + public_key: Curve25519PublicKey, + rendezvous_url: &str, + homeserver_url: Option, + ) -> Result { + let public_key = public_key.inner; + let rendezvous_url = Url::parse(rendezvous_url)?; + + let mode_data = if let Some(homeserver_url) = homeserver_url { + qr_login::QrCodeModeData::Reciprocate { homeserver_url: Url::parse(&homeserver_url)? } + } else { + qr_login::QrCodeModeData::Login + }; + + let inner = qr_login::QrCodeData { public_key, rendezvous_url, mode_data }; + + Ok(QrCodeData { inner }) + } + + /// Attempt to decode a slice of bytes into a [`QrCodeData`] object. + /// + /// The slice of bytes would generally be returned by a QR code decoder. + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Self { inner: qr_login::QrCodeData::from_bytes(bytes)? }) + } + + /// Encode the [`QrCodeData`] into a list of bytes. + /// + /// The list of bytes can be used by a QR code generator to create an image + /// containing a QR code. + pub fn to_bytes(&self) -> Vec { + self.inner.to_bytes() + } + + /// Attempt to decode a base64 encoded string into a [`QrCodeData`] object. + pub fn from_base64(data: &str) -> Result { + Ok(Self { inner: qr_login::QrCodeData::from_base64(data)? }) + } + + /// Encode the [`QrCodeData`] into a string using base64. + /// + /// This format can be used for debugging purposes and the + /// [`QrcodeData::from_base64()`] method can be used to parse the string + /// again. + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } + + /// Get the Curve25519 public key embedded in the [`QrCodeData`]. + /// + /// This Curve25519 public key should be used to establish an + /// [ECIES](https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme) + /// (Elliptic Curve Integrated Encryption Scheme) channel with the other + /// device. + #[wasm_bindgen(getter)] + pub fn public_key(&self) -> Curve25519PublicKey { + self.inner.public_key.into() + } + + /// Get the URL of the rendezvous server which will be used to exchange + /// messages between the two devices. + #[wasm_bindgen(getter)] + pub fn rendezvous_url(&self) -> String { + self.inner.rendezvous_url.as_str().to_owned() + } + + /// Get the homeserver URL which the new device will be logged in to. + /// + /// This will be only available if the existing device has generated the QR + /// code and the new device is the one scanning the QR code. + #[wasm_bindgen(getter)] + pub fn homeserver_url(&self) -> Option { + if let qr_login::QrCodeModeData::Reciprocate { homeserver_url } = &self.inner.mode_data { + Some(homeserver_url.as_str().to_owned()) + } else { + None + } + } + + /// Get the mode of this [`QrCodeData`] instance. + #[wasm_bindgen(getter)] + pub fn mode(&self) -> QrCodeMode { + self.inner.mode().into() + } +} diff --git a/tests/qr_code.test.js b/tests/qr_code.test.js new file mode 100644 index 000000000..3d6a3d535 --- /dev/null +++ b/tests/qr_code.test.js @@ -0,0 +1,36 @@ +const { QrCodeData, QrCodeMode, Curve25519PublicKey } = require("../pkg/matrix_sdk_crypto_wasm"); + +describe(QrCodeData.name, () => { + test("can parse the QR code bytes from the MSC", () => { + const base64Data = + "TUFUUklYAgPYhmhqshl7eA4wCp1KIUdIBwDXkp85qzG55RQ3AkjtawBHaHR0cHM6Ly9yZW5kZXp2b3VzLmxhYi5lbGVtZW50LmRldi9lOGRhNjM1NS01NTBiLTRhMzItYTE5My0xNjE5ZDk4MzA2Njg"; + + const data = QrCodeData.from_base64(base64Data); + + expect(data.public_key.toBase64()).toStrictEqual("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws"); + expect(data.rendezvous_url).toStrictEqual( + "https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668", + ); + expect(data.mode).toStrictEqual(QrCodeMode.Login); + + const encoded = data.to_base64(); + + expect(base64Data).toStrictEqual(encoded); + }); + + test("can construct a new QrCodeData class", () => { + const base64Data = + "TUFUUklYAgPYhmhqshl7eA4wCp1KIUdIBwDXkp85qzG55RQ3AkjtawBHaHR0cHM6Ly9yZW5kZXp2b3VzLmxhYi5lbGVtZW50LmRldi9lOGRhNjM1NS01NTBiLTRhMzItYTE5My0xNjE5ZDk4MzA2Njg"; + const publicKey = new Curve25519PublicKey("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws"); + const rendezvousUrl = "https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668"; + + const data = new QrCodeData(publicKey, rendezvousUrl); + + expect(data.public_key.toBase64()).toStrictEqual("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws"); + expect(data.rendezvous_url).toStrictEqual(rendezvousUrl); + expect(data.mode).toStrictEqual(QrCodeMode.Login); + + const encoded = data.to_base64(); + expect(base64Data).toStrictEqual(encoded); + }); +}); From 749927ef8ce4e0386a558c16dc5e6a82abbb70c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 15 May 2024 15:13:46 +0200 Subject: [PATCH 2/3] Expose the ECIES support from vodozemac --- CHANGELOG.md | 3 + src/vodozemac/ecies.rs | 211 +++++++++++++++++++++++++++++++++++++++++ src/vodozemac/mod.rs | 2 + tests/ecies.test.ts | 28 ++++++ 4 files changed, 244 insertions(+) create mode 100644 src/vodozemac/ecies.rs create mode 100644 tests/ecies.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e23e672a8..64ba9e497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # UNRELEASED +- 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). + - Add `QrCodeData` and `QrCodeMode` classes which can be used to parse or generate QR codes intended for the QR code login mechanism described in [MSC4108](https://github.com/matrix-org/matrix-spec-proposals/pull/4108). diff --git a/src/vodozemac/ecies.rs b/src/vodozemac/ecies.rs new file mode 100644 index 000000000..f37d7cab4 --- /dev/null +++ b/src/vodozemac/ecies.rs @@ -0,0 +1,211 @@ +//! This module implements [ECIES](https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme), the +//! elliptic curve variant of the Integrated Encryption Scheme. +//! +//! Please take a look at the vodozemac documentation of this module for more +//! info. + +#![allow(missing_debug_implementations)] +use std::sync::{Arc, Mutex}; + +use matrix_sdk_crypto::vodozemac::ecies; +use wasm_bindgen::prelude::*; + +use super::Curve25519PublicKey; + +/// The result of an inbound ECIES channel establishment. +#[wasm_bindgen(getter_with_clone)] +pub struct InboundCreationResult { + /// The established ECIES channel. + pub channel: EstablishedEcies, + /// The plaintext of the initial message. + pub message: String, +} + +/// The result of an outbound ECIES channel establishment. +#[wasm_bindgen(getter_with_clone)] +pub struct OutboundCreationResult { + /// The established ECIES channel. + pub channel: EstablishedEcies, + /// The initial encrypted message. + pub initial_message: String, +} + +/// An unestablished ECIES session. +#[wasm_bindgen] +pub struct Ecies { + inner: Option, + public_key: Curve25519PublicKey, +} + +#[wasm_bindgen] +impl Ecies { + /// Create a new, random, unestablished ECIES session. + /// + /// This method will use the + /// [`MATRIX_QR_CODE_LOGIN`](https://github.com/matrix-org/matrix-spec-proposals/pull/4108) + /// info for domain separation when creating the session. + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + let inner = ecies::Ecies::new(); + let public_key = inner.public_key().into(); + + Self { inner: Some(inner), public_key } + } + + /// Get our [`Curve25519PublicKey`]. + /// + /// This public key needs to be sent to the other side to be able to + /// establish an ECIES channel. + pub fn public_key(&self) -> Curve25519PublicKey { + self.public_key.clone() + } + + fn used_up_error() -> JsError { + JsError::new("The ECIES channel was already established and used up.") + } + + /// Create a [`EstablishedEcies`] from an initial message encrypted by the + /// other side. + pub fn establish_inbound_channel( + &mut self, + initial_message: &str, + ) -> Result { + let message = ecies::InitialMessage::decode(&initial_message)?; + let result = self + .inner + .take() + .ok_or_else(Self::used_up_error)? + .establish_inbound_channel(&message)?; + + let message = String::from_utf8_lossy(&result.message).to_string(); + + Ok(InboundCreationResult { message, channel: result.ecies.into() }) + } + + /// Create an [`EstablishedEcies`] session using the other side's Curve25519 + /// public key and an initial plaintext. + /// + /// After the channel has been established, we can encrypt messages to send + /// to the other side. The other side uses the initial message to + /// establishes the same channel on its side. + pub fn establish_outbound_channel( + &mut self, + public_key: &Curve25519PublicKey, + initial_message: &str, + ) -> Result { + let result = self + .inner + .take() + .ok_or_else(Self::used_up_error)? + .establish_outbound_channel(public_key.inner, initial_message.as_bytes())?; + + Ok(OutboundCreationResult { + initial_message: result.message.encode(), + channel: result.ecies.into(), + }) + } +} + +/// An established ECIES session. +/// +/// This session can be used to encrypt and decrypt messages between the two +/// sides of the channel. +#[derive(Clone)] +#[wasm_bindgen] +pub struct EstablishedEcies { + inner: Arc>, +} + +#[wasm_bindgen] +impl EstablishedEcies { + /// Get our [`Curve25519PublicKey`]. + /// + /// This public key needs to be sent to the other side so that it can + /// complete the ECIES channel establishment. + pub fn public_key(&self) -> Curve25519PublicKey { + self.inner.lock().unwrap().public_key().into() + } + + /// Encrypt the given plaintext using this [`EstablishedEcies`] session. + pub fn encrypt(&mut self, message: &str) -> String { + self.inner.lock().unwrap().encrypt(message.as_bytes()).encode() + } + + /// Decrypt the given message using this [`EstablishedEcies`] session. + pub fn decrypt(&mut self, message: &str) -> Result { + let message = ecies::Message::decode(message)?; + let result = self.inner.lock().unwrap().decrypt(&message)?; + + Ok(String::from_utf8_lossy(&result).to_string()) + } + + /// Get the [`CheckCode`] which uniquely identifies this + /// [`EstablishedEcies`] session. + /// + /// This check code can be used to verify and confirm that both sides of the + /// session are indeed using the same shared secret. + pub fn check_code(&self) -> CheckCode { + self.inner.lock().unwrap().check_code().into() + } +} + +/// A check code that can be used to confirm that two [`EstablishedEcies`] +/// objects share the same secret. This is supposed to be shared out-of-band to +/// protect against active Man-in-the-middle (MITM) attacks. +/// +/// Since the initiator device can always tell whether a MITM attack is in +/// progress after channel establishment, this code technically carries only a +/// single bit of information, representing whether the initiator has determined +/// that the channel is "secure" or "not secure". +/// +/// However, given this will need to be interactively confirmed by the user, +/// there is risk that the user would confirm the dialogue without paying +/// attention to its content. By expanding this single bit into a deterministic +/// two-digit check code, the user is forced to pay more attention by having to +/// enter it instead of just clicking through a dialogue. +#[derive(Clone)] +#[wasm_bindgen] +pub struct CheckCode { + inner: matrix_sdk_crypto::vodozemac::ecies::CheckCode, +} + +#[wasm_bindgen] +impl CheckCode { + /// Convert the check code to an array of two bytes. + /// + /// The bytes can be converted to a more user-friendly representation. The + /// [`CheckCode::to_digit`] converts the bytes to a two-digit number. + pub fn as_bytes(&self) -> Vec { + self.inner.as_bytes().to_vec() + } + + /// Convert the check code to two base-10 numbers. + /// + /// The number should be displayed with a leading 0 in case the first digit + /// is a 0. + /// + /// # Examples + /// + /// ```no_run + /// # use vodozemac::ecies::CheckCode; + /// # let check_code: CheckCode = unimplemented!(); + /// let check_code = check_code.to_digit(); + /// + /// println!("The check code of the IECS channel is: {check_code:02}"); + /// ``` + pub fn to_digit(&self) -> u8 { + self.inner.to_digit() + } +} + +impl From<&ecies::CheckCode> for CheckCode { + fn from(value: &matrix_sdk_crypto::vodozemac::ecies::CheckCode) -> Self { + Self { inner: value.clone() } + } +} + +impl From for EstablishedEcies { + fn from(value: ecies::EstablishedEcies) -> Self { + Self { inner: Mutex::new(value).into() } + } +} diff --git a/src/vodozemac/mod.rs b/src/vodozemac/mod.rs index 70e69916f..69b0987d5 100644 --- a/src/vodozemac/mod.rs +++ b/src/vodozemac/mod.rs @@ -5,6 +5,8 @@ use wasm_bindgen::prelude::*; use crate::impl_from_to_inner; +pub mod ecies; + /// An Ed25519 public key, used to verify digital signatures. #[wasm_bindgen] #[derive(Debug, Clone)] diff --git a/tests/ecies.test.ts b/tests/ecies.test.ts new file mode 100644 index 000000000..881d35b2c --- /dev/null +++ b/tests/ecies.test.ts @@ -0,0 +1,28 @@ +const { Ecies } = require("../pkg/matrix_sdk_crypto_wasm"); + +describe(Ecies.name, () => { + test("can establish a channel and decrypt the initial message", () => { + const alice = new Ecies(); + const bob = new Ecies(); + + const { initial_message, channel: alice_established } = alice.establish_outbound_channel( + bob.public_key(), + "It's a secret to everybody", + ); + + const { message, channel } = bob.establish_inbound_channel(initial_message); + expect(message).toStrictEqual("It's a secret to everybody"); + + const alice_check = alice_established.check_code(); + const bob_check = channel.check_code(); + + expect(alice_check.as_bytes()).toStrictEqual(bob_check.as_bytes()); + expect(alice_check.to_digit()).toStrictEqual(bob_check.to_digit()); + + const ciphertext = channel.encrypt("Other message"); + const second_plaintext = alice_established.decrypt(ciphertext); + + expect(message).toStrictEqual("It's a secret to everybody"); + expect(second_plaintext).toStrictEqual("Other message"); + }); +}); From c6993805765d2a4b1ffffe2130060981856cc763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 15 May 2024 15:08:10 +0200 Subject: [PATCH 3/3] Expose the support to import and export a secrets bundle --- CHANGELOG.md | 4 +++ src/machine.rs | 43 +++++++++++++++++++++++++++ src/store.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++- tests/ecies.test.ts | 64 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 180 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ba9e497..7118e54b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/src/machine.rs b/src/machine.rs index 6b2db51d5..9be619096 100644 --- a/src/machine.rs +++ b/src/machine.rs @@ -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 diff --git a/src/store.rs b/src/store.rs index c9d8cb4ab..fd8ebb6a5 100644 --- a/src/store.rs +++ b/src/store.rs @@ -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::{ @@ -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 { + 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 { + Ok(serde_wasm_bindgen::to_value(&self.inner)?) + } + + /// Deserialize the [`SecretsBundle`] from a JSON object. + pub fn from_json(json: JsValue) -> Result { + let bundle = serde_wasm_bindgen::from_value(json)?; + + Ok(Self { inner: bundle }) + } +} + +impl_from_to_inner!(matrix_sdk_crypto::types::SecretsBundle => SecretsBundle); diff --git a/tests/ecies.test.ts b/tests/ecies.test.ts index 881d35b2c..746e1f790 100644 --- a/tests/ecies.test.ts +++ b/tests/ecies.test.ts @@ -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", () => { @@ -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(); + }); +});