From 6bb8fd7853fae0b77b60f41730fcd579a4c5129f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 3 Mar 2022 12:30:31 +0100 Subject: [PATCH] feat: Import the Uniffi based matrix-sdk-crypto bindings --- crates/matrix-crypto-ffi/Cargo.toml | 49 + crates/matrix-crypto-ffi/README.md | 76 + crates/matrix-crypto-ffi/build.rs | 3 + .../src/backup_recovery_key.rs | 152 ++ crates/matrix-crypto-ffi/src/device.rs | 42 + crates/matrix-crypto-ffi/src/error.rs | 63 + crates/matrix-crypto-ffi/src/lib.rs | 164 +++ crates/matrix-crypto-ffi/src/logger.rs | 56 + crates/matrix-crypto-ffi/src/machine.rs | 1305 +++++++++++++++++ crates/matrix-crypto-ffi/src/olm.udl | 426 ++++++ crates/matrix-crypto-ffi/src/responses.rs | 322 ++++ crates/matrix-crypto-ffi/src/users.rs | 61 + crates/matrix-crypto-ffi/src/verification.rs | 232 +++ 13 files changed, 2951 insertions(+) create mode 100644 crates/matrix-crypto-ffi/Cargo.toml create mode 100644 crates/matrix-crypto-ffi/README.md create mode 100644 crates/matrix-crypto-ffi/build.rs create mode 100644 crates/matrix-crypto-ffi/src/backup_recovery_key.rs create mode 100644 crates/matrix-crypto-ffi/src/device.rs create mode 100644 crates/matrix-crypto-ffi/src/error.rs create mode 100644 crates/matrix-crypto-ffi/src/lib.rs create mode 100644 crates/matrix-crypto-ffi/src/logger.rs create mode 100644 crates/matrix-crypto-ffi/src/machine.rs create mode 100644 crates/matrix-crypto-ffi/src/olm.udl create mode 100644 crates/matrix-crypto-ffi/src/responses.rs create mode 100644 crates/matrix-crypto-ffi/src/users.rs create mode 100644 crates/matrix-crypto-ffi/src/verification.rs diff --git a/crates/matrix-crypto-ffi/Cargo.toml b/crates/matrix-crypto-ffi/Cargo.toml new file mode 100644 index 000000000..22cbac2ab --- /dev/null +++ b/crates/matrix-crypto-ffi/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "matrix-crypto-ffi" +version = "0.1.0" +authors = ["Damir Jelić "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "matrix_crypto" + +[dependencies] +base64 = "0.13.0" +hmac = "0.11.0" +http = "0.2.4" +pbkdf2 = "0.8.0" +rand = "0.8.4" +serde = "1.0.126" +serde_json = "1.0.64" +sha2 = "0.9.5" +thiserror = "1.0.25" +tracing = "0.1.26" +tracing-subscriber = "0.2.18" +uniffi = "0.17.0" + +[dependencies.js_int] +version = "0.2.1" +features = ["lax_deserialize"] + +[dependencies.matrix-sdk-common] +git = "https://github.com/matrix-org/matrix-rust-sdk/" +rev = "009ead2eeaf365e1fb0f790557f20d4eaf6874ae" + +[dependencies.matrix-sdk-crypto] +git = "https://github.com/matrix-org/matrix-rust-sdk/" +rev = "009ead2eeaf365e1fb0f790557f20d4eaf6874ae" +features = ["sled_cryptostore", "qrcode", "backups_v1"] + +[dependencies.tokio] +version = "1.7.1" +default_features = false +features = ["rt-multi-thread"] + +[dependencies.ruma] +git = "https://github.com/ruma/ruma" +rev = "fdbc4d6d1dd273c8a6ac95b329943ed8c68df70d" +features = ["client-api-c"] + +[build-dependencies] +uniffi_build = { version = "0.17.0", features = ["builtin-bindgen"] } diff --git a/crates/matrix-crypto-ffi/README.md b/crates/matrix-crypto-ffi/README.md new file mode 100644 index 000000000..0d0507aae --- /dev/null +++ b/crates/matrix-crypto-ffi/README.md @@ -0,0 +1,76 @@ +# Uniffi based bindings for the Rust SDK crypto crate. + +This crate contains Uniffi based bindings for the `matrix-sdk-crypto` crate. The +README mainly describes how to build and integrate the bindings into a Kotlin +based Android project, but the Android specific bits can be skipped if you are +targeting an x86 Linux project. + +## Prerequisites + +### Rust + +To build the bindings [Rust] will be needed it can be either installed using an +OS specific package manager or directly with the provided [installer](https://rustup.rs/). + +### Android NDK + +The Android NDK will be required as well, it can be installed either through +Android Studio or directly using an [installer](https://developer.android.com/ndk/downloads). + +### Configuring Rust for cross compilation + +First we'll need to install the Rust target for our desired Android architecture, +for example: + +``` +# rustup target add aarch64-linux-android +``` + +This will add support to cross-compile for the aarch64-linux-android target, +Rust supports many different [targets], you'll have to make sure to pick the +right one for your device or emulator. + +After this is done, we'll have to configure [Cargo] to use the correct linker +for our target. Cargo is configured using a TOML file that will be found in +`%USERPROFILE%\.cargo\config.toml` on Windows or `$HOME/.cargo/config` on Unix +platforms. More details and configuration options for Cargo can be found in the +official docs over [here](https://doc.rust-lang.org/cargo/reference/config.html). + +``` +[target.aarch64-linux-android] +ar = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/ar" +linker = "NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang" +``` + +## Building + +To enable cross compilation fro `olm-sys` which builds our libolm C library +we'll need to set the `ANDROID_NDK` environment variable to the location of our +Android NDK installation. + +``` +$ export ANDROID_NDK=$HOME/Android/Sdk/ndk/22.0.7026061/ +``` + +### Building for a target + +The bindings can built for the `aarch64` target with: + +``` +$ cargo build --target aarch64-linux-android +``` + +After that, a dynamic library can be found in the `target/aarch64-linux-android/debug` directory. +The library will be called `libmatrix_crypto.so` and needs to be renamed and +copied into the `jniLibs` directory of your Android project, for Element Android: + +``` +$ cp ../../target/aarch64-linux-android/debug/libmatrix_crypto.so \ + /home/example/matrix-sdk-android/src/main/jniLibs/aarch64/libuniffi_olm.so +``` + +[Rust]: https://www.rust-lang.org/ +[installer]: https://rustup.rs/ +[targets]: https://doc.rust-lang.org/nightly/rustc/platform-support.html +[Cargo]: https://doc.rust-lang.org/cargo/ +[uniffi]: https://github.com/mozilla/uniffi-rs/ diff --git a/crates/matrix-crypto-ffi/build.rs b/crates/matrix-crypto-ffi/build.rs new file mode 100644 index 000000000..bfce95467 --- /dev/null +++ b/crates/matrix-crypto-ffi/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi_build::generate_scaffolding("./src/olm.udl").unwrap(); +} diff --git a/crates/matrix-crypto-ffi/src/backup_recovery_key.rs b/crates/matrix-crypto-ffi/src/backup_recovery_key.rs new file mode 100644 index 000000000..fe790018c --- /dev/null +++ b/crates/matrix-crypto-ffi/src/backup_recovery_key.rs @@ -0,0 +1,152 @@ +use std::{collections::HashMap, iter}; + +use hmac::Hmac; +use matrix_sdk_crypto::{ + backups::{OlmPkDecryptionError, RecoveryKey}, + store::CryptoStoreError as InnerStoreError, +}; +use pbkdf2::pbkdf2; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use sha2::Sha512; +use thiserror::Error; + +/// The private part of the backup key, the one used for recovery. +pub struct BackupRecoveryKey { + pub(crate) inner: RecoveryKey, + passphrase_info: Option, +} + +/// Error type for the decryption of backed up room keys. +#[derive(Debug, Error)] +pub enum PkDecryptionError { + /// An internal libolm error happened during decryption. + #[error("Error decryption a PkMessage {0}")] + Olm(#[from] OlmPkDecryptionError), +} + +/// Error type for the decoding and storing of the backup key. +#[derive(Debug, Error)] +pub enum DecodeError { + /// An error happened while decoding the recovery key. + #[error(transparent)] + Decode(#[from] matrix_sdk_crypto::backups::DecodeError), + /// An error happened in the storage layer while trying to save the + /// decoded recovery key. + #[error(transparent)] + CryptoStore(#[from] InnerStoreError), +} + +/// Struct containing info about the way the backup key got derived from a +/// passphrase. +#[derive(Debug, Clone)] +pub struct PassphraseInfo { + /// The salt that was used during key derivation. + pub private_key_salt: String, + /// The number of PBKDF rounds that were used for key derivation. + pub private_key_iterations: i32, +} + +/// The public part of the backup key. +pub struct MegolmV1BackupKey { + /// The actual base64 encoded public key. + pub public_key: String, + /// Signatures that have signed our backup key. + pub signatures: HashMap>, + /// The passphrase info, if the key was derived from one. + pub passphrase_info: Option, + /// Get the full name of the backup algorithm this backup key supports. + pub backup_algorithm: String, +} + +impl BackupRecoveryKey { + const KEY_SIZE: usize = 32; + const SALT_SIZE: usize = 32; + const PBKDF_ROUNDS: i32 = 500_000; + + /// Create a new random [`BackupRecoveryKey`]. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + inner: RecoveryKey::new() + .expect("Can't gather enough randomness to create a recovery key"), + passphrase_info: None, + } + } + + /// Try to create a [`BackupRecoveryKey`] from a base 64 encoded string. + pub fn from_base64(key: String) -> Result { + Ok(Self { inner: RecoveryKey::from_base64(&key)?, passphrase_info: None }) + } + + /// Try to create a [`BackupRecoveryKey`] from a base 58 encoded string. + pub fn from_base58(key: String) -> Result { + Ok(Self { inner: RecoveryKey::from_base58(&key)?, passphrase_info: None }) + } + + /// Create a new [`BackupRecoveryKey`] from the given passphrase. + pub fn new_from_passphrase(passphrase: String) -> Self { + let mut rng = thread_rng(); + let salt: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(Self::SALT_SIZE) + .collect(); + + Self::from_passphrase(passphrase, salt, Self::PBKDF_ROUNDS) + } + + /// Restore a [`BackupRecoveryKey`] from the given passphrase. + pub fn from_passphrase(passphrase: String, salt: String, rounds: i32) -> Self { + let mut key = [0u8; Self::KEY_SIZE]; + let rounds = rounds as u32; + + pbkdf2::>(passphrase.as_bytes(), salt.as_bytes(), rounds, &mut key); + + Self { + inner: RecoveryKey::from_bytes(key), + passphrase_info: Some(PassphraseInfo { + private_key_salt: salt, + private_key_iterations: rounds as i32, + }), + } + } + + /// Get the public part of the backup key. + pub fn megolm_v1_public_key(&self) -> MegolmV1BackupKey { + let public_key = self.inner.megolm_v1_public_key(); + + let signatures: HashMap> = public_key + .signatures() + .into_iter() + .map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect())) + .collect(); + + MegolmV1BackupKey { + public_key: public_key.to_base64(), + signatures, + passphrase_info: self.passphrase_info.clone(), + backup_algorithm: public_key.backup_algorithm().to_owned(), + } + } + + /// Convert the recovery key to a base 58 encoded string. + pub fn to_base58(&self) -> String { + self.inner.to_base58() + } + + /// Convert the recovery key to a base 64 encoded string. + pub fn to_base64(&self) -> String { + self.inner.to_base64() + } + + /// Try to decrypt a message that was encrypted using the public part of the + /// backup key. + pub fn decrypt_v1( + &self, + ephemeral_key: String, + mac: String, + ciphertext: String, + ) -> Result { + self.inner.decrypt_v1(ephemeral_key, mac, ciphertext).map_err(|e| e.into()) + } +} diff --git a/crates/matrix-crypto-ffi/src/device.rs b/crates/matrix-crypto-ffi/src/device.rs new file mode 100644 index 000000000..c21b4be76 --- /dev/null +++ b/crates/matrix-crypto-ffi/src/device.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +use matrix_sdk_crypto::Device as InnerDevice; + +/// An E2EE capable Matrix device. +pub struct Device { + /// The device owner. + pub user_id: String, + /// The unique ID of the device. + pub device_id: String, + /// The published public identity keys of the devices + /// + /// A map from the key type (e.g. curve25519) to the base64 encoded key. + pub keys: HashMap, + /// The supported algorithms of the device. + pub algorithms: Vec, + /// The human readable name of the device. + pub display_name: Option, + /// A flag indicating if the device has been blocked, blocked devices don't + /// receive any room keys from us. + pub is_blocked: bool, + /// Is the device locally trusted + pub locally_trusted: bool, + /// Is our cross signing identity trusted and does the identity trust the + /// device. + pub cross_signing_trusted: bool, +} + +impl From for Device { + fn from(d: InnerDevice) -> Self { + Device { + user_id: d.user_id().to_string(), + device_id: d.device_id().to_string(), + keys: d.keys().iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), + algorithms: d.algorithms().iter().map(|a| a.to_string()).collect(), + display_name: d.display_name().map(|d| d.to_owned()), + is_blocked: d.is_blacklisted(), + locally_trusted: d.is_locally_trusted(), + cross_signing_trusted: d.is_cross_signing_trusted(), + } + } +} diff --git a/crates/matrix-crypto-ffi/src/error.rs b/crates/matrix-crypto-ffi/src/error.rs new file mode 100644 index 000000000..1a79c5f6b --- /dev/null +++ b/crates/matrix-crypto-ffi/src/error.rs @@ -0,0 +1,63 @@ +#![allow(missing_docs)] + +use matrix_sdk_crypto::{ + store::CryptoStoreError as InnerStoreError, KeyExportError, MegolmError, OlmError, + SecretImportError as RustSecretImportError, SignatureError as InnerSignatureError, +}; +use ruma::identifiers::Error as RumaIdentifierError; + +#[derive(Debug, thiserror::Error)] +pub enum KeyImportError { + #[error(transparent)] + Export(#[from] KeyExportError), + #[error(transparent)] + CryptoStore(#[from] InnerStoreError), + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum SecretImportError { + #[error(transparent)] + CryptoStore(#[from] InnerStoreError), + #[error(transparent)] + Import(#[from] RustSecretImportError), +} + +#[derive(Debug, thiserror::Error)] +pub enum SignatureError { + #[error(transparent)] + Signature(#[from] InnerSignatureError), + #[error(transparent)] + Identifier(#[from] RumaIdentifierError), + #[error(transparent)] + CryptoStore(#[from] InnerStoreError), + #[error("Unknown device {0} {1}")] + UnknownDevice(String, String), + #[error("Unknown user identity {0}")] + UnknownUserIdentity(String), +} + +#[derive(Debug, thiserror::Error)] +pub enum CryptoStoreError { + #[error(transparent)] + CryptoStore(#[from] InnerStoreError), + #[error(transparent)] + OlmError(#[from] OlmError), + #[error(transparent)] + Serialization(#[from] serde_json::Error), + #[error("The given string is not a valid user ID: source {0}, error {1}")] + InvalidUserId(String, RumaIdentifierError), + #[error(transparent)] + Identifier(#[from] RumaIdentifierError), +} + +#[derive(Debug, thiserror::Error)] +pub enum DecryptionError { + #[error(transparent)] + Serialization(#[from] serde_json::Error), + #[error(transparent)] + Identifier(#[from] RumaIdentifierError), + #[error(transparent)] + Megolm(#[from] MegolmError), +} diff --git a/crates/matrix-crypto-ffi/src/lib.rs b/crates/matrix-crypto-ffi/src/lib.rs new file mode 100644 index 000000000..c7d937635 --- /dev/null +++ b/crates/matrix-crypto-ffi/src/lib.rs @@ -0,0 +1,164 @@ +#![deny( + dead_code, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unused_extern_crates, + unused_import_braces +)] + +//! Uniffi based bindings for the `matrix-sdk-crypto` crate. +//! +//! This crate can be used to introduce E2EE support into an existing Matrix +//! client or client library in any of the language targets Uniffi supports. + +mod backup_recovery_key; +mod device; +mod error; +mod logger; +mod machine; +mod responses; +mod users; +mod verification; + +use std::convert::TryFrom; + +pub use backup_recovery_key::{ + BackupRecoveryKey, DecodeError, MegolmV1BackupKey, PassphraseInfo, PkDecryptionError, +}; +pub use device::Device; +pub use error::{ + CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError, +}; +pub use logger::{set_logger, Logger}; +pub use machine::{KeyRequestPair, OlmMachine}; +pub use responses::{ + BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest, + Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest, +}; +pub use users::UserIdentity; +pub use verification::{ + CancelInfo, ConfirmVerificationResult, QrCode, RequestVerificationResult, Sas, ScanResult, + StartSasResult, Verification, VerificationRequest, +}; + +/// Callback that will be passed over the FFI to report progress +pub trait ProgressListener { + /// The callback that should be called on the Rust side + /// + /// # Arguments + /// + /// * `progress` - The current number of items that have been handled + /// + /// * `total` - The total number of items that will be handled + fn on_progress(&self, progress: i32, total: i32); +} + +/// An event that was successfully decrypted. +pub struct DecryptedEvent { + /// The decrypted version of the event. + pub clear_event: String, + /// The claimed curve25519 key of the sender. + pub sender_curve25519_key: String, + /// The claimed ed25519 key of the sender. + pub claimed_ed25519_key: Option, + /// The curve25519 chain of the senders that forwarded the Megolm decryption + /// key to us. Is empty if the key came directly from the sender of the + /// event. + pub forwarding_curve25519_chain: Vec, +} + +/// Struct representing the state of our private cross signing keys, it shows +/// which private cross signing keys we have locally stored. +#[derive(Debug, Clone)] +pub struct CrossSigningStatus { + /// Do we have the master key. + pub has_master: bool, + /// Do we have the self signing key, this one is necessary to sign our own + /// devices. + pub has_self_signing: bool, + /// Do we have the user signing key, this one is necessary to sign other + /// users. + pub has_user_signing: bool, +} + +/// A struct containing private cross signing keys that can be backed up or +/// uploaded to the secret store. +pub struct CrossSigningKeyExport { + /// The seed of the master key encoded as unpadded base64. + pub master_key: Option, + /// The seed of the self signing key encoded as unpadded base64. + pub self_signing_key: Option, + /// The seed of the user signing key encoded as unpadded base64. + pub user_signing_key: Option, +} + +/// Struct holding the number of room keys we have. +pub struct RoomKeyCounts { + /// The total number of room keys. + pub total: i64, + /// The number of backed up room keys. + pub backed_up: i64, +} + +/// Backup keys and information we load from the store. +pub struct BackupKeys { + /// The recovery key as a base64 encoded string. + pub recovery_key: String, + /// The version that is used with the recovery key. + pub backup_version: String, +} + +impl std::convert::TryFrom for BackupKeys { + type Error = (); + + fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result { + Ok(Self { + recovery_key: keys.recovery_key.ok_or(())?.to_base64(), + backup_version: keys.backup_version.ok_or(())?, + }) + } +} + +impl From for RoomKeyCounts { + fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self { + Self { total: count.total as i64, backed_up: count.backed_up as i64 } + } +} + +impl From for CrossSigningKeyExport { + fn from(e: matrix_sdk_crypto::CrossSigningKeyExport) -> Self { + Self { + master_key: e.master_key.clone(), + self_signing_key: e.self_signing_key.clone(), + user_signing_key: e.user_signing_key.clone(), + } + } +} + +impl From for matrix_sdk_crypto::CrossSigningKeyExport { + fn from(e: CrossSigningKeyExport) -> Self { + matrix_sdk_crypto::CrossSigningKeyExport { + master_key: e.master_key, + self_signing_key: e.self_signing_key, + user_signing_key: e.user_signing_key, + } + } +} + +impl From for CrossSigningStatus { + fn from(s: matrix_sdk_crypto::CrossSigningStatus) -> Self { + Self { + has_master: s.has_master, + has_self_signing: s.has_self_signing, + has_user_signing: s.has_user_signing, + } + } +} + +fn parse_user_id(user_id: &str) -> Result, CryptoStoreError> { + Box::::try_from(user_id) + .map_err(|e| CryptoStoreError::InvalidUserId(user_id.to_owned(), e)) +} + +include!(concat!(env!("OUT_DIR"), "/olm.uniffi.rs")); diff --git a/crates/matrix-crypto-ffi/src/logger.rs b/crates/matrix-crypto-ffi/src/logger.rs new file mode 100644 index 000000000..993edfc32 --- /dev/null +++ b/crates/matrix-crypto-ffi/src/logger.rs @@ -0,0 +1,56 @@ +use std::{ + io::{Result, Write}, + sync::{Arc, Mutex}, +}; + +use tracing_subscriber::{fmt::MakeWriter, EnvFilter}; + +/// Trait that can be used to forward Rust logs over FFI to a language specific +/// logger. +pub trait Logger: Send { + /// Called every time the Rust side wants to post a log line. + fn log(&self, log_line: String); + // TODO add support for different log levels, do this by adding more methods + // to the trait. +} + +impl Write for LoggerWrapper { + fn write(&mut self, buf: &[u8]) -> Result { + let data = String::from_utf8_lossy(buf).to_string(); + self.inner.lock().unwrap().log(data); + + Ok(buf.len()) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } +} + +impl MakeWriter for LoggerWrapper { + type Writer = LoggerWrapper; + + fn make_writer(&self) -> Self::Writer { + self.clone() + } +} + +#[derive(Clone)] +pub struct LoggerWrapper { + inner: Arc>>, +} + +/// Set the logger that should be used to forward Rust logs over FFI. +pub fn set_logger(logger: Box) { + let logger = LoggerWrapper { inner: Arc::new(Mutex::new(logger)) }; + + let filter = EnvFilter::from_default_env().add_directive( + "matrix_sdk_crypto=trace".parse().expect("Can't parse logging filter directive"), + ); + + let _ = tracing_subscriber::fmt() + .with_writer(logger) + .with_env_filter(filter) + .without_time() + .try_init(); +} diff --git a/crates/matrix-crypto-ffi/src/machine.rs b/crates/matrix-crypto-ffi/src/machine.rs new file mode 100644 index 000000000..51b5bd13e --- /dev/null +++ b/crates/matrix-crypto-ffi/src/machine.rs @@ -0,0 +1,1305 @@ +use std::{ + collections::{BTreeMap, HashMap}, + convert::{TryFrom, TryInto}, + io::Cursor, + ops::Deref, +}; + +use base64::{decode_config, encode, STANDARD_NO_PAD}; +use js_int::UInt; +use matrix_sdk_common::{deserialized_responses::AlgorithmInfo, uuid::Uuid}; +use matrix_sdk_crypto::{ + backups::{MegolmV1BackupKey as RustBackupKey, RecoveryKey}, + decrypt_key_export, encrypt_key_export, + matrix_qrcode::QrVerificationData, + olm::ExportedRoomKey, + EncryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentities, + Verification as RustVerification, +}; +use ruma::{ + api::{ + client::r0::{ + backup::add_backup_keys::Response as KeysBackupResponse, + keys::{ + claim_keys::Response as KeysClaimResponse, get_keys::Response as KeysQueryResponse, + upload_keys::Response as KeysUploadResponse, + upload_signatures::Response as SignatureUploadResponse, + }, + sync::sync_events::{DeviceLists as RumaDeviceLists, ToDevice}, + to_device::send_event_to_device::Response as ToDeviceResponse, + }, + IncomingResponse, + }, + events::{ + key::verification::VerificationMethod, room::encrypted::RoomEncryptedEventContent, + AnyMessageEventContent, EventContent, SyncMessageEvent, + }, + DeviceKeyAlgorithm, EventId, RoomId, UserId, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{value::RawValue, Value}; +use tokio::runtime::Runtime; + +use crate::{ + error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError}, + parse_user_id, + responses::{response_from_string, OutgoingVerificationRequest, OwnedResponse}, + BackupKeys, BootstrapCrossSigningResult, ConfirmVerificationResult, CrossSigningKeyExport, + CrossSigningStatus, DecodeError, DecryptedEvent, Device, DeviceLists, KeyImportError, + KeysImportResult, MegolmV1BackupKey, ProgressListener, QrCode, Request, RequestType, + RequestVerificationResult, RoomKeyCounts, ScanResult, SignatureUploadRequest, StartSasResult, + UserIdentity, Verification, VerificationRequest, +}; + +/// A high level state machine that handles E2EE for Matrix. +pub struct OlmMachine { + inner: InnerMachine, + runtime: Runtime, +} + +/// A pair of outgoing room key requests, both of those are sendToDevice +/// requests. +pub struct KeyRequestPair { + /// The optional cancellation, this is None if no previous key request was + /// sent out for this key, thus it doesn't need to be cancelled. + pub cancellation: Option, + /// The actual key request. + pub key_request: Request, +} + +impl OlmMachine { + /// Create a new `OlmMachine` + /// + /// # Arguments + /// + /// * `user_id` - The unique ID of the user that owns this machine. + /// + /// * `device_id` - The unique ID of the device that owns this machine. + /// + /// * `path` - The path where the state of the machine should be persisted. + pub fn new(user_id: &str, device_id: &str, path: &str) -> Result { + let user_id = parse_user_id(user_id)?; + let device_id = device_id.into(); + let runtime = Runtime::new().expect("Couldn't create a tokio runtime"); + + Ok(OlmMachine { + inner: runtime + .block_on(InnerMachine::new_with_default_store(&user_id, device_id, path, None))?, + runtime, + }) + } + + /// Get the user ID of the owner of this `OlmMachine`. + pub fn user_id(&self) -> String { + self.inner.user_id().to_string() + } + + /// Get the device ID of the device of this `OlmMachine`. + pub fn device_id(&self) -> String { + self.inner.device_id().to_string() + } + + /// Get the display name of our own device. + pub fn display_name(&self) -> Result, CryptoStoreError> { + Ok(self.runtime.block_on(self.inner.display_name())?) + } + + /// Get a cross signing user identity for the given user ID. + pub fn get_identity(&self, user_id: &str) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + Ok(if let Some(identity) = self.runtime.block_on(self.inner.get_identity(&user_id))? { + Some(self.runtime.block_on(UserIdentity::from_rust(identity))?) + } else { + None + }) + } + + /// Check if a user identity is considered to be verified by us. + pub fn is_identity_verified(&self, user_id: &str) -> Result { + let user_id = parse_user_id(user_id)?; + + Ok(if let Some(identity) = self.runtime.block_on(self.inner.get_identity(&user_id))? { + match identity { + UserIdentities::Own(i) => i.is_verified(), + UserIdentities::Other(i) => i.verified(), + } + } else { + false + }) + } + + /// Manually the user with the given user ID. + /// + /// This method will attempt to sign the user identity using either our + /// private cross signing key, for other user identities, or our device keys + /// for our own user identity. + /// + /// This method can fail if we don't have the private part of our + /// user-signing key. + /// + /// Returns a request that needs to be sent out for the user identity to be + /// marked as verified. + pub fn verify_identity(&self, user_id: &str) -> Result { + let user_id = Box::::try_from(user_id)?; + + let user_identity = self.runtime.block_on(self.inner.get_identity(&user_id))?; + + if let Some(user_identity) = user_identity { + Ok(match user_identity { + UserIdentities::Own(i) => self.runtime.block_on(i.verify())?, + UserIdentities::Other(i) => self.runtime.block_on(i.verify())?, + } + .into()) + } else { + Err(SignatureError::UnknownUserIdentity(user_id.to_string())) + } + } + + /// Get a `Device` from the store. + /// + /// # Arguments + /// + /// * `user_id` - The id of the device owner. + /// + /// * `device_id` - The id of the device itself. + pub fn get_device( + &self, + user_id: &str, + device_id: &str, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + Ok(self + .runtime + .block_on(self.inner.get_device(&user_id, device_id.into()))? + .map(|d| d.into())) + } + + /// Manually the device of the given user with the given device ID. + /// + /// This method will attempt to sign the device using our private cross + /// signing key. + /// + /// This method will always fail if the device belongs to someone else, we + /// can only sign our own devices. + /// + /// It can also fail if we don't have the private part of our self-signing + /// key. + /// + /// Returns a request that needs to be sent out for the device to be marked + /// as verified. + pub fn verify_device( + &self, + user_id: &str, + device_id: &str, + ) -> Result { + let user_id = Box::::try_from(user_id)?; + let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id.into()))?; + + if let Some(device) = device { + Ok(self.runtime.block_on(device.verify())?.into()) + } else { + Err(SignatureError::UnknownDevice(user_id.to_string(), device_id.to_string())) + } + } + + /// Mark the device of the given user with the given device id as trusted. + pub fn mark_device_as_trusted( + &self, + user_id: &str, + device_id: &str, + ) -> Result<(), CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + let device = self.runtime.block_on(self.inner.get_device(&user_id, device_id.into()))?; + + if let Some(device) = device { + self.runtime.block_on(device.set_local_trust(LocalTrust::Verified))?; + } + + Ok(()) + } + + /// Get all devices of an user. + /// + /// # Arguments + /// + /// * `user_id` - The id of the device owner. + pub fn get_user_devices(&self, user_id: &str) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + Ok(self + .runtime + .block_on(self.inner.get_user_devices(&user_id))? + .devices() + .map(|d| d.into()) + .collect()) + } + + /// Get our own identity keys. + pub fn identity_keys(&self) -> HashMap { + self.inner.identity_keys().iter().map(|(k, v)| (k.to_owned(), v.to_owned())).collect() + } + + /// Get the list of outgoing requests that need to be sent to the + /// homeserver. + /// + /// After the request was sent out and a successful response was received + /// the response body should be passed back to the state machine using the + /// [mark_request_as_sent()](#method.mark_request_as_sent) method. + /// + /// **Note**: This method call should be locked per call. + pub fn outgoing_requests(&self) -> Result, CryptoStoreError> { + Ok(self + .runtime + .block_on(self.inner.outgoing_requests())? + .into_iter() + .map(|r| r.into()) + .collect()) + } + + /// Mark a request that was sent to the server as sent. + /// + /// # Arguments + /// + /// * `request_id` - The unique ID of the request that was sent out. This + /// needs to be an UUID. + /// + /// * `request_type` - The type of the request that was sent out. + /// + /// * `response_body` - The body of the response that was received. + pub fn mark_request_as_sent( + &self, + request_id: &str, + request_type: RequestType, + response_body: &str, + ) -> Result<(), CryptoStoreError> { + let id = Uuid::parse_str(request_id).expect("Can't parse request id"); + + let response = response_from_string(response_body); + + let response: OwnedResponse = match request_type { + RequestType::KeysUpload => { + KeysUploadResponse::try_from_http_response(response).map(Into::into) + } + RequestType::KeysQuery => { + KeysQueryResponse::try_from_http_response(response).map(Into::into) + } + RequestType::ToDevice => { + ToDeviceResponse::try_from_http_response(response).map(Into::into) + } + RequestType::KeysClaim => { + KeysClaimResponse::try_from_http_response(response).map(Into::into) + } + RequestType::SignatureUpload => { + SignatureUploadResponse::try_from_http_response(response).map(Into::into) + } + RequestType::KeysBackup => { + KeysBackupResponse::try_from_http_response(response).map(Into::into) + } + } + .expect("Can't convert json string to response"); + + self.runtime.block_on(self.inner.mark_request_as_sent(&id, &response))?; + + Ok(()) + } + + /// Let the state machine know about E2EE related sync changes that we + /// received from the server. + /// + /// This needs to be called after every sync, ideally before processing + /// any other sync changes. + /// + /// # Arguments + /// + /// * `events` - A serialized array of to-device events we received in the + /// current sync response. + /// + /// * `device_changes` - The list of devices that have changed in some way + /// since the previous sync. + /// + /// * `key_counts` - The map of uploaded one-time key types and counts. + pub fn receive_sync_changes( + &self, + events: &str, + device_changes: DeviceLists, + key_counts: HashMap, + unused_fallback_keys: Option>, + ) -> Result { + let events: ToDevice = serde_json::from_str(events)?; + let device_changes: RumaDeviceLists = device_changes.into(); + let key_counts: BTreeMap = key_counts + .into_iter() + .map(|(k, v)| { + ( + DeviceKeyAlgorithm::from(k), + v.clamp(0, i32::MAX) + .try_into() + .expect("Couldn't convert key counts into an UInt"), + ) + }) + .collect(); + + let unused_fallback_keys: Option> = + unused_fallback_keys.map(|u| u.into_iter().map(DeviceKeyAlgorithm::from).collect()); + + let events = self.runtime.block_on(self.inner.receive_sync_changes( + events, + &device_changes, + &key_counts, + unused_fallback_keys.as_deref(), + ))?; + + Ok(serde_json::to_string(&events)?) + } + + /// Add the given list of users to be tracked, triggering a key query + /// request for them. + /// + /// *Note*: Only users that aren't already tracked will be considered for an + /// update. It's safe to call this with already tracked users, it won't + /// result in excessive keys query requests. + /// + /// # Arguments + /// + /// `users` - The users that should be queued up for a key query. + pub fn update_tracked_users(&self, users: Vec) { + let users: Vec> = + users.into_iter().filter_map(|u| Box::::try_from(u).ok()).collect(); + + self.runtime.block_on(self.inner.update_tracked_users(users.iter().map(Deref::deref))); + } + + /// Check if the given user is considered to be tracked. + /// + /// A user can be marked for tracking using the + /// [`OlmMachine::update_tracked_users()`] method. + pub fn is_user_tracked(&self, user_id: &str) -> Result { + let user_id = parse_user_id(user_id)?; + Ok(self.inner.tracked_users().contains(&user_id)) + } + + /// Generate one-time key claiming requests for all the users we are missing + /// sessions for. + /// + /// After the request was sent out and a successful response was received + /// the response body should be passed back to the state machine using the + /// [mark_request_as_sent()](#method.mark_request_as_sent) method. + /// + /// This method should be called every time before a call to + /// [`share_group_session()`](#method.share_group_session) is made. + /// + /// # Arguments + /// + /// * `users` - The list of users for which we would like to establish 1:1 + /// Olm sessions for. + pub fn get_missing_sessions( + &self, + users: Vec, + ) -> Result, CryptoStoreError> { + let users: Vec> = + users.into_iter().filter_map(|u| Box::::try_from(u).ok()).collect(); + + Ok(self + .runtime + .block_on(self.inner.get_missing_sessions(users.iter().map(Deref::deref)))? + .map(|r| r.into())) + } + + /// Share a room key with the given list of users for the given room. + /// + /// After the request was sent out and a successful response was received + /// the response body should be passed back to the state machine using the + /// [mark_request_as_sent()](#method.mark_request_as_sent) method. + /// + /// This method should be called every time before a call to + /// [`encrypt()`](#method.encrypt) with the given `room_id` is made. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room, note that this doesn't strictly + /// need to be a Matrix room, it just needs to be an unique identifier for + /// the group that will participate in the conversation. + /// + /// * `users` - The list of users which are considered to be members of the + /// room and should receive the room key. + pub fn share_room_key( + &self, + room_id: &str, + users: Vec, + ) -> Result, CryptoStoreError> { + let users: Vec> = + users.into_iter().filter_map(|u| Box::::try_from(u).ok()).collect(); + + let room_id = Box::::try_from(room_id)?; + let requests = self.runtime.block_on(self.inner.share_group_session( + &room_id, + users.iter().map(Deref::deref), + EncryptionSettings::default(), + ))?; + + Ok(requests.into_iter().map(|r| (&*r).into()).collect()) + } + + /// Encrypt the given event with the given type and content for the given + /// room. + /// + /// **Note**: A room key needs to be shared with the group of users that are + /// members in the given room. If this is not done this method will panic. + /// + /// The usual flow to encrypt an event using this state machine is as + /// follows: + /// + /// 1. Get the one-time key claim request to establish 1:1 Olm sessions for + /// the room members of the room we wish to participate in. This is done + /// using the [`get_missing_sessions()`](#method.get_missing_sessions) + /// method. This method call should be locked per call. + /// + /// 2. Share a room key with all the room members using the + /// [`share_group_session()`](#method.share_group_session). This method + /// call should be locked per room. + /// + /// 3. Encrypt the event using this method. + /// + /// 4. Send the encrypted event to the server. + /// + /// After the room key is shared steps 1 and 2 will become noops, unless + /// there's some changes in the room membership or in the list of devices a + /// member has. + /// + /// # Arguments + /// + /// * `room_id` - The unique id of the room where the event will be sent to. + /// + /// * `even_type` - The type of the event. + /// + /// * `content` - The serialized content of the event. + pub fn encrypt( + &self, + room_id: &str, + event_type: &str, + content: &str, + ) -> Result { + let room_id = Box::::try_from(room_id)?; + let content: Box = serde_json::from_str(content)?; + + let content = AnyMessageEventContent::from_parts(event_type, &content)?; + let encrypted_content = self + .runtime + .block_on(self.inner.encrypt(&room_id, content)) + .expect("Encrypting an event produced an error"); + + Ok(serde_json::to_string(&encrypted_content)?) + } + + /// Decrypt the given event that was sent in the given room. + /// + /// # Arguments + /// + /// * `event` - The serialized encrypted version of the event. + /// + /// * `room_id` - The unique id of the room where the event was sent to. + pub fn decrypt_room_event( + &self, + event: &str, + room_id: &str, + ) -> Result { + // Element Android wants only the content and the type and will create a + // decrypted event with those two itself, this struct makes sure we + // throw away all the other fields. + #[derive(Deserialize, Serialize)] + struct Event<'a> { + #[serde(rename = "type")] + event_type: String, + #[serde(borrow)] + content: &'a RawValue, + } + + let event: SyncMessageEvent = serde_json::from_str(event)?; + let room_id = Box::::try_from(room_id)?; + + let decrypted = self.runtime.block_on(self.inner.decrypt_room_event(&event, &room_id))?; + + let encryption_info = + decrypted.encryption_info.expect("Decrypted event didn't contain any encryption info"); + + let event_json: Event = serde_json::from_str(decrypted.event.json().get())?; + + Ok(match &encryption_info.algorithm_info { + AlgorithmInfo::MegolmV1AesSha2 { + curve25519_key, + sender_claimed_keys, + forwarding_curve25519_key_chain, + } => DecryptedEvent { + clear_event: serde_json::to_string(&event_json)?, + sender_curve25519_key: curve25519_key.to_owned(), + claimed_ed25519_key: sender_claimed_keys.get(&DeviceKeyAlgorithm::Ed25519).cloned(), + forwarding_curve25519_chain: forwarding_curve25519_key_chain.to_owned(), + }, + }) + } + + /// Request or re-request a room key that was used to encrypt the given + /// event. + /// + /// # Arguments + /// + /// * `event` - The undecryptable event that we would wish to request a room + /// key for. + /// + /// * `room_id` - The id of the room the event was sent to. + pub fn request_room_key( + &self, + event: &str, + room_id: &str, + ) -> Result { + let event: SyncMessageEvent = serde_json::from_str(event)?; + let room_id = Box::::try_from(room_id)?; + + let (cancel, request) = + self.runtime.block_on(self.inner.request_room_key(&event, &room_id))?; + + let cancellation = cancel.map(|r| r.into()); + let key_request = request.into(); + + Ok(KeyRequestPair { cancellation, key_request }) + } + + /// Export all of our room keys. + /// + /// # Arguments + /// + /// * `passphrase` - The passphrase that should be used to encrypt the key + /// export. + /// + /// * `rounds` - The number of rounds that should be used when expanding the + /// passphrase into an key. + pub fn export_keys(&self, passphrase: &str, rounds: i32) -> Result { + let keys = self.runtime.block_on(self.inner.export_keys(|_| true))?; + + let encrypted = encrypt_key_export(&keys, passphrase, rounds as u32) + .map_err(CryptoStoreError::Serialization)?; + + Ok(encrypted) + } + + fn import_keys_helper( + &self, + keys: Vec, + from_backup: bool, + progress_listener: Box, + ) -> Result { + let listener = |progress: usize, total: usize| { + progress_listener.on_progress(progress as i32, total as i32) + }; + + let result = self.runtime.block_on(self.inner.import_keys(keys, from_backup, listener))?; + + Ok(KeysImportResult { + imported: result.imported_count as i64, + total: result.total_count as i64, + keys: result + .keys + .into_iter() + .map(|(r, m)| { + ( + r.to_string(), + m.into_iter().map(|(s, k)| (s, k.into_iter().collect())).collect(), + ) + }) + .collect(), + }) + } + + /// Import room keys from the given serialized key export. + /// + /// # Arguments + /// + /// * `keys` - The serialized version of the key export. + /// + /// * `passphrase` - The passphrase that was used to encrypt the key export. + /// + /// * `progress_listener` - A callback that can be used to introspect the + /// progress of the key import. + pub fn import_keys( + &self, + keys: &str, + passphrase: &str, + progress_listener: Box, + ) -> Result { + let keys = Cursor::new(keys); + let keys = decrypt_key_export(keys, passphrase)?; + self.import_keys_helper(keys, false, progress_listener) + } + + /// Import room keys from the given serialized unencrypted key export. + /// + /// This method is the same as [`OlmMachine::import_keys`] but the + /// decryption step is skipped and should be performed by the caller. This + /// should be used if the room keys are coming from the server-side backup, + /// the method will mark all imported room keys as backed up. + /// + /// # Arguments + /// + /// * `keys` - The serialized version of the unencrypted key export. + /// + /// * `progress_listener` - A callback that can be used to introspect the + /// progress of the key import. + pub fn import_decrypted_keys( + &self, + keys: &str, + progress_listener: Box, + ) -> Result { + let keys: Vec = serde_json::from_str(keys)?; + + let keys = keys.into_iter().map(serde_json::from_value).filter_map(|k| k.ok()).collect(); + + self.import_keys_helper(keys, true, progress_listener) + } + + /// Discard the currently active room key for the given room if there is + /// one. + pub fn discard_room_key(&self, room_id: &str) -> Result<(), CryptoStoreError> { + let room_id = Box::::try_from(room_id)?; + + self.runtime.block_on(self.inner.invalidate_group_session(&room_id))?; + + Ok(()) + } + + /// Get all the verification requests that we share with the given user. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to fetch the + /// verification requests. + pub fn get_verification_requests(&self, user_id: &str) -> Vec { + let user_id = if let Ok(user_id) = Box::::try_from(user_id) { + user_id + } else { + return vec![]; + }; + + self.inner.get_verification_requests(&user_id).into_iter().map(|v| v.into()).collect() + } + + /// Get a verification requests that we share with the given user with the + /// given flow id. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to fetch the + /// verification requests. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn get_verification_request( + &self, + user_id: &str, + flow_id: &str, + ) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + + self.inner.get_verification_request(&user_id, flow_id).map(|v| v.into()) + } + + /// Accept a verification requests that we share with the given user with + /// the given flow id. + /// + /// This will move the verification request into the ready state. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to accept the + /// verification requests. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// * `methods` - A list of verification methods that we want to advertise + /// as supported. + pub fn accept_verification_request( + &self, + user_id: &str, + flow_id: &str, + methods: Vec, + ) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + verification.accept_with_methods(methods).map(|r| r.into()) + } else { + None + } + } + + /// Get an m.key.verification.request content for the given user. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user which we would like to request to + /// verify. + /// + /// * `methods` - The list of verification methods we want to advertise to + /// support. + pub fn verification_request_content( + &self, + user_id: &str, + methods: Vec, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + let identity = self.runtime.block_on(self.inner.get_identity(&user_id))?; + + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + Ok(if let Some(identity) = identity.and_then(|i| i.other()) { + let content = + self.runtime.block_on(identity.verification_request_content(Some(methods))); + Some(serde_json::to_string(&content)?) + } else { + None + }) + } + + /// Request a verification flow to begin with the given user in the given + /// room. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user which we would like to request to + /// verify. + /// + /// * `room_id` - The ID of the room that represents a DM with the given + /// user. + /// + /// * `event_id` - The event ID of the `m.key.verification.request` event + /// that we sent out to request the verification to begin. The content for + /// this request can be created using the [verification_request_content()] + /// method. + /// + /// * `methods` - The list of verification methods we advertised as + /// supported in the `m.key.verification.request` event. + /// + /// [verification_request_content()]: #method.verification_request_content + pub fn request_verification( + &self, + user_id: &str, + room_id: &str, + event_id: &str, + methods: Vec, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + let event_id = Box::::try_from(event_id)?; + let room_id = Box::::try_from(room_id)?; + + let identity = self.runtime.block_on(self.inner.get_identity(&user_id))?; + + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + Ok(if let Some(identity) = identity.and_then(|i| i.other()) { + let request = self.runtime.block_on(identity.request_verification( + &room_id, + &event_id, + Some(methods), + )); + + Some(request.into()) + } else { + None + }) + } + + /// Request a verification flow to begin with the given user's device. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user which we would like to request to + /// verify. + /// + /// * `device_id` - The ID of the device that we wish to verify. + /// + /// * `methods` - The list of verification methods we advertised as + /// supported in the `m.key.verification.request` event. + pub fn request_verification_with_device( + &self, + user_id: &str, + device_id: &str, + methods: Vec, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + Ok( + if let Some(device) = + self.runtime.block_on(self.inner.get_device(&user_id, device_id.into()))? + { + let (verification, request) = + self.runtime.block_on(device.request_verification_with_methods(methods)); + + Some(RequestVerificationResult { + verification: verification.into(), + request: request.into(), + }) + } else { + None + }, + ) + } + + /// Request a verification flow to begin with our other devices. + /// + /// # Arguments + /// + /// `methods` - The list of verification methods we want to advertise to + /// support. + pub fn request_self_verification( + &self, + methods: Vec, + ) -> Result, CryptoStoreError> { + let identity = self.runtime.block_on(self.inner.get_identity(self.inner.user_id()))?; + + let methods = methods.into_iter().map(VerificationMethod::from).collect(); + + Ok(if let Some(identity) = identity.and_then(|i| i.own()) { + let (verification, request) = + self.runtime.block_on(identity.request_verification_with_methods(methods))?; + Some(RequestVerificationResult { + verification: verification.into(), + request: request.into(), + }) + } else { + None + }) + } + + /// Get a verification flow object for the given user with the given flow + /// id. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to fetch the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn get_verification(&self, user_id: &str, flow_id: &str) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + + self.inner.get_verification(&user_id, flow_id).map(|v| match v { + RustVerification::SasV1(s) => Verification::SasV1 { sas: s.into() }, + RustVerification::QrV1(qr) => Verification::QrCodeV1 { qrcode: qr.into() }, + }) + } + + /// Cancel a verification for the given user with the given flow id using + /// the given cancel code. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to cancel the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// * `cancel_code` - The error code for why the verification was cancelled, + /// manual cancellatio usually happens with `m.user` cancel code. The full + /// list of cancel codes can be found in the [spec] + /// + /// [spec]: https://spec.matrix.org/unstable/client-server-api/#mkeyverificationcancel + pub fn cancel_verification( + &self, + user_id: &str, + flow_id: &str, + cancel_code: &str, + ) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + + if let Some(request) = self.inner.get_verification_request(&user_id, flow_id) { + request.cancel().map(|r| r.into()) + } else if let Some(verification) = self.inner.get_verification(&user_id, flow_id) { + match verification { + RustVerification::SasV1(v) => { + v.cancel_with_code(cancel_code.into()).map(|r| r.into()) + } + RustVerification::QrV1(v) => { + v.cancel_with_code(cancel_code.into()).map(|r| r.into()) + } + } + } else { + None + } + } + + /// Confirm a verification was successful. + /// + /// This method should be called either if a short auth string should be + /// confirmed as matching, or if we want to confirm that the other side has + /// scanned our QR code. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to confirm the + /// verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn confirm_verification( + &self, + user_id: &str, + flow_id: &str, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + Ok(if let Some(verification) = self.inner.get_verification(&user_id, flow_id) { + match verification { + RustVerification::SasV1(v) => { + let (request, signature_request) = self.runtime.block_on(v.confirm())?; + + request.map(|r| ConfirmVerificationResult { + request: r.into(), + signature_request: signature_request.map(|s| s.into()), + }) + } + RustVerification::QrV1(v) => v.confirm_scanning().map(|r| { + ConfirmVerificationResult { request: r.into(), signature_request: None } + }), + } + } else { + None + }) + } + + /// Transition from a verification request into QR code verification. + /// + /// This method should be called when one wants to display a QR code so the + /// other side can scan it and move the QR code verification forward. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// QR code verification. + /// + /// * `flow_id` - The ID of the verification request that initiated the + /// verification flow. + pub fn start_qr_verification( + &self, + user_id: &str, + flow_id: &str, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + Ok(self.runtime.block_on(verification.generate_qr_code())?.map(|qr| qr.into())) + } else { + Ok(None) + } + } + + /// Generate data that should be encoded as a QR code. + /// + /// This method should be called right before a QR code should be displayed, + /// the returned data is base64 encoded (without padding) and needs to be + /// decoded on the other side before it can be put through a QR code + /// generator. + /// + /// *Note*: You'll need to call [start_qr_verification()] before calling + /// this method, otherwise `None` will be returned. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// QR code verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + /// + /// [start_qr_verification()]: #method.start_qr_verification + pub fn generate_qr_code(&self, user_id: &str, flow_id: &str) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + self.inner + .get_verification(&user_id, flow_id) + .and_then(|v| v.qr_v1().and_then(|qr| qr.to_bytes().map(encode).ok())) + } + + /// Pass data from a scanned QR code to an active verification request and + /// transition into QR code verification. + /// + /// This requires an active `VerificationRequest` to succeed, returns `None` + /// if no `VerificationRequest` is found or if the QR code data is invalid. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// QR code verification. + /// + /// * `flow_id` - The ID of the verification request that initiated the + /// verification flow. + /// + /// * `data` - The data that was extracted from the scanned QR code as an + /// base64 encoded string, without padding. + pub fn scan_qr_code(&self, user_id: &str, flow_id: &str, data: &str) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + let data = decode_config(data, STANDARD_NO_PAD).ok()?; + let data = QrVerificationData::from_bytes(data).ok()?; + + if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + if let Some(qr) = self.runtime.block_on(verification.scan_qr_code(data)).ok()? { + let request = qr.reciprocate()?; + + Some(ScanResult { qr: qr.into(), request: request.into() }) + } else { + None + } + } else { + None + } + } + + /// Transition from a verification request into short auth string based + /// verification. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// SAS verification. + /// + /// * `flow_id` - The ID of the verification request that initiated the + /// verification flow. + pub fn start_sas_verification( + &self, + user_id: &str, + flow_id: &str, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + Ok(if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { + self.runtime + .block_on(verification.start_sas())? + .map(|(sas, r)| StartSasResult { sas: sas.into(), request: r.into() }) + } else { + None + }) + } + + /// Start short auth string verification with a device without going + /// through a verification request first. + /// + /// **Note**: This has been largely deprecated and the + /// [request_verification_with_device()] method should be used instead. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to start the + /// SAS verification. + /// + /// * `device_id` - The ID of device we would like to verify. + /// + /// [request_verification_with_device()]: #method.request_verification_with_device + pub fn start_sas_with_device( + &self, + user_id: &str, + device_id: &str, + ) -> Result, CryptoStoreError> { + let user_id = parse_user_id(user_id)?; + + Ok( + if let Some(device) = + self.runtime.block_on(self.inner.get_device(&user_id, device_id.into()))? + { + let (sas, request) = self.runtime.block_on(device.start_verification())?; + + Some(StartSasResult { sas: sas.into(), request: request.into() }) + } else { + None + }, + ) + } + + /// Accept that we're going forward with the short auth string verification. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to accept the + /// SAS verification. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn accept_sas_verification( + &self, + user_id: &str, + flow_id: &str, + ) -> Option { + let user_id = Box::::try_from(user_id).ok()?; + + self.inner + .get_verification(&user_id, flow_id) + .and_then(|s| s.sas_v1()) + .and_then(|s| s.accept().map(|r| r.into())) + } + + /// Get a list of emoji indices of the emoji representation of the short + /// auth string. + /// + /// *Note*: A SAS verification needs to be started and in the presentable + /// state for this to return the list of emoji indices, otherwise returns + /// `None`. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to get the + /// short auth string. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn get_emoji_index(&self, user_id: &str, flow_id: &str) -> Option> { + let user_id = Box::::try_from(user_id).ok()?; + + self.inner.get_verification(&user_id, flow_id).and_then(|s| { + s.sas_v1() + .and_then(|s| s.emoji_index().map(|v| v.iter().map(|i| (*i).into()).collect())) + }) + } + + /// Get the decimal representation of the short auth string. + /// + /// *Note*: A SAS verification needs to be started and in the presentable + /// state for this to return the list of decimals, otherwise returns + /// `None`. + /// + /// # Arguments + /// + /// * `user_id` - The ID of the user for which we would like to get the + /// short auth string. + /// + /// * `flow_id` - The ID that uniquely identifies the verification flow. + pub fn get_decimals(&self, user_id: &str, flow_id: &str) -> Option> { + let user_id = Box::::try_from(user_id).ok()?; + + self.inner.get_verification(&user_id, flow_id).and_then(|s| { + s.sas_v1() + .and_then(|s| s.decimals().map(|v| [v.0.into(), v.1.into(), v.2.into()].to_vec())) + }) + } + + /// Create a new private cross signing identity and create a request to + /// upload the public part of it to the server. + pub fn bootstrap_cross_signing(&self) -> Result { + Ok(self.runtime.block_on(self.inner.bootstrap_cross_signing(true))?.into()) + } + + /// Get the status of the private cross signing keys. + /// + /// This can be used to check which private cross signing keys we have + /// stored locally. + pub fn cross_signing_status(&self) -> CrossSigningStatus { + self.runtime.block_on(self.inner.cross_signing_status()).into() + } + + /// Export all our private cross signing keys. + /// + /// The export will contain the seed for the ed25519 keys as a base64 + /// encoded string. + /// + /// This method returns `None` if we don't have any private cross signing + /// keys. + pub fn export_cross_signing_keys(&self) -> Option { + self.runtime.block_on(self.inner.export_cross_signing_keys()).map(|e| e.into()) + } + + /// Import our private cross signing keys. + /// + /// The export needs to contain the seed for the ed25519 keys as a base64 + /// encoded string. + pub fn import_cross_signing_keys( + &self, + export: CrossSigningKeyExport, + ) -> Result<(), SecretImportError> { + self.runtime.block_on(self.inner.import_cross_signing_keys(export.into()))?; + + Ok(()) + } + + /// Activate the given backup key to be used with the given backup version. + /// + /// **Warning**: The caller needs to make sure that the given `BackupKey` is + /// trusted, otherwise we might be encrypting room keys that a malicious + /// party could decrypt. + /// + /// The [`OlmMachine::verify_backup`] method can be used to so. + pub fn enable_backup_v1( + &self, + key: MegolmV1BackupKey, + version: String, + ) -> Result<(), DecodeError> { + let backup_key = RustBackupKey::from_base64(&key.public_key)?; + backup_key.set_version(version); + + self.runtime.block_on(self.inner.backup_machine().enable_backup_v1(backup_key))?; + + Ok(()) + } + + /// Are we able to encrypt room keys. + /// + /// This returns true if we have an active `BackupKey` and backup version + /// registered with the state machine. + pub fn backup_enabled(&self) -> bool { + self.runtime.block_on(self.inner.backup_machine().enabled()) + } + + /// Disable and reset our backup state. + /// + /// This will remove any pending backup request, remove the backup key and + /// reset the backup state of each room key we have. + pub fn disable_backup(&self) -> Result<(), CryptoStoreError> { + Ok(self.runtime.block_on(self.inner.backup_machine().disable_backup())?) + } + + /// Encrypt a batch of room keys and return a request that needs to be sent + /// out to backup the room keys. + pub fn backup_room_keys(&self) -> Result, CryptoStoreError> { + let request = self.runtime.block_on(self.inner.backup_machine().backup())?; + + let request = request.map(|r| r.into()); + + Ok(request) + } + + /// Get the number of backed up room keys and the total number of room keys. + pub fn room_key_counts(&self) -> Result { + Ok(self.runtime.block_on(self.inner.backup_machine().room_key_counts())?.into()) + } + + /// Store the recovery key in the cryptostore. + /// + /// This is useful if the client wants to support gossiping of the backup + /// key. + pub fn save_recovery_key( + &self, + key: Option, + version: Option, + ) -> Result<(), CryptoStoreError> { + let key = key.map(|k| RecoveryKey::from_base64(&k)).transpose().ok().flatten(); + Ok(self.runtime.block_on(self.inner.backup_machine().save_recovery_key(key, version))?) + } + + /// Get the backup keys we have saved in our crypto store. + pub fn get_backup_keys(&self) -> Result, CryptoStoreError> { + Ok(self.runtime.block_on(self.inner.backup_machine().get_backup_keys())?.try_into().ok()) + } + + /// Sign the given message using our device key and if available cross + /// signing master key. + pub fn sign(&self, message: &str) -> HashMap> { + self.runtime + .block_on(self.inner.sign(message)) + .into_iter() + .map(|(k, v)| (k.to_string(), v.into_iter().map(|(k, v)| (k.to_string(), v)).collect())) + .collect() + } + + /// Check if the given backup has been verified by us or by another of our + /// devices that we trust. + pub fn verify_backup(&self, auth_data: &str) -> Result { + let auth_data = serde_json::from_str(auth_data)?; + Ok(self.runtime.block_on(self.inner.backup_machine().verify_backup(auth_data))?) + } +} diff --git a/crates/matrix-crypto-ffi/src/olm.udl b/crates/matrix-crypto-ffi/src/olm.udl new file mode 100644 index 000000000..b93a39823 --- /dev/null +++ b/crates/matrix-crypto-ffi/src/olm.udl @@ -0,0 +1,426 @@ +namespace olm { + void set_logger(Logger logger); +}; + +callback interface Logger { + void log(string log_line); +}; + +callback interface ProgressListener { + void on_progress(i32 progress, i32 total); +}; + +[Error] +enum PkDecryptionError { + "Olm", +}; + +[Error] +enum KeyImportError { + "Export", + "CryptoStore", + "Json", +}; + +[Error] +enum SignatureError { + "Signature", + "Identifier", + "CryptoStore", + "UnknownDevice", + "UnknownUserIdentity", +}; + +[Error] +enum SecretImportError { + "Import", + "CryptoStore", +}; + + +[Error] +enum CryptoStoreError { + "CryptoStore", + "OlmError", + "Serialization", + "Identifier", + "InvalidUserId", +}; + +[Error] +enum DecryptionError { + "Identifier", + "Serialization", + "Megolm", +}; + +dictionary DeviceLists { + sequence changed; + sequence left; +}; + +dictionary KeysImportResult { + i64 imported; + i64 total; + record>> keys; +}; + +dictionary DecryptedEvent { + string clear_event; + string sender_curve25519_key; + string? claimed_ed25519_key; + sequence forwarding_curve25519_chain; +}; + +dictionary Device { + string user_id; + string device_id; + record keys; + sequence algorithms; + string? display_name; + boolean is_blocked; + boolean locally_trusted; + boolean cross_signing_trusted; +}; + +[Enum] +interface UserIdentity { + Own( + string user_id, + boolean trusts_our_own_device, + string master_key, + string self_signing_key, + string user_signing_key + ); + Other( + string user_id, + string master_key, + string self_signing_key + ); +}; + +dictionary CrossSigningStatus { + boolean has_master; + boolean has_self_signing; + boolean has_user_signing; +}; + +dictionary CrossSigningKeyExport { + string? master_key; + string? self_signing_key; + string? user_signing_key; +}; + +dictionary UploadSigningKeysRequest { + string master_key; + string self_signing_key; + string user_signing_key; +}; + +dictionary BootstrapCrossSigningResult { + UploadSigningKeysRequest upload_signing_keys_request; + SignatureUploadRequest signature_request; +}; + +dictionary CancelInfo { + string cancel_code; + string reason; + boolean cancelled_by_us; +}; + +dictionary StartSasResult { + Sas sas; + OutgoingVerificationRequest request; +}; + +dictionary Sas { + string other_user_id; + string other_device_id; + string flow_id; + string? room_id; + boolean we_started; + boolean has_been_accepted; + boolean can_be_presented; + boolean supports_emoji; + boolean have_we_confirmed; + boolean is_done; + boolean is_cancelled; + CancelInfo? cancel_info; +}; + +dictionary ScanResult { + QrCode qr; + OutgoingVerificationRequest request; +}; + +dictionary QrCode { + string other_user_id; + string other_device_id; + string flow_id; + string? room_id; + boolean we_started; + boolean other_side_scanned; + boolean has_been_confirmed; + boolean reciprocated; + boolean is_done; + boolean is_cancelled; + CancelInfo? cancel_info; +}; + +dictionary VerificationRequest { + string other_user_id; + string? other_device_id; + string flow_id; + string? room_id; + boolean we_started; + boolean is_ready; + boolean is_passive; + boolean is_done; + boolean is_cancelled; + CancelInfo? cancel_info; + sequence? their_methods; + sequence? our_methods; + +}; + +dictionary RequestVerificationResult { + VerificationRequest verification; + OutgoingVerificationRequest request; +}; + +dictionary ConfirmVerificationResult { + OutgoingVerificationRequest request; + SignatureUploadRequest? signature_request; +}; + +[Enum] +interface Verification { + SasV1(Sas sas); + QrCodeV1(QrCode qrcode); +}; + +dictionary KeyRequestPair { + Request? cancellation; + Request key_request; +}; + +[Enum] +interface OutgoingVerificationRequest { + ToDevice(string request_id, string event_type, string body); + InRoom(string request_id, string room_id, string event_type, string content); +}; + +[Enum] +interface Request { + ToDevice(string request_id, string event_type, string body); + KeysUpload(string request_id, string body); + KeysQuery(string request_id, sequence users); + KeysClaim(string request_id, record> one_time_keys); + KeysBackup(string request_id, string version, string rooms); + RoomMessage(string request_id, string room_id, string event_type, string content); + SignatureUpload(string request_id, string body); +}; + +dictionary SignatureUploadRequest { + string body; +}; + +enum RequestType { + "KeysQuery", + "KeysClaim", + "KeysUpload", + "ToDevice", + "SignatureUpload", + "KeysBackup", +}; + +interface OlmMachine { + [Throws=CryptoStoreError] + constructor([ByRef] string user_id, [ByRef] string device_id, [ByRef] string path); + + record identity_keys(); + string user_id(); + string device_id(); + + [Throws=CryptoStoreError] + string receive_sync_changes([ByRef] string events, + DeviceLists device_changes, + record key_counts, + sequence? unused_fallback_keys); + [Throws=CryptoStoreError] + sequence outgoing_requests(); + [Throws=CryptoStoreError] + void mark_request_as_sent( + [ByRef] string request_id, + RequestType request_type, + [ByRef] string response + ); + + [Throws=DecryptionError] + DecryptedEvent decrypt_room_event([ByRef] string event, [ByRef] string room_id); + [Throws=CryptoStoreError] + string encrypt([ByRef] string room_id, [ByRef] string event_type, [ByRef] string content); + + [Throws=CryptoStoreError] + UserIdentity? get_identity([ByRef] string user_id); + [Throws=SignatureError] + SignatureUploadRequest verify_identity([ByRef] string user_id); + [Throws=CryptoStoreError] + Device? get_device([ByRef] string user_id, [ByRef] string device_id); + [Throws=CryptoStoreError] + void mark_device_as_trusted([ByRef] string user_id, [ByRef] string device_id); + [Throws=SignatureError] + SignatureUploadRequest verify_device([ByRef] string user_id, [ByRef] string device_id); + [Throws=CryptoStoreError] + sequence get_user_devices([ByRef] string user_id); + + [Throws=CryptoStoreError] + boolean is_user_tracked([ByRef] string user_id); + void update_tracked_users(sequence users); + [Throws=CryptoStoreError] + Request? get_missing_sessions(sequence users); + [Throws=CryptoStoreError] + sequence share_room_key([ByRef] string room_id, sequence users); + + sequence get_verification_requests([ByRef] string user_id); + VerificationRequest? get_verification_request([ByRef] string user_id, [ByRef] string flow_id); + Verification? get_verification([ByRef] string user_id, [ByRef] string flow_id); + + [Throws=CryptoStoreError] + VerificationRequest? request_verification( + [ByRef] string user_id, + [ByRef] string room_id, + [ByRef] string event_id, + sequence methods + ); + [Throws=CryptoStoreError] + string? verification_request_content( + [ByRef] string user_id, + sequence methods + ); + [Throws=CryptoStoreError] + RequestVerificationResult? request_self_verification(sequence methods); + [Throws=CryptoStoreError] + RequestVerificationResult? request_verification_with_device( + [ByRef] string user_id, + [ByRef] string device_id, + sequence methods + ); + + OutgoingVerificationRequest? accept_verification_request( + [ByRef] string user_id, + [ByRef] string flow_id, + sequence methods + ); + + [Throws=CryptoStoreError] + ConfirmVerificationResult? confirm_verification([ByRef] string user_id, [ByRef] string flow_id); + OutgoingVerificationRequest? cancel_verification( + [ByRef] string user_id, + [ByRef] string flow_id, + [ByRef] string cancel_code + ); + + [Throws=CryptoStoreError] + StartSasResult? start_sas_with_device([ByRef] string user_id, [ByRef] string device_id); + [Throws=CryptoStoreError] + StartSasResult? start_sas_verification([ByRef] string user_id, [ByRef] string flow_id); + OutgoingVerificationRequest? accept_sas_verification([ByRef] string user_id, [ByRef] string flow_id); + sequence? get_emoji_index([ByRef] string user_id, [ByRef] string flow_id); + sequence? get_decimals([ByRef] string user_id, [ByRef] string flow_id); + + [Throws=CryptoStoreError] + QrCode? start_qr_verification([ByRef] string user_id, [ByRef] string flow_id); + ScanResult? scan_qr_code([ByRef] string user_id, [ByRef] string flow_id, [ByRef] string data); + string? generate_qr_code([ByRef] string user_id, [ByRef] string flow_id); + + [Throws=DecryptionError] + KeyRequestPair request_room_key([ByRef] string event, [ByRef] string room_id); + + [Throws=CryptoStoreError] + string export_keys([ByRef] string passphrase, i32 rounds); + [Throws=KeyImportError] + KeysImportResult import_keys( + [ByRef] string keys, + [ByRef] string passphrase, + ProgressListener progress_listener + ); + [Throws=KeyImportError] + KeysImportResult import_decrypted_keys( + [ByRef] string keys, + ProgressListener progress_listener + ); + [Throws=CryptoStoreError] + void discard_room_key([ByRef] string room_id); + + CrossSigningStatus cross_signing_status(); + [Throws=CryptoStoreError] + BootstrapCrossSigningResult bootstrap_cross_signing(); + CrossSigningKeyExport? export_cross_signing_keys(); + [Throws=SecretImportError] + void import_cross_signing_keys(CrossSigningKeyExport export); + [Throws=CryptoStoreError] + boolean is_identity_verified([ByRef] string user_id); + + record> sign([ByRef] string message); + [Throws=DecodeError] + void enable_backup_v1(MegolmV1BackupKey key, string version); + [Throws=CryptoStoreError] + void disable_backup(); + [Throws=CryptoStoreError] + Request? backup_room_keys(); + [Throws=CryptoStoreError] + void save_recovery_key(string? key, string? version); + [Throws=CryptoStoreError] + RoomKeyCounts room_key_counts(); + [Throws=CryptoStoreError] + BackupKeys? get_backup_keys(); + boolean backup_enabled(); + [Throws=CryptoStoreError] + boolean verify_backup([ByRef] string auth_data); +}; + +dictionary PassphraseInfo { + string private_key_salt; + i32 private_key_iterations; +}; + +dictionary MegolmV1BackupKey { + string public_key; + record> signatures; + PassphraseInfo? passphrase_info; + string backup_algorithm; +}; + +dictionary BackupKeys { + string recovery_key; + string backup_version; +}; + +dictionary RoomKeyCounts { + i64 total; + i64 backed_up; +}; + +[Error] +enum DecodeError { + "Decode", + "CryptoStore", +}; + +interface BackupRecoveryKey { + constructor(); + [Name=from_passphrase] + constructor(string passphrase, string salt, i32 rounds); + [Name=new_from_passphrase] + constructor(string passphrase); + [Name=from_base64, Throws=DecodeError] + constructor(string key); + [Name=from_base58, Throws=DecodeError] + constructor(string key); + string to_base58(); + string to_base64(); + MegolmV1BackupKey megolm_v1_public_key(); + [Throws=PkDecryptionError] + string decrypt_v1(string ephemeral_key, string mac, string ciphertext); +}; diff --git a/crates/matrix-crypto-ffi/src/responses.rs b/crates/matrix-crypto-ffi/src/responses.rs new file mode 100644 index 000000000..eacb24c5b --- /dev/null +++ b/crates/matrix-crypto-ffi/src/responses.rs @@ -0,0 +1,322 @@ +#![allow(missing_docs)] + +use std::{collections::HashMap, convert::TryFrom}; + +use http::Response; +use matrix_sdk_common::uuid::Uuid; +use matrix_sdk_crypto::{ + IncomingResponse, OutgoingRequest, OutgoingVerificationRequest as SdkVerificationRequest, + RoomMessageRequest, ToDeviceRequest, UploadSigningKeysRequest as RustUploadSigningKeysRequest, +}; +use ruma::{ + api::client::r0::{ + backup::add_backup_keys::Response as KeysBackupResponse, + keys::{ + claim_keys::{Request as KeysClaimRequest, Response as KeysClaimResponse}, + get_keys::Response as KeysQueryResponse, + upload_keys::Response as KeysUploadResponse, + upload_signatures::{ + Request as RustSignatureUploadRequest, Response as SignatureUploadResponse, + }, + }, + sync::sync_events::DeviceLists as RumaDeviceLists, + to_device::send_event_to_device::Response as ToDeviceResponse, + }, + assign, + events::EventContent, + identifiers::UserId, +}; +use serde_json::json; + +pub struct SignatureUploadRequest { + pub body: String, +} + +impl From for SignatureUploadRequest { + fn from(r: RustSignatureUploadRequest) -> Self { + Self { + body: serde_json::to_string(&r.signed_keys) + .expect("Can't serialize signature upload request"), + } + } +} + +pub struct UploadSigningKeysRequest { + pub master_key: String, + pub self_signing_key: String, + pub user_signing_key: String, +} + +impl From for UploadSigningKeysRequest { + fn from(r: RustUploadSigningKeysRequest) -> Self { + Self { + master_key: serde_json::to_string( + &r.master_key.expect("Request didn't contain a master key"), + ) + .expect("Can't serialize cross signing master key"), + self_signing_key: serde_json::to_string( + &r.self_signing_key.expect("Request didn't contain a self-signing key"), + ) + .expect("Can't serialize cross signing self-signing key"), + user_signing_key: serde_json::to_string( + &r.user_signing_key.expect("Request didn't contain a user-signing key"), + ) + .expect("Can't serialize cross signing user-signing key"), + } + } +} + +pub struct BootstrapCrossSigningResult { + pub upload_signing_keys_request: UploadSigningKeysRequest, + pub signature_request: SignatureUploadRequest, +} + +impl From<(RustUploadSigningKeysRequest, RustSignatureUploadRequest)> + for BootstrapCrossSigningResult +{ + fn from(requests: (RustUploadSigningKeysRequest, RustSignatureUploadRequest)) -> Self { + Self { + upload_signing_keys_request: requests.0.into(), + signature_request: requests.1.into(), + } + } +} + +pub enum OutgoingVerificationRequest { + ToDevice { request_id: String, event_type: String, body: String }, + InRoom { request_id: String, room_id: String, event_type: String, content: String }, +} + +impl From for OutgoingVerificationRequest { + fn from(r: SdkVerificationRequest) -> Self { + match r { + SdkVerificationRequest::ToDevice(r) => r.into(), + SdkVerificationRequest::InRoom(r) => Self::InRoom { + request_id: r.txn_id.to_string(), + room_id: r.room_id.to_string(), + content: serde_json::to_string(&r.content) + .expect("Can't serialize message content"), + event_type: r.content.event_type().to_string(), + }, + } + } +} + +impl From for OutgoingVerificationRequest { + fn from(r: ToDeviceRequest) -> Self { + Self::ToDevice { + request_id: r.txn_id_string(), + event_type: r.event_type.to_string(), + body: serde_json::to_string(&r.messages).expect("Can't serialize to-device body"), + } + } +} + +#[derive(Debug)] +pub enum Request { + ToDevice { request_id: String, event_type: String, body: String }, + KeysUpload { request_id: String, body: String }, + KeysQuery { request_id: String, users: Vec }, + KeysClaim { request_id: String, one_time_keys: HashMap> }, + RoomMessage { request_id: String, room_id: String, event_type: String, content: String }, + SignatureUpload { request_id: String, body: String }, + KeysBackup { request_id: String, version: String, rooms: String }, +} + +impl From for Request { + fn from(r: OutgoingRequest) -> Self { + use matrix_sdk_crypto::OutgoingRequests::*; + + match r.request() { + KeysUpload(u) => { + let body = json!({ + "device_keys": u.device_keys, + "one_time_keys": u.one_time_keys, + }); + + Request::KeysUpload { + request_id: r.request_id().to_string(), + body: serde_json::to_string(&body) + .expect("Can't serialize keys upload request"), + } + } + KeysQuery(k) => { + let users: Vec = k.device_keys.keys().map(|u| u.to_string()).collect(); + Request::KeysQuery { request_id: r.request_id().to_string(), users } + } + ToDeviceRequest(t) => Request::from(t), + SignatureUpload(t) => Request::SignatureUpload { + request_id: r.request_id().to_string(), + body: serde_json::to_string(&t.signed_keys) + .expect("Can't serialize signature upload request"), + }, + RoomMessage(r) => Request::from(r), + KeysClaim(c) => (*r.request_id(), c.clone()).into(), + KeysBackup(b) => Request::KeysBackup { + request_id: r.request_id().to_string(), + version: b.version.to_owned(), + rooms: serde_json::to_string(&b.rooms) + .expect("Can't serialize keys backup request"), + }, + } + } +} + +impl From for Request { + fn from(r: ToDeviceRequest) -> Self { + Request::ToDevice { + request_id: r.txn_id_string(), + event_type: r.event_type.to_string(), + body: serde_json::to_string(&r.messages).expect("Can't serialize to-device body"), + } + } +} + +impl From<(Uuid, KeysClaimRequest)> for Request { + fn from(request_tuple: (Uuid, KeysClaimRequest)) -> Self { + let (request_id, request) = request_tuple; + + Request::KeysClaim { + request_id: request_id.to_string(), + one_time_keys: request + .one_time_keys + .into_iter() + .map(|(u, d)| { + ( + u.to_string(), + d.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(), + ) + }) + .collect(), + } + } +} + +impl From<&ToDeviceRequest> for Request { + fn from(r: &ToDeviceRequest) -> Self { + Request::ToDevice { + request_id: r.txn_id_string(), + event_type: r.event_type.to_string(), + body: serde_json::to_string(&r.messages).expect("Can't serialize to-device body"), + } + } +} + +impl From<&RoomMessageRequest> for Request { + fn from(r: &RoomMessageRequest) -> Self { + Self::RoomMessage { + request_id: r.txn_id.to_string(), + room_id: r.room_id.to_string(), + event_type: r.content.event_type().to_string(), + content: serde_json::to_string(&r.content).expect("Can't serialize message content"), + } + } +} + +pub(crate) fn response_from_string(body: &str) -> Response> { + Response::builder() + .status(200) + .body(body.as_bytes().to_vec()) + .expect("Can't create HTTP response") +} + +pub enum RequestType { + KeysQuery, + KeysClaim, + KeysUpload, + ToDevice, + SignatureUpload, + KeysBackup, +} + +pub struct DeviceLists { + pub changed: Vec, + pub left: Vec, +} + +impl From for RumaDeviceLists { + fn from(d: DeviceLists) -> Self { + assign!(RumaDeviceLists::new(), { + changed: d + .changed + .into_iter() + .filter_map(|u| Box::::try_from(u).ok()) + .collect(), + left: d + .left + .into_iter() + .filter_map(|u| Box::::try_from(u).ok()) + .collect(), + }) + } +} + +pub struct KeysImportResult { + /// The number of room keys that were imported. + pub imported: i64, + /// The total number of room keys that were found in the export. + pub total: i64, + /// The map of keys that were imported. + /// + /// It's a map from room id to a map of the sender key to a list of session + /// ids. + pub keys: HashMap>>, +} + +pub(crate) enum OwnedResponse { + KeysClaim(KeysClaimResponse), + KeysUpload(KeysUploadResponse), + KeysQuery(KeysQueryResponse), + ToDevice(ToDeviceResponse), + SignatureUpload(SignatureUploadResponse), + KeysBackup(KeysBackupResponse), +} + +impl From for OwnedResponse { + fn from(response: KeysClaimResponse) -> Self { + OwnedResponse::KeysClaim(response) + } +} + +impl From for OwnedResponse { + fn from(response: KeysQueryResponse) -> Self { + OwnedResponse::KeysQuery(response) + } +} + +impl From for OwnedResponse { + fn from(response: KeysUploadResponse) -> Self { + OwnedResponse::KeysUpload(response) + } +} + +impl From for OwnedResponse { + fn from(response: ToDeviceResponse) -> Self { + OwnedResponse::ToDevice(response) + } +} + +impl From for OwnedResponse { + fn from(response: SignatureUploadResponse) -> Self { + Self::SignatureUpload(response) + } +} + +impl From for OwnedResponse { + fn from(r: KeysBackupResponse) -> Self { + Self::KeysBackup(r) + } +} + +impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> { + fn from(r: &'a OwnedResponse) -> Self { + match r { + OwnedResponse::KeysClaim(r) => IncomingResponse::KeysClaim(r), + OwnedResponse::KeysQuery(r) => IncomingResponse::KeysQuery(r), + OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r), + OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r), + OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r), + OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r), + } + } +} diff --git a/crates/matrix-crypto-ffi/src/users.rs b/crates/matrix-crypto-ffi/src/users.rs new file mode 100644 index 000000000..4c1a05a55 --- /dev/null +++ b/crates/matrix-crypto-ffi/src/users.rs @@ -0,0 +1,61 @@ +use matrix_sdk_crypto::UserIdentities; +use ruma::encryption::CrossSigningKey; + +use crate::CryptoStoreError; + +/// Enum representing cross signing identities of our own user or some other +/// user. +pub enum UserIdentity { + /// Our own user identity. + Own { + /// The unique id of our own user. + user_id: String, + /// Does our own user identity trust our own device. + trusts_our_own_device: bool, + /// The public master key of our identity. + master_key: String, + /// The public user-signing key of our identity. + user_signing_key: String, + /// The public self-signing key of our identity. + self_signing_key: String, + }, + /// The user identity of other users. + Other { + /// The unique id of the user. + user_id: String, + /// The public master key of the identity. + master_key: String, + /// The public self-signing key of our identity. + self_signing_key: String, + }, +} + +impl UserIdentity { + pub(crate) async fn from_rust(i: UserIdentities) -> Result { + Ok(match i { + UserIdentities::Own(i) => { + let master: CrossSigningKey = i.master_key().to_owned().into(); + let user_signing: CrossSigningKey = i.user_signing_key().to_owned().into(); + let self_signing: CrossSigningKey = i.self_signing_key().to_owned().into(); + + UserIdentity::Own { + user_id: i.user_id().to_string(), + trusts_our_own_device: i.trusts_our_own_device().await?, + master_key: serde_json::to_string(&master)?, + user_signing_key: serde_json::to_string(&user_signing)?, + self_signing_key: serde_json::to_string(&self_signing)?, + } + } + UserIdentities::Other(i) => { + let master: CrossSigningKey = i.master_key().to_owned().into(); + let self_signing: CrossSigningKey = i.self_signing_key().to_owned().into(); + + UserIdentity::Other { + user_id: i.user_id().to_string(), + master_key: serde_json::to_string(&master)?, + self_signing_key: serde_json::to_string(&self_signing)?, + } + } + }) + } +} diff --git a/crates/matrix-crypto-ffi/src/verification.rs b/crates/matrix-crypto-ffi/src/verification.rs new file mode 100644 index 000000000..13d7204d9 --- /dev/null +++ b/crates/matrix-crypto-ffi/src/verification.rs @@ -0,0 +1,232 @@ +use matrix_sdk_crypto::{ + CancelInfo as RustCancelInfo, QrVerification as InnerQr, Sas as InnerSas, + VerificationRequest as InnerVerificationRequest, +}; + +use crate::{OutgoingVerificationRequest, SignatureUploadRequest}; + +/// Enum representing the different verification flows we support. +pub enum Verification { + /// The `m.sas.v1` verification flow. + SasV1 { + #[allow(missing_docs)] + sas: Sas, + }, + /// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1` + /// verification flow. + QrCodeV1 { + #[allow(missing_docs)] + qrcode: QrCode, + }, +} + +/// The `m.sas.v1` verification flow. +pub struct Sas { + /// The other user that is participating in the verification flow + pub other_user_id: String, + /// The other user's device that is participating in the verification flow + pub other_device_id: String, + /// The unique ID of this verification flow, will be a random string for + /// to-device events or a event ID for in-room events. + pub flow_id: String, + /// The room ID where this verification is happening, will be `None` if the + /// verification is going through to-device messages + pub room_id: Option, + /// Did we initiate the verification flow + pub we_started: bool, + /// Has the non-initiating side accepted the verification flow + pub has_been_accepted: bool, + /// Can the short auth string be presented + pub can_be_presented: bool, + /// Does the flow support the emoji representation of the short auth string + pub supports_emoji: bool, + /// Have we confirmed that the short auth strings match + pub have_we_confirmed: bool, + /// Has the verification completed successfully + pub is_done: bool, + /// Has the flow been cancelled + pub is_cancelled: bool, + /// Information about the cancellation of the flow, will be `None` if the + /// flow hasn't been cancelled + pub cancel_info: Option, +} + +/// The `m.qr_code.scan.v1`, `m.qr_code.show.v1`, and `m.reciprocate.v1` +/// verification flow. +pub struct QrCode { + /// The other user that is participating in the verification flow + pub other_user_id: String, + /// The other user's device that is participating in the verification flow + pub other_device_id: String, + /// The unique ID of this verification flow, will be a random string for + /// to-device events or a event ID for in-room events. + pub flow_id: String, + /// The room ID where this verification is happening, will be `None` if the + /// verification is going through to-device messages + pub room_id: Option, + /// Did we initiate the verification flow + pub we_started: bool, + /// Has the QR code been scanned by the other side + pub other_side_scanned: bool, + /// Has the scanning of the QR code been confirmed by us + pub has_been_confirmed: bool, + /// Did we scan the QR code and sent out a reciprocation + pub reciprocated: bool, + /// Has the verification completed successfully + pub is_done: bool, + /// Has the flow been cancelled + pub is_cancelled: bool, + /// Information about the cancellation of the flow, will be `None` if the + /// flow hasn't been cancelled + pub cancel_info: Option, +} + +impl From for QrCode { + fn from(qr: InnerQr) -> Self { + Self { + other_user_id: qr.other_user_id().to_string(), + flow_id: qr.flow_id().as_str().to_owned(), + is_cancelled: qr.is_cancelled(), + is_done: qr.is_done(), + cancel_info: qr.cancel_info().map(|c| c.into()), + reciprocated: qr.reciprocated(), + we_started: qr.we_started(), + other_side_scanned: qr.has_been_scanned(), + has_been_confirmed: qr.has_been_confirmed(), + other_device_id: qr.other_device_id().to_string(), + room_id: qr.room_id().map(|r| r.to_string()), + } + } +} + +/// Information on why a verification flow has been cancelled and by whom. +pub struct CancelInfo { + /// The textual representation of the cancel reason + pub reason: String, + /// The code describing the cancel reason + pub cancel_code: String, + /// Was the verification flow cancelled by us + pub cancelled_by_us: bool, +} + +impl From for CancelInfo { + fn from(c: RustCancelInfo) -> Self { + Self { + reason: c.reason().to_owned(), + cancel_code: c.cancel_code().to_string(), + cancelled_by_us: c.cancelled_by_us(), + } + } +} + +/// A result type for starting SAS verifications. +pub struct StartSasResult { + /// The SAS verification object that got created. + pub sas: Sas, + /// The request that needs to be sent out to notify the other side that a + /// SAS verification should start. + pub request: OutgoingVerificationRequest, +} + +/// A result type for scanning QR codes. +pub struct ScanResult { + /// The QR code verification object that got created. + pub qr: QrCode, + /// The request that needs to be sent out to notify the other side that a + /// QR code verification should start. + pub request: OutgoingVerificationRequest, +} + +impl From for Sas { + fn from(sas: InnerSas) -> Self { + Self { + other_user_id: sas.other_user_id().to_string(), + other_device_id: sas.other_device_id().to_string(), + flow_id: sas.flow_id().as_str().to_owned(), + is_cancelled: sas.is_cancelled(), + is_done: sas.is_done(), + can_be_presented: sas.can_be_presented(), + supports_emoji: sas.supports_emoji(), + have_we_confirmed: sas.have_we_confirmed(), + we_started: sas.we_started(), + room_id: sas.room_id().map(|r| r.to_string()), + has_been_accepted: sas.has_been_accepted(), + cancel_info: sas.cancel_info().map(|c| c.into()), + } + } +} + +/// A result type for requesting verifications. +pub struct RequestVerificationResult { + /// The verification request object that got created. + pub verification: VerificationRequest, + /// The request that needs to be sent out to notify the other side that + /// we're requesting verification to begin. + pub request: OutgoingVerificationRequest, +} + +/// A result type for confirming verifications. +pub struct ConfirmVerificationResult { + /// The request that needs to be sent out to notify the other side that we + /// confirmed the verification. + pub request: OutgoingVerificationRequest, + /// A request that will upload signatures of the verified device or user, if + /// the verification is completed and we're able to sign devices or users + pub signature_request: Option, +} + +/// The verificatoin request object which then can transition into some concrete +/// verification method +pub struct VerificationRequest { + /// The other user that is participating in the verification flow + pub other_user_id: String, + /// The other user's device that is participating in the verification flow + pub other_device_id: Option, + /// The unique ID of this verification flow, will be a random string for + /// to-device events or a event ID for in-room events. + pub flow_id: String, + /// The room ID where this verification is happening, will be `None` if the + /// verification is going through to-device messages + pub room_id: Option, + /// Did we initiate the verification flow + pub we_started: bool, + /// Did both parties aggree to verification + pub is_ready: bool, + /// Did another device respond to the verification request + pub is_passive: bool, + /// Has the verification completed successfully + pub is_done: bool, + /// Has the flow been cancelled + pub is_cancelled: bool, + /// The list of verification methods that the other side advertised as + /// supported + pub their_methods: Option>, + /// The list of verification methods that we advertised as supported + pub our_methods: Option>, + /// Information about the cancellation of the flow, will be `None` if the + /// flow hasn't been cancelled + pub cancel_info: Option, +} + +impl From for VerificationRequest { + fn from(v: InnerVerificationRequest) -> Self { + Self { + other_user_id: v.other_user().to_string(), + other_device_id: v.other_device_id().map(|d| d.to_string()), + flow_id: v.flow_id().as_str().to_owned(), + is_cancelled: v.is_cancelled(), + is_done: v.is_done(), + is_ready: v.is_ready(), + room_id: v.room_id().map(|r| r.to_string()), + we_started: v.we_started(), + is_passive: v.is_passive(), + cancel_info: v.cancel_info().map(|c| c.into()), + their_methods: v + .their_supported_methods() + .map(|v| v.into_iter().map(|m| m.to_string()).collect()), + our_methods: v + .our_supported_methods() + .map(|v| v.into_iter().map(|m| m.to_string()).collect()), + } + } +}