mod cache;
mod event_enums;
mod machine;
#[cfg(feature = "qrcode")]
mod qrcode;
mod requests;
mod sas;
use std::{collections::HashMap, ops::Deref, sync::Arc};
use as_variant::as_variant;
use event_enums::OutgoingContent;
pub use machine::VerificationMachine;
#[cfg(feature = "qrcode")]
pub use qrcode::{QrVerification, QrVerificationState, ScanError};
pub use requests::{VerificationRequest, VerificationRequestState};
#[cfg(feature = "qrcode")]
use ruma::events::key::verification::done::{
KeyVerificationDoneEventContent, ToDeviceKeyVerificationDoneEventContent,
};
use ruma::{
api::client::keys::upload_signatures::v3::Request as SignatureUploadRequest,
events::{
key::verification::cancel::{
CancelCode, KeyVerificationCancelEventContent,
ToDeviceKeyVerificationCancelEventContent,
},
relation::Reference,
AnyMessageLikeEventContent, AnyToDeviceEventContent,
},
DeviceId, EventId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, RoomId,
UserId,
};
pub use sas::{AcceptSettings, AcceptedProtocols, EmojiShortAuthString, Sas, SasState};
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn};
use crate::{
error::SignatureError,
gossiping::{GossipMachine, GossipRequest},
olm::{PrivateCrossSigningIdentity, StaticAccountData},
store::{Changes, CryptoStoreWrapper},
types::{requests::OutgoingVerificationRequest, Signatures},
CryptoStoreError, DeviceData, LocalTrust, OwnUserIdentityData, UserIdentityData,
};
#[derive(Clone, Debug)]
pub(crate) struct VerificationStore {
pub account: StaticAccountData,
pub private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
inner: Arc<CryptoStoreWrapper>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)]
pub struct Emoji {
pub symbol: &'static str,
pub description: &'static str,
}
pub fn format_emojis(emojis: [Emoji; 7]) -> String {
let (emojis, descriptions): (Vec<_>, Vec<_>) =
emojis.iter().map(|e| (e.symbol, e.description)).unzip();
let center_emoji = |emoji: &str| -> String {
const EMOJI_WIDTH: usize = 2;
const VARIATION_SELECTOR_EMOJIS: [&str; 7] = ["☁️", "❤️", "☂️", "✏️", "✂️", "☎️", "✈️"];
let emoji = if VARIATION_SELECTOR_EMOJIS.contains(&emoji) {
format!("{emoji} ")
} else {
emoji.to_owned()
};
let placeholder = ".".repeat(EMOJI_WIDTH);
format!("{placeholder:^12}").replace(&placeholder, &emoji)
};
let emoji_string = emojis.iter().map(|e| center_emoji(e)).collect::<Vec<_>>().join("");
let description = descriptions.iter().map(|d| format!("{d:^12}")).collect::<Vec<_>>().join("");
format!("{emoji_string}\n{description}")
}
impl VerificationStore {
pub async fn get_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
) -> Result<Option<DeviceData>, CryptoStoreError> {
Ok(self.inner.get_device(user_id, device_id).await?.filter(|d| {
!(d.user_id() == self.account.user_id && d.device_id() == self.account.device_id)
}))
}
pub async fn get_user_identity(
&self,
user_id: &UserId,
) -> Result<Option<UserIdentityData>, CryptoStoreError> {
self.inner.get_user_identity(user_id).await
}
pub async fn get_identities(
&self,
device_being_verified: DeviceData,
) -> Result<IdentitiesBeingVerified, CryptoStoreError> {
let identity_being_verified =
self.get_user_identity(device_being_verified.user_id()).await?;
Ok(IdentitiesBeingVerified {
private_identity: self.private_identity.lock().await.clone(),
store: self.clone(),
device_being_verified,
own_identity: self
.get_user_identity(&self.account.user_id)
.await?
.and_then(|i| i.into_own()),
identity_being_verified,
})
}
pub async fn save_changes(&self, changes: Changes) -> Result<(), CryptoStoreError> {
self.inner.save_changes(changes).await
}
pub async fn get_user_devices(
&self,
user_id: &UserId,
) -> Result<HashMap<OwnedDeviceId, DeviceData>, CryptoStoreError> {
self.inner.get_user_devices(user_id).await
}
pub async fn device_signatures(&self) -> Result<Option<Signatures>, CryptoStoreError> {
Ok(self
.inner
.get_device(&self.account.user_id, &self.account.device_id)
.await?
.map(|d| d.signatures().to_owned()))
}
pub fn inner(&self) -> &CryptoStoreWrapper {
self.inner.deref()
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum Verification {
SasV1(Sas),
#[cfg(feature = "qrcode")]
QrV1(QrVerification),
}
impl Verification {
pub fn sas_v1(self) -> Option<Sas> {
as_variant!(self, Verification::SasV1)
}
#[cfg(feature = "qrcode")]
pub fn qr_v1(self) -> Option<QrVerification> {
as_variant!(self, Verification::QrV1)
}
pub fn is_done(&self) -> bool {
match self {
Verification::SasV1(s) => s.is_done(),
#[cfg(feature = "qrcode")]
Verification::QrV1(qr) => qr.is_done(),
}
}
pub fn flow_id(&self) -> &str {
match self {
Verification::SasV1(s) => s.flow_id().as_str(),
#[cfg(feature = "qrcode")]
Verification::QrV1(qr) => qr.flow_id().as_str(),
}
}
pub fn is_cancelled(&self) -> bool {
match self {
Verification::SasV1(s) => s.is_cancelled(),
#[cfg(feature = "qrcode")]
Verification::QrV1(qr) => qr.is_cancelled(),
}
}
pub fn user_id(&self) -> &UserId {
match self {
Verification::SasV1(v) => v.user_id(),
#[cfg(feature = "qrcode")]
Verification::QrV1(v) => v.user_id(),
}
}
pub fn other_user(&self) -> &UserId {
match self {
Verification::SasV1(s) => s.other_user_id(),
#[cfg(feature = "qrcode")]
Verification::QrV1(qr) => qr.other_user_id(),
}
}
pub fn is_self_verification(&self) -> bool {
match self {
Verification::SasV1(v) => v.is_self_verification(),
#[cfg(feature = "qrcode")]
Verification::QrV1(v) => v.is_self_verification(),
}
}
fn cancel(&self) -> Option<OutgoingVerificationRequest> {
match self {
Verification::SasV1(v) => v.cancel(),
#[cfg(feature = "qrcode")]
Verification::QrV1(v) => v.cancel(),
}
}
}
impl From<Sas> for Verification {
fn from(sas: Sas) -> Self {
Self::SasV1(sas)
}
}
#[cfg(feature = "qrcode")]
impl From<QrVerification> for Verification {
fn from(qr: QrVerification) -> Self {
Self::QrV1(qr)
}
}
#[cfg(feature = "qrcode")]
#[derive(Clone, Debug)]
pub struct Done {
verified_devices: Arc<[DeviceData]>,
verified_master_keys: Arc<[UserIdentityData]>,
}
#[cfg(feature = "qrcode")]
impl Done {
pub fn as_content(&self, flow_id: &FlowId) -> OutgoingContent {
match flow_id {
FlowId::ToDevice(t) => AnyToDeviceEventContent::KeyVerificationDone(
ToDeviceKeyVerificationDoneEventContent::new(t.to_owned()),
)
.into(),
FlowId::InRoom(r, e) => (
r.to_owned(),
AnyMessageLikeEventContent::KeyVerificationDone(
KeyVerificationDoneEventContent::new(Reference::new(e.to_owned())),
),
)
.into(),
}
}
}
#[derive(Clone, Debug)]
pub struct CancelInfo {
cancelled_by_us: bool,
cancel_code: CancelCode,
reason: &'static str,
}
impl CancelInfo {
pub fn reason(&self) -> &'static str {
self.reason
}
pub fn cancel_code(&self) -> &CancelCode {
&self.cancel_code
}
pub fn cancelled_by_us(&self) -> bool {
self.cancelled_by_us
}
}
impl From<Cancelled> for CancelInfo {
fn from(c: Cancelled) -> Self {
Self { cancelled_by_us: c.cancelled_by_us, cancel_code: c.cancel_code, reason: c.reason }
}
}
#[derive(Clone, Debug)]
pub struct Cancelled {
cancelled_by_us: bool,
cancel_code: CancelCode,
reason: &'static str,
}
impl Cancelled {
fn new(cancelled_by_us: bool, code: CancelCode) -> Self {
let reason = match code {
CancelCode::Accepted => {
"A m.key.verification.request was accepted by a different device."
}
CancelCode::InvalidMessage => "The received message was invalid.",
CancelCode::KeyMismatch => "The expected key did not match the verified one",
CancelCode::MismatchedCommitment => "The hash commitment did not match.",
CancelCode::MismatchedSas => "The SAS did not match.",
CancelCode::Timeout => "The verification process timed out.",
CancelCode::UnexpectedMessage => "The device received an unexpected message.",
CancelCode::UnknownMethod => {
"The device does not know how to handle the requested method."
}
CancelCode::UnknownTransaction => {
"The device does not know about the given transaction ID."
}
CancelCode::User => "The user cancelled the verification.",
CancelCode::UserMismatch => "The expected user did not match the verified user",
_ => "Unknown cancel reason",
};
Self { cancelled_by_us, cancel_code: code, reason }
}
pub fn as_content(&self, flow_id: &FlowId) -> OutgoingContent {
match flow_id {
FlowId::ToDevice(s) => AnyToDeviceEventContent::KeyVerificationCancel(
ToDeviceKeyVerificationCancelEventContent::new(
s.clone(),
self.reason.to_owned(),
self.cancel_code.clone(),
),
)
.into(),
FlowId::InRoom(r, e) => (
r.clone(),
AnyMessageLikeEventContent::KeyVerificationCancel(
KeyVerificationCancelEventContent::new(
self.reason.to_owned(),
self.cancel_code.clone(),
Reference::new(e.clone()),
),
),
)
.into(),
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd)]
pub enum FlowId {
ToDevice(OwnedTransactionId),
InRoom(OwnedRoomId, OwnedEventId),
}
impl FlowId {
pub fn room_id(&self) -> Option<&RoomId> {
as_variant!(self, Self::InRoom(room_id, _) => room_id)
}
pub fn as_str(&self) -> &str {
match self {
Self::InRoom(_, event_id) => event_id.as_str(),
Self::ToDevice(transaction_id) => transaction_id.as_str(),
}
}
}
impl From<OwnedTransactionId> for FlowId {
fn from(transaction_id: OwnedTransactionId) -> Self {
Self::ToDevice(transaction_id)
}
}
impl From<(OwnedRoomId, OwnedEventId)> for FlowId {
fn from(ids: (OwnedRoomId, OwnedEventId)) -> Self {
Self::InRoom(ids.0, ids.1)
}
}
impl From<(&RoomId, &EventId)> for FlowId {
fn from(ids: (&RoomId, &EventId)) -> Self {
Self::InRoom(ids.0.to_owned(), ids.1.to_owned())
}
}
#[derive(Clone, Debug)]
pub enum VerificationResult {
Ok,
Cancel(CancelCode),
SignatureUpload(SignatureUploadRequest),
}
#[derive(Clone, Debug)]
pub struct IdentitiesBeingVerified {
private_identity: PrivateCrossSigningIdentity,
store: VerificationStore,
device_being_verified: DeviceData,
own_identity: Option<OwnUserIdentityData>,
identity_being_verified: Option<UserIdentityData>,
}
impl IdentitiesBeingVerified {
#[cfg(feature = "qrcode")]
async fn can_sign_devices(&self) -> bool {
self.private_identity.can_sign_devices().await
}
fn user_id(&self) -> &UserId {
self.private_identity.user_id()
}
fn is_self_verification(&self) -> bool {
self.user_id() == self.other_user_id()
}
fn other_user_id(&self) -> &UserId {
self.device_being_verified.user_id()
}
fn other_device_id(&self) -> &DeviceId {
self.device_being_verified.device_id()
}
fn other_device(&self) -> &DeviceData {
&self.device_being_verified
}
pub async fn mark_as_done(
&self,
verified_devices: Option<&[DeviceData]>,
verified_identities: Option<&[UserIdentityData]>,
) -> Result<VerificationResult, CryptoStoreError> {
let device = self.mark_device_as_verified(verified_devices).await?;
let (identity, should_request_secrets) =
self.mark_identity_as_verified(verified_identities).await?;
if device.is_none() && identity.is_none() {
return Ok(VerificationResult::Cancel(CancelCode::KeyMismatch));
}
let mut changes = Changes::default();
let signature_request = if let Some(device) = device {
let signature_request = if device.user_id() == self.user_id() {
match self.private_identity.sign_device(&device).await {
Ok(r) => Some(r),
Err(SignatureError::MissingSigningKey) => {
warn!(
"Can't sign the device keys for {} {}, \
no private device signing key found",
device.user_id(),
device.device_id(),
);
None
}
Err(e) => {
error!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
"Error signing device keys: {e:?}",
);
None
}
}
} else {
None
};
changes.devices.changed.push(device);
signature_request
} else {
None
};
let identity_signature_request = if let Some(i) = identity {
let request = if let Some(i) = i.other() {
match self.private_identity.sign_user(i).await {
Ok(r) => Some(r),
Err(SignatureError::MissingSigningKey) => {
warn!(
user_id = ?i.user_id(),
"Can't sign the public cross signing keys, \
no private user signing key found",
);
None
}
Err(e) => {
error!(
user_id = ?i.user_id(),
"Error signing the public cross signing keys: {e:?}",
);
None
}
}
} else {
None
};
changes.identities.changed.push(i);
request
} else {
None
};
let merged_request = if let Some(mut r) = signature_request {
if let Some(user_request) = identity_signature_request {
r.signed_keys.extend(user_request.signed_keys);
}
Some(r)
} else {
identity_signature_request
};
if should_request_secrets {
let secret_requests = self.request_missing_secrets().await?;
changes.key_requests = secret_requests;
}
self.store.save_changes(changes).await?;
Ok(merged_request
.map(VerificationResult::SignatureUpload)
.unwrap_or(VerificationResult::Ok))
}
async fn request_missing_secrets(&self) -> Result<Vec<GossipRequest>, CryptoStoreError> {
let mut secrets = self.private_identity.get_missing_secrets().await;
if self.store.inner.load_backup_keys().await?.decryption_key.is_none() {
secrets.push(ruma::events::secret::request::SecretName::RecoveryKey);
}
Ok(GossipMachine::request_missing_secrets(self.user_id(), secrets))
}
async fn mark_identity_as_verified(
&self,
verified_identities: Option<&[UserIdentityData]>,
) -> Result<(Option<UserIdentityData>, bool), CryptoStoreError> {
if self.identity_being_verified.is_none() {
return Ok((None, false));
}
let identity = self.store.get_user_identity(self.other_user_id()).await?;
Ok(if let Some(identity) = identity {
if self
.identity_being_verified
.as_ref()
.is_some_and(|i| i.master_key() == identity.master_key())
{
if verified_identities.is_some_and(|i| {
i.iter().any(|verified| verified.user_id() == identity.user_id())
}) {
info!(
user_id = ?self.other_user_id(),
"The interactive verification process verified the identity of \
the remote user: marking as verified."
);
let should_request_secrets = if let UserIdentityData::Own(i) = &identity {
i.mark_as_verified();
true
} else {
false
};
(Some(identity), should_request_secrets)
} else {
debug!(
user_id = ?self.other_user_id(),
"The interactive verification process didn't verify \
the user identity of the user that participated in \
the interactive verification",
);
(None, false)
}
} else {
warn!(
user_id = ?self.other_user_id(),
"The master keys of the user have changed while an interactive \
verification was going on, not marking the identity as verified.",
);
(None, false)
}
} else {
info!(
user_id = ?self.other_user_id(),
"The identity of the user was deleted while an interactive \
verification was going on.",
);
(None, false)
})
}
async fn mark_device_as_verified(
&self,
verified_devices: Option<&[DeviceData]>,
) -> Result<Option<DeviceData>, CryptoStoreError> {
let device = self.store.get_device(self.other_user_id(), self.other_device_id()).await?;
let Some(device) = device else {
let device = &self.device_being_verified;
info!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
"The device was deleted while an interactive verification was going on.",
);
return Ok(None);
};
if device.keys() != self.device_being_verified.keys() {
warn!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
"The device keys have changed while an interactive verification \
was going on, not marking the device as verified.",
);
return Ok(None);
}
if verified_devices.is_some_and(|v| v.contains(&device)) {
info!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
"The interactive verification process verified the remote device: marking as verified.",
);
device.set_trust_state(LocalTrust::Verified);
Ok(Some(device))
} else {
debug!(
user_id = ?device.user_id(),
device_id = ?device.device_id(),
"The interactive verification process didn't verify the remote device",
);
Ok(None)
}
}
}
#[cfg(test)]
pub(crate) mod tests {
use std::sync::Arc;
use ruma::{
device_id,
events::{AnyToDeviceEventContent, ToDeviceEvent},
user_id, DeviceId, UserId,
};
use tokio::sync::Mutex;
use super::{event_enums::OutgoingContent, VerificationStore};
use crate::{
olm::PrivateCrossSigningIdentity,
store::{Changes, CryptoStore, CryptoStoreWrapper, IdentityChanges, MemoryStore},
types::{
events::ToDeviceEvents,
requests::{AnyOutgoingRequest, OutgoingRequest, OutgoingVerificationRequest},
},
Account, DeviceData, OtherUserIdentityData, OwnUserIdentityData,
};
pub(crate) fn request_to_event(
sender: &UserId,
request: &OutgoingVerificationRequest,
) -> ToDeviceEvents {
let content =
request.to_owned().try_into().expect("Can't fetch content out of the request");
wrap_any_to_device_content(sender, content)
}
pub(crate) fn outgoing_request_to_event(
sender: &UserId,
request: &OutgoingRequest,
) -> ToDeviceEvents {
match request.request() {
AnyOutgoingRequest::ToDeviceRequest(r) => request_to_event(sender, &r.clone().into()),
_ => panic!("Unsupported outgoing request"),
}
}
pub(crate) fn wrap_any_to_device_content(
sender: &UserId,
content: OutgoingContent,
) -> ToDeviceEvents {
let content = if let OutgoingContent::ToDevice(c) = content { c } else { unreachable!() };
let sender = sender.to_owned();
match content {
AnyToDeviceEventContent::KeyVerificationRequest(c) => {
ToDeviceEvents::KeyVerificationRequest(ToDeviceEvent { sender, content: c })
}
AnyToDeviceEventContent::KeyVerificationReady(c) => {
ToDeviceEvents::KeyVerificationReady(ToDeviceEvent { sender, content: c })
}
AnyToDeviceEventContent::KeyVerificationKey(c) => {
ToDeviceEvents::KeyVerificationKey(ToDeviceEvent { sender, content: c })
}
AnyToDeviceEventContent::KeyVerificationStart(c) => {
ToDeviceEvents::KeyVerificationStart(ToDeviceEvent { sender, content: c })
}
AnyToDeviceEventContent::KeyVerificationAccept(c) => {
ToDeviceEvents::KeyVerificationAccept(ToDeviceEvent { sender, content: c })
}
AnyToDeviceEventContent::KeyVerificationMac(c) => {
ToDeviceEvents::KeyVerificationMac(ToDeviceEvent { sender, content: c })
}
AnyToDeviceEventContent::KeyVerificationDone(c) => {
ToDeviceEvents::KeyVerificationDone(ToDeviceEvent { sender, content: c })
}
_ => unreachable!(),
}
}
pub fn alice_id() -> &'static UserId {
user_id!("@alice:example.org")
}
pub fn alice_device_id() -> &'static DeviceId {
device_id!("JLAFKJWSCS")
}
pub fn bob_id() -> &'static UserId {
user_id!("@bob:example.org")
}
pub fn bob_device_id() -> &'static DeviceId {
device_id!("BOBDEVICE")
}
pub(crate) async fn setup_stores() -> (Account, VerificationStore, Account, VerificationStore) {
let alice = Account::with_device_id(alice_id(), alice_device_id());
let alice_store = MemoryStore::new();
let (alice_private_identity, _, _) =
PrivateCrossSigningIdentity::with_account(&alice).await;
let alice_private_identity = Mutex::new(alice_private_identity);
let bob = Account::with_device_id(bob_id(), bob_device_id());
let bob_store = MemoryStore::new();
let (bob_private_identity, _, _) = PrivateCrossSigningIdentity::with_account(&bob).await;
let bob_private_identity = Mutex::new(bob_private_identity);
let alice_public_identity =
OtherUserIdentityData::from_private(&*alice_private_identity.lock().await).await;
let alice_identity_data =
OwnUserIdentityData::from_private(&*alice_private_identity.lock().await).await;
let bob_public_identity =
OtherUserIdentityData::from_private(&*bob_private_identity.lock().await).await;
let bob_identity_data =
OwnUserIdentityData::from_private(&*bob_private_identity.lock().await).await;
let alice_device = DeviceData::from_account(&alice);
let bob_device = DeviceData::from_account(&bob);
let alice_changes = Changes {
identities: IdentityChanges {
new: vec![alice_identity_data.into(), bob_public_identity.into()],
changed: vec![],
unchanged: vec![],
},
..Default::default()
};
alice_store.save_changes(alice_changes).await.unwrap();
alice_store.save_devices(vec![bob_device]);
let bob_changes = Changes {
identities: IdentityChanges {
new: vec![bob_identity_data.into(), alice_public_identity.into()],
changed: vec![],
unchanged: vec![],
},
..Default::default()
};
bob_store.save_changes(bob_changes).await.unwrap();
bob_store.save_devices(vec![alice_device]);
let alice_store = VerificationStore {
inner: Arc::new(CryptoStoreWrapper::new(
alice.user_id(),
alice.device_id(),
alice_store,
)),
account: alice.static_data.clone(),
private_identity: alice_private_identity.into(),
};
let bob_store = VerificationStore {
account: bob.static_data.clone(),
inner: Arc::new(CryptoStoreWrapper::new(bob.user_id(), bob.device_id(), bob_store)),
private_identity: bob_private_identity.into(),
};
(alice, alice_store, bob, bob_store)
}
}