From 00c3921d2a7456d70c68a2889b629254d88aa115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 9 Jun 2021 16:30:15 +0200 Subject: [PATCH] crypto: Add initial support for QR code verification --- matrix_sdk_crypto/Cargo.toml | 1 + matrix_sdk_crypto/src/identities/user.rs | 10 +- matrix_sdk_crypto/src/lib.rs | 3 +- matrix_sdk_crypto/src/machine.rs | 2 +- matrix_sdk_crypto/src/olm/signing/mod.rs | 21 +- .../src/session_manager/group_sessions.rs | 5 - matrix_sdk_crypto/src/store/mod.rs | 2 +- matrix_sdk_crypto/src/verification/cache.rs | 18 +- matrix_sdk_crypto/src/verification/machine.rs | 11 + matrix_sdk_crypto/src/verification/mod.rs | 52 +- matrix_sdk_crypto/src/verification/qrcode.rs | 886 ++++++++++++++++++ .../src/verification/requests.rs | 196 +++- 12 files changed, 1177 insertions(+), 30 deletions(-) create mode 100644 matrix_sdk_crypto/src/verification/qrcode.rs diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index 764b1f2c..bbad56e7 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -20,6 +20,7 @@ sled_cryptostore = ["sled"] docs = ["sled_cryptostore"] [dependencies] +matrix-qrcode = { version = "0.1.0", path = "../matrix_qrcode" } matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" } ruma = { version = "0.1.2", features = ["client-api-c", "unstable-pre-spec"] } diff --git a/matrix_sdk_crypto/src/identities/user.rs b/matrix_sdk_crypto/src/identities/user.rs index 4142ea0f..2015e56c 100644 --- a/matrix_sdk_crypto/src/identities/user.rs +++ b/matrix_sdk_crypto/src/identities/user.rs @@ -213,6 +213,14 @@ impl MasterPubkey { self.0.keys.get(key_id.as_str()).map(|k| k.as_str()) } + /// Get the first available master key. + /// + /// There's usually only a single master key so this will usually fetch the + /// only key. + pub fn get_first_key(&self) -> Option<&str> { + self.0.keys.values().map(|k| k.as_str()).next() + } + /// Check if the given cross signing sub-key is signed by the master key. /// /// # Arguments @@ -803,7 +811,7 @@ pub(crate) mod test { Arc::new(MemoryStore::new()), ); - let public_identity = identity.as_public_identity().await.unwrap(); + let public_identity = identity.to_public_identity().await.unwrap(); let mut device = Device { inner: device, diff --git a/matrix_sdk_crypto/src/lib.rs b/matrix_sdk_crypto/src/lib.rs index 1fef664e..dcd4db02 100644 --- a/matrix_sdk_crypto/src/lib.rs +++ b/matrix_sdk_crypto/src/lib.rs @@ -48,6 +48,7 @@ pub use identities::{ Device, LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserDevices, UserIdentities, UserIdentity, }; pub use machine::OlmMachine; +pub use matrix_qrcode; pub use olm::EncryptionSettings; pub(crate) use olm::ReadOnlyAccount; pub use requests::{ @@ -55,4 +56,4 @@ pub use requests::{ OutgoingVerificationRequest, RoomMessageRequest, ToDeviceRequest, }; pub use store::CryptoStoreError; -pub use verification::{AcceptSettings, Sas, VerificationRequest}; +pub use verification::{AcceptSettings, QrVerification, Sas, Verification, VerificationRequest}; diff --git a/matrix_sdk_crypto/src/machine.rs b/matrix_sdk_crypto/src/machine.rs index 6abde4de..258c61b1 100644 --- a/matrix_sdk_crypto/src/machine.rs +++ b/matrix_sdk_crypto/src/machine.rs @@ -379,7 +379,7 @@ impl OlmMachine { *identity = id; - let public = identity.as_public_identity().await.expect( + let public = identity.to_public_identity().await.expect( "Couldn't create a public version of the identity from a new private identity", ); diff --git a/matrix_sdk_crypto/src/olm/signing/mod.rs b/matrix_sdk_crypto/src/olm/signing/mod.rs index 7f4581cd..3f28b6ad 100644 --- a/matrix_sdk_crypto/src/olm/signing/mod.rs +++ b/matrix_sdk_crypto/src/olm/signing/mod.rs @@ -33,8 +33,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Error as JsonError; use crate::{ - error::SignatureError, requests::UploadSigningKeysRequest, OwnUserIdentity, ReadOnlyAccount, - ReadOnlyDevice, UserIdentity, + error::SignatureError, identities::MasterPubkey, requests::UploadSigningKeysRequest, + OwnUserIdentity, ReadOnlyAccount, ReadOnlyDevice, UserIdentity, }; /// Private cross signing identity. @@ -91,6 +91,21 @@ impl PrivateCrossSigningIdentity { !(has_master && has_user && has_self) } + /// Can we sign our own devices, i.e. do we have a self signing key. + pub async fn can_sign_devices(&self) -> bool { + self.self_signing_key.lock().await.is_some() + } + + /// Do we have the master key. + pub async fn has_master_key(&self) -> bool { + self.master_key.lock().await.is_some() + } + + /// Get the public part of the master key, if we have one. + pub async fn master_public_key(&self) -> Option { + self.master_key.lock().await.as_ref().map(|m| m.public_key.to_owned()) + } + /// Create a new empty identity. pub(crate) fn empty(user_id: UserId) -> Self { Self { @@ -102,7 +117,7 @@ impl PrivateCrossSigningIdentity { } } - pub(crate) async fn as_public_identity(&self) -> Result { + pub(crate) async fn to_public_identity(&self) -> Result { let master = self .master_key .lock() diff --git a/matrix_sdk_crypto/src/session_manager/group_sessions.rs b/matrix_sdk_crypto/src/session_manager/group_sessions.rs index 26269716..5d084a4c 100644 --- a/matrix_sdk_crypto/src/session_manager/group_sessions.rs +++ b/matrix_sdk_crypto/src/session_manager/group_sessions.rs @@ -146,11 +146,6 @@ impl GroupSessionManager { let mut changes = Changes::default(); changes.outbound_group_sessions.push(s.clone()); self.store.save_changes(changes).await?; - } else { - trace!( - request_id = request_id.to_string().as_str(), - "Marking room key share request as sent but session found that owns the given id" - ) } Ok(()) diff --git a/matrix_sdk_crypto/src/store/mod.rs b/matrix_sdk_crypto/src/store/mod.rs index 1f41609d..022c5c03 100644 --- a/matrix_sdk_crypto/src/store/mod.rs +++ b/matrix_sdk_crypto/src/store/mod.rs @@ -239,7 +239,7 @@ impl Deref for Store { } } -#[derive(Error, Debug)] +#[derive(Debug, Error)] /// The crypto store's error type. pub enum CryptoStoreError { /// The account that owns the sessions, group sessions, and devices wasn't diff --git a/matrix_sdk_crypto/src/verification/cache.rs b/matrix_sdk_crypto/src/verification/cache.rs index 8d54a165..bf0fd9f8 100644 --- a/matrix_sdk_crypto/src/verification/cache.rs +++ b/matrix_sdk_crypto/src/verification/cache.rs @@ -19,7 +19,7 @@ use matrix_sdk_common::uuid::Uuid; use ruma::{DeviceId, UserId}; use super::{event_enums::OutgoingContent, sas::content_to_request, Sas, Verification}; -use crate::{OutgoingRequest, RoomMessageRequest}; +use crate::{OutgoingRequest, QrVerification, RoomMessageRequest}; #[derive(Clone, Debug)] pub struct VerificationCache { @@ -51,6 +51,20 @@ impl VerificationCache { self.insert(sas); } + pub fn insert_qr(&self, qr: QrVerification) { + self.insert(qr) + } + + pub fn get_qr(&self, sender: &UserId, flow_id: &str) -> Option { + self.get(sender, flow_id).and_then(|v| { + if let Verification::QrV1(qr) = v { + Some(qr) + } else { + None + } + }) + } + pub fn get(&self, sender: &UserId, flow_id: &str) -> Option { self.verification.get(sender).and_then(|m| m.get(flow_id).map(|v| v.clone())) } @@ -92,7 +106,7 @@ impl VerificationCache { pub fn get_sas(&self, user_id: &UserId, flow_id: &str) -> Option { self.get(user_id, flow_id).and_then(|v| { if let Verification::SasV1(sas) = v { - Some(sas.clone()) + Some(sas) } else { None } diff --git a/matrix_sdk_crypto/src/verification/machine.rs b/matrix_sdk_crypto/src/verification/machine.rs index c84e39dc..99c35ab7 100644 --- a/matrix_sdk_crypto/src/verification/machine.rs +++ b/matrix_sdk_crypto/src/verification/machine.rs @@ -340,6 +340,17 @@ impl VerificationMachine { self.mark_sas_as_done(sas, content).await?; } } + Some(Verification::QrV1(qr)) => { + let (cancellation, request) = qr.receive_done(&c).await?; + + if let Some(c) = cancellation { + self.verifications.add_request(c.into()) + } + + if let Some(s) = request { + self.verifications.add_request(s.into()) + } + } None => (), } } diff --git a/matrix_sdk_crypto/src/verification/mod.rs b/matrix_sdk_crypto/src/verification/mod.rs index f0c5776e..aa0a99e8 100644 --- a/matrix_sdk_crypto/src/verification/mod.rs +++ b/matrix_sdk_crypto/src/verification/mod.rs @@ -12,11 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -#![allow(missing_docs)] - mod cache; mod event_enums; mod machine; +mod qrcode; mod requests; mod sas; @@ -24,6 +23,7 @@ use std::sync::Arc; use event_enums::OutgoingContent; pub use machine::VerificationMachine; +pub use qrcode::QrVerification; pub use requests::VerificationRequest; use ruma::{ api::client::r0::keys::upload_signatures::Request as SignatureUploadRequest, @@ -47,18 +47,25 @@ use crate::{ CryptoStoreError, LocalTrust, ReadOnlyDevice, UserIdentities, }; +/// An enum over the different verification types the SDK supports. #[derive(Clone, Debug)] pub enum Verification { + /// The `m.sas.v1` verification variant. SasV1(Sas), + /// The `m.qr_code.*.v1` verification variant. + QrV1(QrVerification), } impl Verification { + /// Has this verification finished. pub fn is_done(&self) -> bool { match self { Verification::SasV1(s) => s.is_done(), + Verification::QrV1(qr) => qr.is_done(), } } + /// Try to deconstruct this verification enum into a SAS verification. pub fn sas_v1(self) -> Option { if let Verification::SasV1(sas) = self { Some(sas) @@ -67,21 +74,52 @@ impl Verification { } } + /// Try to deconstruct this verification enum into a QR code verification. + pub fn qr_v1(self) -> Option { + if let Verification::QrV1(qr) = self { + Some(qr) + } else { + None + } + } + + /// Get the ID that uniquely identifies this verification flow. pub fn flow_id(&self) -> &str { match self { Verification::SasV1(s) => s.flow_id().as_str(), + Verification::QrV1(qr) => qr.flow_id().as_str(), } } + /// Has the verification been cancelled. pub fn is_cancelled(&self) -> bool { match self { Verification::SasV1(s) => s.is_cancelled(), + Verification::QrV1(qr) => qr.is_cancelled(), } } + /// Get our own user id that is participating in this verification. + pub fn user_id(&self) -> &UserId { + match self { + Verification::SasV1(v) => v.user_id(), + Verification::QrV1(v) => v.user_id(), + } + } + + /// Get the other user id that is participating in this verification. pub fn other_user(&self) -> &UserId { match self { Verification::SasV1(s) => s.other_user_id(), + Verification::QrV1(qr) => qr.other_user_id(), + } + } + + /// Is this a verification verifying a device that belongs to us. + pub fn is_self_verification(&self) -> bool { + match self { + Verification::SasV1(v) => v.is_self_verification(), + Verification::QrV1(v) => v.is_self_verification(), } } } @@ -92,6 +130,12 @@ impl From for Verification { } } +impl From for Verification { + fn from(qr: QrVerification) -> Self { + Self::QrV1(qr) + } +} + /// The verification state indicating that the verification finished /// successfully. /// @@ -236,6 +280,10 @@ pub struct IdentitiesBeingVerified { } impl IdentitiesBeingVerified { + async fn can_sign_devices(&self) -> bool { + self.private_identity.can_sign_devices().await + } + fn user_id(&self) -> &UserId { self.private_identity.user_id() } diff --git a/matrix_sdk_crypto/src/verification/qrcode.rs b/matrix_sdk_crypto/src/verification/qrcode.rs new file mode 100644 index 00000000..b9005b5f --- /dev/null +++ b/matrix_sdk_crypto/src/verification/qrcode.rs @@ -0,0 +1,886 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::{Arc, Mutex}; + +use matrix_qrcode::{ + qrcode::QrCode, EncodingError, QrVerificationData, SelfVerificationData, + SelfVerificationNoMasterKey, VerificationData, +}; +use matrix_sdk_common::uuid::Uuid; +use ruma::{ + api::client::r0::keys::upload_signatures::Request as SignatureUploadRequest, + events::{ + key::verification::{ + cancel::CancelCode, + done::{DoneEventContent, DoneToDeviceEventContent}, + start::{ + self, ReciprocateV1Content, StartEventContent, StartMethod, + StartToDeviceEventContent, + }, + Relation, + }, + AnyMessageEventContent, AnyToDeviceEventContent, + }, + identifiers::{DeviceIdBox, DeviceKeyAlgorithm, UserId}, +}; +use thiserror::Error; + +use super::{ + event_enums::{DoneContent, OutgoingContent, OwnedStartContent, StartContent}, + sas::content_to_request, + Cancelled, Done, FlowId, IdentitiesBeingVerified, VerificationResult, +}; +use crate::{ + olm::{PrivateCrossSigningIdentity, ReadOnlyAccount}, + store::CryptoStore, + CryptoStoreError, OutgoingVerificationRequest, ReadOnlyDevice, RoomMessageRequest, + UserIdentities, +}; + +const SECRET_SIZE: usize = 16; + +/// An error for the different failure modes that can happen during the +/// validation of a scanned QR code. +#[derive(Debug, Error)] +pub enum ScanError { + /// An IO error inside the crypto store happened during the validation of + /// the QR code scan. + #[error(transparent)] + Store(#[from] CryptoStoreError), + /// A key mismatch happened during the validation of the QR code scan. + #[error("The keys that are being verified didn't match (expected {expected}, found {found})")] + KeyMismatch { expected: String, found: String }, + /// One of the users that is participating in this verification doesn't have + /// a valid cross signing identity. + #[error("The user {0} is missing a valid cross signing identity")] + MissingCrossSigningIdentity(UserId), + /// The device of the user that is participating in this verification + /// doesn't have a valid device key. + #[error("The user's {0} device {1} is not E2E capable")] + MissingDeviceKeys(UserId, DeviceIdBox), + /// The ID uniquely identifying this verification flow didn't match to the + /// one that has been scanned. + #[error("The unique verification flow id did not match (expected {expected}, found {found})")] + FlowIdMismatch { expected: String, found: String }, +} + +/// An object controlling QR code style key verification flows. +#[derive(Clone)] +pub struct QrVerification { + flow_id: FlowId, + store: Arc, + inner: Arc, + state: Arc>, + identities: IdentitiesBeingVerified, +} + +impl std::fmt::Debug for QrVerification { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("QrVerification") + .field("flow_id", &self.flow_id) + .field("inner", self.inner.as_ref()) + .field("state", &self.state.lock().unwrap()) + .finish() + } +} + +impl QrVerification { + /// Has the QR verification been scanned by the other side. + /// + /// When the verification object is in this state it's required that the + /// user confirms that the other side has scanned the QR code. + pub fn is_scanned(&self) -> bool { + matches!(&*self.state.lock().unwrap(), InnerState::Scanned(_)) + } + + /// Get our own user id. + pub fn user_id(&self) -> &UserId { + self.identities.user_id() + } + + /// Get the user id of the other user that is participating in this + /// verification flow. + pub fn other_user_id(&self) -> &UserId { + self.identities.other_user_id() + } + + /// Has the verification flow completed. + pub fn is_done(&self) -> bool { + matches!(&*self.state.lock().unwrap(), InnerState::Done(_)) + } + + /// Has the verification flow been cancelled. + pub fn is_cancelled(&self) -> bool { + matches!(&*self.state.lock().unwrap(), InnerState::Cancelled(_)) + } + + /// Is this a verification that is veryfying one of our own devices + pub fn is_self_verification(&self) -> bool { + self.identities.is_self_verification() + } + + /// Get the unique ID that identifies this QR code verification flow. + pub fn flow_id(&self) -> &FlowId { + &self.flow_id + } + + /// Generate a QR code object that is representing this verification flow. + /// + /// The `QrCode` can then be rendered as an image or as an unicode string. + /// + /// The [`to_bytes()`](#method.to_bytes) method can be used to instead + /// output the raw bytes that should be encoded as a QR code. + pub fn to_qr_code(&self) -> Result { + self.inner.to_qr_code() + } + + /// Generate a the raw bytes that should be encoded as a QR code is + /// representing this verification flow. + /// + /// The [`to_qr_code()`](#method.to_qr_code) method can be used to instead + /// output a `QrCode` object that can be rendered. + pub fn to_bytes(&self) -> Result, EncodingError> { + self.inner.to_bytes() + } + + /// Cancel the verification flow. + pub fn cancel(&self) -> Option { + self.cancel_with_code(CancelCode::User).map(|c| self.content_to_request(c)) + } + + /// Notify the other side that we have successfully scanned the QR code and + /// that the QR verification flow can start. + /// + /// This will return some `OutgoingContent` if the object is in the correct + /// state to start the verification flow, otherwise `None`. + pub fn reciprocate(&self) -> Option { + match &*self.state.lock().unwrap() { + InnerState::Reciprocated(s) => Some(s.as_content(self.flow_id())), + InnerState::Created(_) + | InnerState::Scanned(_) + | InnerState::Confirmed(_) + | InnerState::Done(_) + | InnerState::Cancelled(_) => None, + } + } + + /// Confirm that the other side has scanned our QR code. + pub fn confirm_scanning(&self) -> Option { + let mut state = self.state.lock().unwrap(); + + match &*state { + InnerState::Scanned(s) => { + let new_state = s.clone().confirm_scanning(); + let content = new_state.as_content(&self.flow_id); + *state = InnerState::Confirmed(new_state); + + Some(self.content_to_request(content)) + } + InnerState::Created(_) + | InnerState::Cancelled(_) + | InnerState::Confirmed(_) + | InnerState::Reciprocated(_) + | InnerState::Done(_) => None, + } + } + + fn content_to_request(&self, content: OutgoingContent) -> OutgoingVerificationRequest { + match content { + OutgoingContent::Room(room_id, content) => { + RoomMessageRequest { room_id, txn_id: Uuid::new_v4(), content }.into() + } + OutgoingContent::ToDevice(c) => content_to_request( + self.identities.other_user_id(), + self.identities.other_device_id().to_owned(), + c, + ) + .into(), + } + } + + fn cancel_with_code(&self, code: CancelCode) -> Option { + let new_state = QrState::::new(code); + let content = new_state.as_content(self.flow_id()); + + let mut state = self.state.lock().unwrap(); + + match &*state { + InnerState::Confirmed(_) + | InnerState::Created(_) + | InnerState::Scanned(_) + | InnerState::Reciprocated(_) + | InnerState::Done(_) => { + *state = InnerState::Cancelled(new_state); + Some(content) + } + InnerState::Cancelled(_) => None, + } + } + + async fn mark_as_done( + &self, + new_state: QrState, + ) -> Result< + (Option, Option), + CryptoStoreError, + > { + let (devices, identities) = new_state.verified_identities(); + + let mut new_state = InnerState::Done(new_state); + + let (content, request) = + match self.identities.mark_as_done(Some(&devices), Some(&identities)).await? { + VerificationResult::Ok => (None, None), + VerificationResult::Cancel(c) => { + let canceled = QrState::::new(c); + let content = canceled.as_content(self.flow_id()); + new_state = InnerState::Cancelled(canceled); + (Some(content), None) + } + VerificationResult::SignatureUpload(s) => (None, Some(s)), + }; + + *self.state.lock().unwrap() = new_state; + + Ok((content.map(|c| self.content_to_request(c)), request)) + } + + pub(crate) async fn receive_done( + &self, + content: &DoneContent<'_>, + ) -> Result< + (Option, Option), + CryptoStoreError, + > { + let state = (*self.state.lock().unwrap()).clone(); + + Ok(match state { + InnerState::Confirmed(s) => { + let (verified_device, verified_identity) = match &*self.inner { + QrVerificationData::Verification(_) => { + (None, self.identities.identity_being_verified.as_ref()) + } + QrVerificationData::SelfVerification(_) => { + (Some(&self.identities.device_being_verified), None) + } + QrVerificationData::SelfVerificationNoMasterKey(_) => { + (None, self.identities.identity_being_verified.as_ref()) + } + }; + + let new_state = s.clone().into_done(content, verified_device, verified_identity); + self.mark_as_done(new_state).await? + } + InnerState::Reciprocated(s) => { + let (verified_device, verified_identity) = match &*self.inner { + QrVerificationData::Verification(_) => { + (None, self.identities.identity_being_verified.as_ref()) + } + QrVerificationData::SelfVerification(_) => { + (None, self.identities.identity_being_verified.as_ref()) + } + QrVerificationData::SelfVerificationNoMasterKey(_) => { + (Some(&self.identities.device_being_verified), None) + } + }; + + let new_state = s.clone().into_done(content, verified_device, verified_identity); + let content = Some(new_state.as_content(self.flow_id())); + let (cancel_content, request) = self.mark_as_done(new_state).await?; + + if cancel_content.is_some() { + (cancel_content, request) + } else { + (content.map(|c| self.content_to_request(c)), request) + } + } + InnerState::Created(_) + | InnerState::Scanned(_) + | InnerState::Done(_) + | InnerState::Cancelled(_) => (None, None), + }) + } + + pub(crate) fn receive_reciprocation( + &self, + content: &StartContent, + ) -> Option { + let mut state = self.state.lock().unwrap(); + + match &*state { + InnerState::Created(s) => match s.clone().receive_reciprocate(content) { + Ok(s) => { + *state = InnerState::Scanned(s); + None + } + Err(s) => { + let content = s.as_content(self.flow_id()); + *state = InnerState::Cancelled(s); + Some(self.content_to_request(content)) + } + }, + InnerState::Confirmed(_) + | InnerState::Scanned(_) + | InnerState::Reciprocated(_) + | InnerState::Done(_) + | InnerState::Cancelled(_) => None, + } + } + + fn generate_secret() -> String { + let mut shared_secret = [0u8; SECRET_SIZE]; + getrandom::getrandom(&mut shared_secret) + .expect("Can't generate randomness for the shared secret"); + crate::utilities::encode(shared_secret) + } + + pub(crate) fn new_self( + store: Arc, + flow_id: FlowId, + own_master_key: String, + other_device_key: String, + identities: IdentitiesBeingVerified, + ) -> Self { + let secret = Self::generate_secret(); + + let inner: QrVerificationData = SelfVerificationData::new( + flow_id.as_str().to_owned(), + own_master_key, + other_device_key, + secret, + ) + .into(); + + Self::new_helper(store, flow_id, inner, identities) + } + + pub(crate) fn new_self_no_master( + account: ReadOnlyAccount, + store: Arc, + flow_id: FlowId, + own_master_key: String, + identities: IdentitiesBeingVerified, + ) -> QrVerification { + let secret = Self::generate_secret(); + + let inner: QrVerificationData = SelfVerificationNoMasterKey::new( + flow_id.as_str().to_owned(), + account.identity_keys().ed25519().to_string(), + own_master_key, + secret, + ) + .into(); + + Self::new_helper(store, flow_id, inner, identities) + } + + pub(crate) fn new_cross( + store: Arc, + flow_id: FlowId, + own_master_key: String, + other_master_key: String, + identities: IdentitiesBeingVerified, + ) -> Self { + let secret = Self::generate_secret(); + + let event_id = if let FlowId::InRoom(_, e) = &flow_id { + e.to_owned() + } else { + panic!("A verification between users is only valid in a room"); + }; + + let inner: QrVerificationData = + VerificationData::new(event_id, own_master_key, other_master_key, secret).into(); + + Self::new_helper(store, flow_id, inner, identities) + } + + pub(crate) async fn from_scan( + store: Arc, + own_account: ReadOnlyAccount, + private_identity: PrivateCrossSigningIdentity, + other_user_id: UserId, + other_device_id: DeviceIdBox, + flow_id: FlowId, + qr_code: QrVerificationData, + ) -> Result { + if flow_id.as_str() != qr_code.flow_id() { + return Err(ScanError::FlowIdMismatch { + expected: flow_id.as_str().to_owned(), + found: qr_code.flow_id().to_owned(), + }); + } + + let own_identity = + store.get_user_identity(own_account.user_id()).await?.ok_or_else(|| { + ScanError::MissingCrossSigningIdentity(own_account.user_id().to_owned()) + })?; + let other_identity = store + .get_user_identity(&other_user_id) + .await? + .ok_or_else(|| ScanError::MissingCrossSigningIdentity(other_user_id.clone()))?; + let other_device = + store.get_device(&other_user_id, &other_device_id).await?.ok_or_else(|| { + ScanError::MissingDeviceKeys(other_user_id.clone(), other_device_id.clone()) + })?; + + let check_master_key = |key, identity: &UserIdentities| { + let master_key = identity.master_key().get_first_key().ok_or_else(|| { + ScanError::MissingCrossSigningIdentity(identity.user_id().clone()) + })?; + + if key != master_key { + Err(ScanError::KeyMismatch { + expected: master_key.to_owned(), + found: qr_code.first_key().to_owned(), + }) + } else { + Ok(()) + } + }; + + let identities = match qr_code { + QrVerificationData::Verification(_) => { + check_master_key(qr_code.first_key(), &other_identity)?; + check_master_key(qr_code.second_key(), &own_identity)?; + + IdentitiesBeingVerified { + private_identity, + store: store.clone(), + device_being_verified: other_device, + identity_being_verified: Some(other_identity), + } + } + QrVerificationData::SelfVerification(_) => { + check_master_key(qr_code.first_key(), &other_identity)?; + if qr_code.second_key() != own_account.identity_keys().ed25519() { + return Err(ScanError::KeyMismatch { + expected: own_account.identity_keys().ed25519().to_owned(), + found: qr_code.second_key().to_owned(), + }); + } + + IdentitiesBeingVerified { + private_identity, + store: store.clone(), + device_being_verified: other_device, + identity_being_verified: Some(other_identity), + } + } + QrVerificationData::SelfVerificationNoMasterKey(_) => { + let device_key = + other_device.get_key(DeviceKeyAlgorithm::Ed25519).ok_or_else(|| { + ScanError::MissingDeviceKeys(other_user_id.clone(), other_device_id.clone()) + })?; + if qr_code.first_key() != device_key { + return Err(ScanError::KeyMismatch { + expected: device_key.to_owned(), + found: qr_code.first_key().to_owned(), + }); + } + check_master_key(qr_code.second_key(), &other_identity)?; + IdentitiesBeingVerified { + private_identity, + store: store.clone(), + device_being_verified: other_device, + identity_being_verified: None, + } + } + }; + + let secret = qr_code.secret().to_owned(); + + Ok(Self { + store, + flow_id, + inner: qr_code.into(), + state: Mutex::new(InnerState::Reciprocated(QrState { + state: Reciprocated { secret, own_device_id: own_account.device_id().to_owned() }, + })) + .into(), + identities, + }) + } + + fn new_helper( + store: Arc, + flow_id: FlowId, + inner: QrVerificationData, + identities: IdentitiesBeingVerified, + ) -> Self { + let secret = inner.secret().to_owned(); + + Self { + store, + flow_id, + inner: inner.into(), + state: Mutex::new(InnerState::Created(QrState { state: Created { secret } })).into(), + identities, + } + } +} + +#[derive(Debug, Clone)] +enum InnerState { + Created(QrState), + Scanned(QrState), + Confirmed(QrState), + Reciprocated(QrState), + Done(QrState), + Cancelled(QrState), +} + +#[derive(Clone, Debug)] +struct QrState { + state: S, +} + +#[derive(Clone, Debug)] +struct Created { + secret: String, +} + +#[derive(Clone, Debug)] +struct Scanned {} + +#[derive(Clone, Debug)] +struct Confirmed {} + +#[derive(Clone, Debug)] +struct Reciprocated { + own_device_id: DeviceIdBox, + secret: String, +} + +impl Reciprocated { + fn as_content(&self, flow_id: &FlowId) -> OutgoingContent { + let content = ReciprocateV1Content::new(self.secret.clone()); + let method = StartMethod::ReciprocateV1(content); + + let content: OwnedStartContent = match flow_id { + FlowId::ToDevice(t) => { + StartToDeviceEventContent::new(self.own_device_id.clone(), t.clone(), method).into() + } + FlowId::InRoom(r, e) => ( + r.clone(), + StartEventContent::new( + self.own_device_id.clone(), + method, + Relation::new(e.clone()), + ), + ) + .into(), + }; + + content.into() + } +} + +impl QrState { + fn confirm_scanning(self) -> QrState { + QrState { state: Confirmed {} } + } +} + +impl QrState { + fn new(cancel_code: CancelCode) -> Self { + QrState { state: Cancelled::new(cancel_code) } + } + + fn as_content(&self, flow_id: &FlowId) -> OutgoingContent { + self.state.as_content(flow_id) + } +} + +impl QrState { + fn receive_reciprocate( + self, + content: &StartContent, + ) -> Result, QrState> { + match content.method() { + start::StartMethod::ReciprocateV1(m) => { + // TODO use constant time eq here. + if self.state.secret == m.secret { + Ok(QrState { state: Scanned {} }) + } else { + Err(QrState::::new(CancelCode::KeyMismatch)) + } + } + _ => Err(QrState::::new(CancelCode::UnknownMethod)), + } + } +} + +impl QrState { + fn as_content(&self, flow_id: &FlowId) -> OutgoingContent { + self.state.as_content(flow_id) + } + + fn verified_identities(&self) -> (Arc<[ReadOnlyDevice]>, Arc<[UserIdentities]>) { + (self.state.verified_devices.clone(), self.state.verified_master_keys.clone()) + } +} + +impl QrState { + fn into_done( + self, + _: &DoneContent, + verified_device: Option<&ReadOnlyDevice>, + verified_identity: Option<&UserIdentities>, + ) -> QrState { + let devices: Vec<_> = verified_device.into_iter().cloned().collect(); + let identities: Vec<_> = verified_identity.into_iter().cloned().collect(); + + QrState { + state: Done { + verified_devices: devices.into(), + verified_master_keys: identities.into(), + }, + } + } + + fn as_content(&self, flow_id: &FlowId) -> OutgoingContent { + match flow_id { + FlowId::ToDevice(t) => AnyToDeviceEventContent::KeyVerificationDone( + DoneToDeviceEventContent::new(t.to_owned()), + ) + .into(), + FlowId::InRoom(r, e) => ( + r.to_owned(), + AnyMessageEventContent::KeyVerificationDone(DoneEventContent::new(Relation::new( + e.to_owned(), + ))), + ) + .into(), + } + } +} + +impl QrState { + fn as_content(&self, flow_id: &FlowId) -> OutgoingContent { + self.state.as_content(flow_id) + } + + fn into_done( + self, + _: &DoneContent, + verified_device: Option<&ReadOnlyDevice>, + verified_identity: Option<&UserIdentities>, + ) -> QrState { + let devices: Vec<_> = verified_device.into_iter().cloned().collect(); + let identities: Vec<_> = verified_identity.into_iter().cloned().collect(); + + QrState { + state: Done { + verified_devices: devices.into(), + verified_master_keys: identities.into(), + }, + } + } +} + +#[cfg(test)] +mod test { + use std::{convert::TryFrom, sync::Arc}; + + use matrix_qrcode::QrVerificationData; + use matrix_sdk_test::async_test; + use ruma::identifiers::{event_id, room_id, user_id, DeviceIdBox, UserId}; + + use crate::{ + olm::{PrivateCrossSigningIdentity, ReadOnlyAccount}, + store::{Changes, CryptoStore, MemoryStore}, + verification::{ + event_enums::{DoneContent, OutgoingContent, StartContent}, + FlowId, IdentitiesBeingVerified, + }, + QrVerification, ReadOnlyDevice, + }; + + fn user_id() -> UserId { + user_id!("@example:localhost") + } + + fn memory_store() -> Arc { + Arc::new(MemoryStore::new()) + } + + fn device_id() -> DeviceIdBox { + "DEVICEID".into() + } + + #[async_test] + async fn test_verification_creation() { + let store = memory_store(); + + let account = ReadOnlyAccount::new(&user_id(), &device_id()); + let private_identity = PrivateCrossSigningIdentity::new(user_id()).await; + let flow_id = FlowId::ToDevice("test_transaction".to_owned()); + + let device_key = account.identity_keys().ed25519().to_owned(); + let master_key = private_identity.master_public_key().await.unwrap(); + let master_key = master_key.get_first_key().unwrap().to_owned(); + + let alice_device = ReadOnlyDevice::from_account(&account).await; + + let identities = IdentitiesBeingVerified { + private_identity, + store: store.clone(), + device_being_verified: alice_device, + identity_being_verified: None, + }; + + let verification = QrVerification::new_self_no_master( + account, + store.clone(), + flow_id.clone(), + master_key.clone(), + identities.clone(), + ); + + assert_eq!(verification.inner.first_key(), &device_key); + assert_eq!(verification.inner.second_key(), &master_key); + + let verification = QrVerification::new_self( + store.clone(), + flow_id, + master_key.clone(), + device_key.clone(), + identities.clone(), + ); + + assert_eq!(verification.inner.first_key(), &master_key); + assert_eq!(verification.inner.second_key(), &device_key); + + let bob_identity = PrivateCrossSigningIdentity::new(user_id!("@bob:example")).await; + let bob_master_key = bob_identity.master_public_key().await.unwrap(); + let bob_master_key = bob_master_key.get_first_key().unwrap().to_owned(); + + let flow_id = FlowId::InRoom(room_id!("!test:example"), event_id!("$EVENTID")); + + let verification = QrVerification::new_cross( + store.clone(), + flow_id, + master_key.clone(), + bob_master_key.clone(), + identities, + ); + + assert_eq!(verification.inner.first_key(), &master_key); + assert_eq!(verification.inner.second_key(), &bob_master_key); + } + + #[async_test] + async fn test_reciprocate_receival() { + let test = |flow_id: FlowId| async move { + let alice_account = ReadOnlyAccount::new(&user_id(), &device_id()); + let store = memory_store(); + + let bob_account = ReadOnlyAccount::new(alice_account.user_id(), "BOBDEVICE".into()); + + let private_identity = PrivateCrossSigningIdentity::new(user_id()).await; + let identity = private_identity.to_public_identity().await.unwrap(); + + let master_key = private_identity.master_public_key().await.unwrap(); + let master_key = master_key.get_first_key().unwrap().to_owned(); + + let alice_device = ReadOnlyDevice::from_account(&alice_account).await; + let bob_device = ReadOnlyDevice::from_account(&bob_account).await; + + let mut changes = Changes::default(); + changes.identities.new.push(identity.clone().into()); + changes.devices.new.push(bob_device.clone()); + store.save_changes(changes).await.unwrap(); + + let identities = IdentitiesBeingVerified { + private_identity: PrivateCrossSigningIdentity::empty( + alice_account.user_id().to_owned(), + ), + store: store.clone(), + device_being_verified: alice_device.clone(), + identity_being_verified: Some(identity.clone().into()), + }; + + let alice_verification = QrVerification::new_self_no_master( + alice_account.clone(), + store, + flow_id.clone(), + master_key.clone(), + identities, + ); + + let bob_store = memory_store(); + + let mut changes = Changes::default(); + changes.identities.new.push(identity.into()); + changes.devices.new.push(alice_device.clone()); + bob_store.save_changes(changes).await.unwrap(); + + let qr_code = alice_verification.to_bytes().unwrap(); + let qr_code = QrVerificationData::from_bytes(qr_code).unwrap(); + + let bob_verification = QrVerification::from_scan( + bob_store, + bob_account, + private_identity, + alice_account.user_id().to_owned(), + alice_account.device_id().to_owned(), + flow_id, + qr_code, + ) + .await + .unwrap(); + + let content = bob_verification.reciprocate().unwrap(); + let content = StartContent::try_from(&content).unwrap(); + + alice_verification.receive_reciprocation(&content); + + let request = alice_verification.confirm_scanning().unwrap(); + let content = OutgoingContent::from(request); + let content = DoneContent::try_from(&content).unwrap(); + + assert!(!alice_verification.is_done()); + assert!(!bob_verification.is_done()); + + let (request, _) = bob_verification.receive_done(&content).await.unwrap(); + let content = OutgoingContent::from(request.unwrap()); + let content = DoneContent::try_from(&content).unwrap(); + alice_verification.receive_done(&content).await.unwrap(); + + assert!(alice_verification.is_done()); + assert!(bob_verification.is_done()); + + let identity = alice_verification + .store + .get_user_identity(alice_account.user_id()) + .await + .unwrap() + .unwrap(); + + let identity = identity.own().unwrap(); + + assert!(!bob_device.is_trusted()); + assert!(alice_device.is_trusted()); + assert!(identity.is_verified()); + }; + + let flow_id = FlowId::ToDevice("test_transaction".to_owned()); + test(flow_id).await; + + let flow_id = FlowId::InRoom(room_id!("!test:example"), event_id!("$EVENTID")); + test(flow_id).await; + } +} diff --git a/matrix_sdk_crypto/src/verification/requests.rs b/matrix_sdk_crypto/src/verification/requests.rs index 8e25cf3b..758b1d90 100644 --- a/matrix_sdk_crypto/src/verification/requests.rs +++ b/matrix_sdk_crypto/src/verification/requests.rs @@ -16,6 +16,7 @@ use std::sync::{Arc, Mutex}; +use matrix_qrcode::QrVerificationData; use matrix_sdk_common::uuid::Uuid; use ruma::{ api::client::r0::to_device::DeviceIdOrAllDevices, @@ -30,17 +31,18 @@ use ruma::{ room::message::KeyVerificationRequestEventContent, AnyMessageEventContent, AnyToDeviceEventContent, }, - DeviceId, DeviceIdBox, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, + DeviceId, DeviceIdBox, DeviceKeyAlgorithm, EventId, MilliSecondsSinceUnixEpoch, RoomId, UserId, }; -use tracing::{info, warn}; +use tracing::{info, trace, warn}; use super::{ cache::VerificationCache, event_enums::{ CancelContent, DoneContent, OutgoingContent, ReadyContent, RequestContent, StartContent, }, + qrcode::{QrVerification, ScanError}, sas::content_to_request, - Cancelled, FlowId, + Cancelled, FlowId, IdentitiesBeingVerified, }; use crate::{ olm::{PrivateCrossSigningIdentity, ReadOnlyAccount}, @@ -49,10 +51,20 @@ use crate::{ ToDeviceRequest, UserIdentities, }; -const SUPPORTED_METHODS: &[VerificationMethod] = &[VerificationMethod::MSasV1]; +const SUPPORTED_METHODS: &[VerificationMethod] = &[ + VerificationMethod::MSasV1, + VerificationMethod::MQrCodeShowV1, + VerificationMethod::MReciprocateV1, +]; +/// An object controlling key verification requests. +/// +/// Interactive verification flows usually start with a verification request, +/// this object lets you send and reply to such a verification request. +/// +/// After the initial handshake the verification flow transitions into one of +/// the verification methods. #[derive(Clone, Debug)] -/// TODO pub struct VerificationRequest { verification_cache: VerificationCache, account: ReadOnlyAccount, @@ -62,7 +74,6 @@ pub struct VerificationRequest { } impl VerificationRequest { - /// TODO pub(crate) fn new( cache: VerificationCache, account: ReadOnlyAccount, @@ -93,7 +104,6 @@ impl VerificationRequest { } } - /// TODO pub(crate) fn new_to_device( cache: VerificationCache, account: ReadOnlyAccount, @@ -122,7 +132,10 @@ impl VerificationRequest { } } - /// TODO + /// Create an event content that can be sent as a to-device event to request + /// verification from the other side. This should be used only for + /// self-verifications and it should be sent to the specific device that we + /// want to verify. pub fn request_to_device(&self) -> RequestToDeviceEventContent { RequestToDeviceEventContent::new( self.account.device_id().into(), @@ -132,7 +145,10 @@ impl VerificationRequest { ) } - /// TODO + /// Create an event content that can be sent as a room event to request + /// verification from the other side. This should be used only for + /// verifications of other users and it should be sent to a room we consider + /// to be a DM with the other user. pub fn request( own_user_id: &UserId, own_device_id: &DeviceId, @@ -157,11 +173,21 @@ impl VerificationRequest { &self.other_user_id } + /// Is the verification request ready to start a verification flow. + pub fn is_ready(&self) -> bool { + matches!(&*self.inner.lock().unwrap(), InnerRequest::Ready(_)) + } + /// Get the unique ID of this verification request pub fn flow_id(&self) -> &FlowId { &self.flow_id } + /// Is this a verification that is veryfying one of our own devices + pub fn is_self_verification(&self) -> bool { + self.account.user_id() == self.other_user() + } + /// Has the verification flow that was started with this request finished. pub fn is_done(&self) -> bool { matches!(&*self.inner.lock().unwrap(), InnerRequest::Done(_)) @@ -173,6 +199,43 @@ impl VerificationRequest { matches!(&*self.inner.lock().unwrap(), InnerRequest::Cancelled(_)) } + /// Generate a QR code that can be used by another client to start a QR code + /// based verification. + pub async fn generate_qr_code(&self) -> Result, CryptoStoreError> { + self.inner.lock().unwrap().generate_qr_code().await + } + + /// Start a QR code verification by providing a scanned QR code for this + /// verification flow. + /// + /// Returns a `ScanError` if the QR code isn't valid, `None` if the + /// verification request isn't in the ready state or we don't support QR + /// code verification, otherwise a newly created `QrVerification` object + /// which will be used for the remainder of the verification flow. + pub async fn scan_qr_code( + &self, + data: QrVerificationData, + ) -> Result, ScanError> { + let state = self.inner.lock().unwrap(); + + if let InnerRequest::Ready(r) = &*state { + Ok(Some( + QrVerification::from_scan( + r.store.clone(), + r.account.clone(), + r.private_cross_signing_identity.clone(), + r.other_user_id.clone(), + r.state.other_device_id.clone(), + r.state.flow_id.clone(), + data, + ) + .await?, + )) + } else { + Ok(None) + } + } + pub(crate) fn from_request( cache: VerificationCache, account: ReadOnlyAccount, @@ -258,11 +321,6 @@ impl VerificationRequest { } } - /// Is the verification request ready to start a verification flow. - pub fn is_ready(&self) -> bool { - matches!(&*self.inner.lock().unwrap(), InnerRequest::Ready(_)) - } - pub(crate) fn start( &self, device: ReadOnlyDevice, @@ -357,6 +415,17 @@ impl InnerRequest { }) } + async fn generate_qr_code(&self) -> Result, CryptoStoreError> { + match self { + InnerRequest::Created(_) => Ok(None), + InnerRequest::Requested(_) => Ok(None), + InnerRequest::Ready(s) => s.generate_qr_code().await, + InnerRequest::Passive(_) => Ok(None), + InnerRequest::Done(_) => Ok(None), + InnerRequest::Cancelled(_) => Ok(None), + } + } + fn to_started_sas( &self, content: &StartContent, @@ -570,6 +639,86 @@ impl RequestState { ) } + async fn generate_qr_code(&self) -> Result, CryptoStoreError> { + // TODO return an error explaining why we can't generate a QR code? + let device = if let Some(device) = + self.store.get_device(&self.other_user_id, &self.state.other_device_id).await? + { + device + } else { + warn!( + user_id = self.other_user_id.as_str(), + device_id = self.state.other_device_id.as_str(), + "Can't create a QR code, the device that accepted the \ + verification doesn't exist" + ); + return Ok(None); + }; + + let identites = IdentitiesBeingVerified { + private_identity: self.private_cross_signing_identity.clone(), + store: self.store.clone(), + device_being_verified: device, + identity_being_verified: self.store.get_user_identity(&self.other_user_id).await?, + }; + + let verification = if let Some(identity) = &identites.identity_being_verified { + match &identity { + UserIdentities::Own(i) => { + if identites.can_sign_devices().await { + Some(QrVerification::new_self( + self.store.clone(), + self.flow_id.as_ref().to_owned(), + i.master_key().get_first_key().unwrap().to_owned(), + identites + .other_device() + .get_key(DeviceKeyAlgorithm::Ed25519) + .unwrap() + .to_owned(), + identites, + )) + } else { + Some(QrVerification::new_self_no_master( + self.account.clone(), + self.store.clone(), + self.flow_id.as_ref().to_owned(), + i.master_key().get_first_key().unwrap().to_owned(), + identites, + )) + } + } + UserIdentities::Other(i) => Some(QrVerification::new_cross( + self.store.clone(), + self.flow_id.as_ref().to_owned(), + self.private_cross_signing_identity + .master_public_key() + .await + .unwrap() + .get_first_key() + .unwrap() + .to_owned(), + i.master_key().get_first_key().unwrap().to_owned(), + identites, + )), + } + } else { + warn!( + user_id = self.other_user_id.as_str(), + device_id = self.state.other_device_id.as_str(), + "Can't create a QR code, the user doesn't have a valid cross \ + signing identity." + ); + + None + }; + + if let Some(verification) = &verification { + self.verification_cache.insert_qr(verification.clone()); + } + + Ok(verification) + } + async fn receive_start( &self, sender: &UserId, @@ -597,6 +746,10 @@ impl RequestState { match content.method() { StartMethod::SasV1(_) => match self.to_started_sas(content, device.clone(), identity) { + // TODO check if there is already a SAS verification, i.e. we + // already started one before the other side tried to do the + // same; ignore it if we did and we're the lexicographically + // smaller user ID, otherwise auto-accept the newly started one. Ok(s) => { info!("Started a new SAS verification."); self.verification_cache.insert_sas(s); @@ -615,6 +768,21 @@ impl RequestState { ) } }, + StartMethod::ReciprocateV1(_) => { + if let Some(qr_verification) = + self.verification_cache.get_qr(sender, content.flow_id()) + { + if let Some(request) = qr_verification.receive_reciprocation(content) { + self.verification_cache.add_request(request.into()) + } + trace!( + sender = device.user_id().as_str(), + device_id = device.device_id().as_str(), + verification =? qr_verification, + "Received a QR code reciprocation" + ) + } + } m => { warn!(method =? m, "Received a key verification start event with an unsupported method") }