diff --git a/src/dummy.rs b/src/dummy.rs new file mode 100644 index 00000000..8c82d160 --- /dev/null +++ b/src/dummy.rs @@ -0,0 +1,24 @@ +//! Types for the *m.dummy* event. + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +event! { + /// This event type is used to indicate new Olm sessions for end-to-end encryption. + /// + /// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event. + /// + /// The event does not have any content associated with it. The sending client is expected to + /// send a key share request shortly after this message, causing the receiving client to process + /// this *m.dummy* event as the most recent event and using the keyshare request to set up the + /// session. The keyshare request and *m.dummy* combination should result in the original + /// sending client receiving keys over the newly established session. + pub struct DummyEvent(DummyEventContent) {} +} + +/// The payload of an *m.dummy* event. +/// +/// The values in the hash map are not meaningful. They are used to generate an empty JSON +/// object to support the structure used by the Matrix specification. +pub type DummyEventContent = HashMap<(), ()>; diff --git a/src/forwarded_room_key.rs b/src/forwarded_room_key.rs new file mode 100644 index 00000000..ad0416c2 --- /dev/null +++ b/src/forwarded_room_key.rs @@ -0,0 +1,47 @@ +//! Types for the *m.forwarded_room_key* event. + +use ruma_identifiers::RoomId; +use serde::{Deserialize, Serialize}; + +use super::Algorithm; + +event! { + /// This event type is used to forward keys for end-to-end encryption. + /// + /// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event. + pub struct ForwardedRoomKeyEvent(ForwardedRoomKeyEventContent) {} +} + +/// The payload of an *m.forwarded_room_key* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ForwardedRoomKeyEventContent { + /// The encryption algorithm the key in this event is to be used with. + pub algorithm: Algorithm, + + /// The room where the key is used. + pub room_id: RoomId, + + /// The Curve25519 key of the device which initiated the session originally. + pub sender_key: String, + + /// The ID of the session that the key is for. + pub session_id: String, + + /// The key to be exchanged. + pub session_key: String, + + /// The Ed25519 key of the device which initiated the session originally. + /// + /// It is "claimed" because the receiving device has no way to tell that the original room_key + /// actually came from a device which owns the private part of this key unless they have done + /// device verification. + pub sender_claimed_ed25519_key: String, + + /// Chain of Curve25519 keys. + /// + /// It starts out empty, but each time the key is forwarded to another device, the previous + /// sender in the chain is added to the end of the list. For example, if the key is forwarded + /// from A to B to C, this field is empty between A and B, and contains A's Curve25519 key + /// between B and C. + pub forwarding_curve25519_key_chain: Vec, +} diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 00000000..ccc5d32e --- /dev/null +++ b/src/key.rs @@ -0,0 +1,3 @@ +//! Modules for events in the *m.key* namespace. + +pub mod verification; diff --git a/src/key/verification.rs b/src/key/verification.rs new file mode 100644 index 00000000..1ce06164 --- /dev/null +++ b/src/key/verification.rs @@ -0,0 +1,117 @@ +//! Modules for events in the *m.key.verification* namespace. +//! +//! This module also contains types shared by events in its child namespaces. + +use serde::{Deserialize, Serialize}; + +pub mod accept; +pub mod cancel; +pub mod key; +pub mod mac; +pub mod request; +pub mod start; + +/// A hash algorithm. +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Deserialize)] +pub enum HashAlgorithm { + /// The SHA256 hash algorithm. + #[serde(rename = "sha256")] + Sha256, + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + #[serde(skip)] + __Nonexhaustive, +} + +impl_enum! { + HashAlgorithm { + Sha256 => "sha256", + } +} + +/// A key agreement protocol. +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Deserialize)] +pub enum KeyAgreementProtocol { + /// The [Curve25519](https://cr.yp.to/ecdh.html) key agreement protocol. + #[serde(rename = "curve25519")] + Curve25519, + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + #[serde(skip)] + __Nonexhaustive, +} + +impl_enum! { + KeyAgreementProtocol { + Curve25519 => "curve25519", + } +} + +/// A message authentication code algorithm. +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Deserialize)] +pub enum MessageAuthenticationCode { + /// The HKDF-HMAC-SHA256 MAC. + #[serde(rename = "hkdf-hmac-sha256")] + HkdfHmacSha256, + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + #[serde(skip)] + __Nonexhaustive, +} + +impl_enum! { + MessageAuthenticationCode { + HkdfHmacSha256 => "hkdf-hmac-sha256", + } +} + +/// A Short Authentication String method. +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Deserialize)] +pub enum ShortAuthenticationString { + /// The decimal method. + #[serde(rename = "decimal")] + Decimal, + + /// The emoji method. + #[serde(rename = "emoji")] + Emoji, + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + #[serde(skip)] + __Nonexhaustive, +} + +impl_enum! { + ShortAuthenticationString { + Decimal => "decimal", + Emoji => "emoji", + } +} + +/// A Short Authentication String (SAS) verification method. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum VerificationMethod { + /// The *m.sas.v1* verification method. + #[serde(rename = "m.sas.v1")] + MSasV1, + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + #[serde(skip)] + __Nonexhaustive, +} + +impl_enum! { + VerificationMethod { + MSasV1 => "m.sas.v1", + } +} diff --git a/src/key/verification/accept.rs b/src/key/verification/accept.rs new file mode 100644 index 00000000..74fdd662 --- /dev/null +++ b/src/key/verification/accept.rs @@ -0,0 +1,51 @@ +//! Types for the *m.key.verification.accept* event. + +use serde::{Deserialize, Serialize}; + +use super::{ + HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString, + VerificationMethod, +}; + +event! { + /// Accepts a previously sent *m.key.verification.start* messge. + /// + /// Typically sent as a to-device event. + pub struct AcceptEvent(AcceptEventContent) {} +} + +/// The payload of an *m.key.verification.accept* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct AcceptEventContent { + /// An opaque identifier for the verification process. + /// + /// Must be the same as the one used for the *m.key.verification.start* message. + pub transaction_id: String, + + /// The verification method to use. + /// + /// Must be `m.sas.v1`. + pub method: VerificationMethod, + + /// The key agreement protocol the device is choosing to use, out of the options in the + /// *m.key.verification.start* message. + pub key_agreement_protocol: KeyAgreementProtocol, + + /// The hash method the device is choosing to use, out of the options in the + /// *m.key.verification.start* message. + pub hash: HashAlgorithm, + + /// The message authentication code the device is choosing to use, out of the options in the + /// *m.key.verification.start* message. + pub message_authentication_code: MessageAuthenticationCode, + + /// The SAS methods both devices involved in the verification process understand. + /// + /// Must be a subset of the options in the *m.key.verification.start* message. + pub short_authentication_string: Vec, + + /// The hash (encoded as unpadded base64) of the concatenation of the device's ephemeral public + /// key (encoded as unpadded base64) and the canonical JSON representation of the + /// *m.key.verification.start* message. + pub commitment: String, +} diff --git a/src/key/verification/cancel.rs b/src/key/verification/cancel.rs new file mode 100644 index 00000000..8f5265b6 --- /dev/null +++ b/src/key/verification/cancel.rs @@ -0,0 +1,189 @@ +//! Types for the *m.key.verification.cancel* event. + +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use serde::{ + de::{Error as SerdeError, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +event! { + /// Cancels a key verification process/request. + /// + /// Typically sent as a to-device event. + pub struct CancelEvent(CancelEventContent) {} +} + +/// The payload of an *m.key.verification.cancel* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct CancelEventContent { + /// The opaque identifier for the verification process/request. + pub transaction_id: String, + + /// A human readable description of the `code`. + /// + /// The client should only rely on this string if it does not understand the `code`. + pub reason: String, + + /// The error code for why the process/request was cancelled by the user. + pub code: CancelCode, +} + +/// An error code for why the process/request was cancelled by the user. +/// +/// Custom error codes should use the Java package naming convention. +#[derive(Clone, Debug, PartialEq)] +pub enum CancelCode { + /// The user cancelled the verification. + User, + + /// The verification process timed out. Verification processes can define their own timeout + /// parameters. + Timeout, + + /// The device does not know about the given transaction ID. + UnknownTransaction, + + /// The device does not know how to handle the requested method. + /// + /// This should be sent for *m.key.verification.start* messages and messages defined by + /// individual verification processes. + UnknownMethod, + + /// The device received an unexpected message. + /// + /// Typically raised when one of the parties is handling the verification out of order. + UnexpectedMessage, + + /// The key was not verified. + KeyMismatch, + + /// The expected user did not match the user verified. + UserMismatch, + + /// The message received was invalid. + InvalidMessage, + + /// An *m.key.verification.request* was accepted by a different device. + /// + /// The device receiving this error can ignore the verification request. + Accepted, + + /// Any code that is not part of the specification. + Custom(String), + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + __Nonexhaustive, +} + +impl Display for CancelCode { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let cancel_code_str = match *self { + CancelCode::User => "m.user", + CancelCode::Timeout => "m.timeout", + CancelCode::UnknownTransaction => "m.unknown_transaction", + CancelCode::UnknownMethod => "m.unknown_method", + CancelCode::UnexpectedMessage => "m.unexpected_message", + CancelCode::KeyMismatch => "m.key_mismatch", + CancelCode::UserMismatch => "m.user_mismatch", + CancelCode::InvalidMessage => "m.invalid_message", + CancelCode::Accepted => "m.accepted", + CancelCode::Custom(ref cancel_code) => cancel_code, + CancelCode::__Nonexhaustive => { + panic!("__Nonexhaustive enum variant is not intended for use.") + } + }; + + write!(f, "{}", cancel_code_str) + } +} + +impl<'a> From<&'a str> for CancelCode { + fn from(s: &'a str) -> CancelCode { + match s { + "m.user" => CancelCode::User, + "m.timeout" => CancelCode::Timeout, + "m.unknown_transaction" => CancelCode::UnknownTransaction, + "m.unknown_method" => CancelCode::UnknownMethod, + "m.unexpected_message" => CancelCode::UnexpectedMessage, + "m.key_mismatch" => CancelCode::KeyMismatch, + "m.user_mismatch" => CancelCode::UserMismatch, + "m.invalid_message" => CancelCode::InvalidMessage, + "m.accepted" => CancelCode::Accepted, + cancel_code => CancelCode::Custom(cancel_code.to_string()), + } + } +} + +impl Serialize for CancelCode { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for CancelCode { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CancelCodeVisitor; + + impl<'de> Visitor<'de> for CancelCodeVisitor { + type Value = CancelCode; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "an `m.key.verification.cancel` code as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + Ok(CancelCode::from(v)) + } + } + + deserializer.deserialize_str(CancelCodeVisitor) + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_str, to_string}; + + use super::CancelCode; + + #[test] + fn cancel_codes_serialize_to_display_form() { + assert_eq!(to_string(&CancelCode::User).unwrap(), r#""m.user""#); + } + + #[test] + fn custom_cancel_codes_serialize_to_display_form() { + assert_eq!( + to_string(&CancelCode::Custom("io.ruma.test".to_string())).unwrap(), + r#""io.ruma.test""# + ); + } + + #[test] + fn cancel_codes_deserialize_from_display_form() { + assert_eq!( + from_str::(r#""m.user""#).unwrap(), + CancelCode::User + ); + } + + #[test] + fn custom_cancel_codes_deserialize_from_display_form() { + assert_eq!( + from_str::(r#""io.ruma.test""#).unwrap(), + CancelCode::Custom("io.ruma.test".to_string()) + ) + } +} diff --git a/src/key/verification/key.rs b/src/key/verification/key.rs new file mode 100644 index 00000000..07cb1067 --- /dev/null +++ b/src/key/verification/key.rs @@ -0,0 +1,22 @@ +//! Types for the *m.key.verification.key* event. + +use serde::{Deserialize, Serialize}; + +event! { + /// Sends the ephemeral public key for a device to the partner device. + /// + /// Typically sent as a to-device event. + pub struct KeyEvent(KeyEventContent) {} +} + +/// The payload of an *m.key.verification.key* event. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct KeyEventContent { + /// An opaque identifier for the verification process. + /// + /// Must be the same as the one used for the *m.key.verification.start* message. + pub transaction_id: String, + + /// The device's ephemeral public key, encoded as unpadded Base64. + pub key: String, +} diff --git a/src/key/verification/mac.rs b/src/key/verification/mac.rs new file mode 100644 index 00000000..8a8b0df1 --- /dev/null +++ b/src/key/verification/mac.rs @@ -0,0 +1,29 @@ +//! Types for the *m.key.verification.mac* event. + +use ruma_signatures::SignatureSet; +use serde::{Deserialize, Serialize}; + +event! { + /// Sends the MAC of a device's key to the partner device. + /// + /// Typically sent as a to-device event. + pub struct MacEvent(MacEventContent) {} +} + +/// The payload for an *m.key.verification.mac* event. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct MacEventContent { + /// An opaque identifier for the verification process. + /// + /// Must be the same as the one used for the *m.key.verification.start* message. + pub transaction_id: String, + + /// A map of the key ID to the MAC of the key, using the algorithm in the verification process. + /// + /// The MAC is encoded as unpadded Base64. + pub mac: SignatureSet, + + /// The MAC of the comma-separated, sorted, list of key IDs given in the `mac` property, encoded + /// as unpadded Base64. + pub keys: String, +} diff --git a/src/key/verification/request.rs b/src/key/verification/request.rs new file mode 100644 index 00000000..7d1a3a4f --- /dev/null +++ b/src/key/verification/request.rs @@ -0,0 +1,34 @@ +//! Types for the *m.key.verification.request* event. + +use ruma_identifiers::DeviceId; +use serde::{Deserialize, Serialize}; + +use super::VerificationMethod; + +event! { + /// Requests a key verification with another user's devices. + /// + /// Typically sent as a to-device event. + pub struct RequestEvent(RequestEventContent) {} +} + +/// The payload of an *m.key.verification.request* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RequestEventContent { + /// The device ID which is initiating the request. + pub from_device: DeviceId, + + /// An opaque identifier for the verification request. + /// + /// Must be unique with respect to the devices involved. + pub transaction_id: String, + + /// The verification methods supported by the sender. + pub methods: Vec, + + /// The POSIX timestamp in milliseconds for when the request was made. + /// + /// If the request is in the future by more than 5 minutes or more than 10 minutes in the past, + /// the message should be ignored by the receiver. + pub timestamp: u64, +} diff --git a/src/key/verification/start.rs b/src/key/verification/start.rs new file mode 100644 index 00000000..c6b28b7f --- /dev/null +++ b/src/key/verification/start.rs @@ -0,0 +1,173 @@ +//! Types for the *m.key.verification.start* event. + +use ruma_identifiers::DeviceId; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{from_value, Value}; + +use super::{ + HashAlgorithm, KeyAgreementProtocol, MessageAuthenticationCode, ShortAuthenticationString, + VerificationMethod, +}; + +event! { + /// Begins an SAS key verification process. + /// + /// Typically sent as a to-device event. + pub struct StartEvent(StartEventContent) {} +} + +/// The payload of an *m.key.verification.start* event. +#[derive(Clone, Debug, PartialEq)] +pub enum StartEventContent { + /// The *m.sas.v1* verification method. + MSasV1(MSasV1Content), + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + __Nonexhaustive, +} + +/// The payload of an *m.key.verification.start* event using the *m.sas.v1* method. +#[derive(Clone, Debug, Serialize, PartialEq, Deserialize)] +pub struct MSasV1Content { + /// The device ID which is initiating the process. + pub from_device: DeviceId, + + /// An opaque identifier for the verification process. + /// + /// Must be unique with respect to the devices involved. Must be the same as the + /// `transaction_id` given in the *m.key.verification.request* if this process is originating + /// from a request. + pub transaction_id: String, + + /// The verification method to use. + /// + /// Must be `m.sas.v1`. + pub method: VerificationMethod, + + /// Optional method to use to verify the other user's key with. + #[serde(skip_serializing_if = "Option::is_none")] + pub next_method: Option, + + /// The key agreement protocols the sending device understands. + /// + /// Must include at least `curve25519`. + pub key_agreement_protocols: Vec, + + /// The hash methods the sending device understands. + /// + /// Must include at least `sha256`. + pub hashes: Vec, + + /// The message authentication codes that the sending device understands. + /// + /// Must include at least `hkdf-hmac-sha256`. + pub message_authentication_codes: Vec, + + /// The SAS methods the sending device (and the sending device's user) understands. + /// + /// Must include at least `decimal`. Optionally can include `emoji`. + pub short_authentication_string: Vec, +} + +impl Serialize for StartEventContent { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + StartEventContent::MSasV1(ref content) => content.serialize(serializer), + _ => panic!("Attempted to serialize __Nonexhaustive variant."), + } + } +} + +impl<'de> Deserialize<'de> for StartEventContent { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: Value = Deserialize::deserialize(deserializer)?; + + let method_value = match value.get("method") { + Some(value) => value.clone(), + None => return Err(D::Error::missing_field("method")), + }; + + let method = match from_value::(method_value.clone()) { + Ok(method) => method, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + match method { + VerificationMethod::MSasV1 => { + let content = match from_value::(value) { + Ok(content) => content, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(StartEventContent::MSasV1(content)) + } + VerificationMethod::__Nonexhaustive => Err(D::Error::custom( + "Attempted to deserialize __Nonexhaustive variant.", + )), + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::{from_str, to_string}; + + use super::{ + HashAlgorithm, KeyAgreementProtocol, MSasV1Content, MessageAuthenticationCode, + ShortAuthenticationString, StartEventContent, VerificationMethod, + }; + + #[test] + fn serializtion() { + let key_verification_start_content = StartEventContent::MSasV1(MSasV1Content { + from_device: "123".to_string(), + transaction_id: "456".to_string(), + method: VerificationMethod::MSasV1, + next_method: None, + hashes: vec![HashAlgorithm::Sha256], + key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519], + message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256], + short_authentication_string: vec![ShortAuthenticationString::Decimal], + }); + + assert_eq!( + to_string(&key_verification_start_content).unwrap(), + r#"{"from_device":"123","transaction_id":"456","method":"m.sas.v1","key_agreement_protocols":["curve25519"],"hashes":["sha256"],"message_authentication_codes":["hkdf-hmac-sha256"],"short_authentication_string":["decimal"]}"# + ); + } + + #[test] + fn deserialization() { + let key_verification_start_content = StartEventContent::MSasV1(MSasV1Content { + from_device: "123".to_string(), + transaction_id: "456".to_string(), + method: VerificationMethod::MSasV1, + next_method: None, + hashes: vec![HashAlgorithm::Sha256], + key_agreement_protocols: vec![KeyAgreementProtocol::Curve25519], + message_authentication_codes: vec![MessageAuthenticationCode::HkdfHmacSha256], + short_authentication_string: vec![ShortAuthenticationString::Decimal], + }); + + assert_eq!( + from_str::( + r#"{"from_device":"123","transaction_id":"456","method":"m.sas.v1","hashes":["sha256"],"key_agreement_protocols":["curve25519"],"message_authentication_codes":["hkdf-hmac-sha256"],"short_authentication_string":["decimal"]}"# + ) + .unwrap(), + key_verification_start_content + ); + } + + #[test] + fn deserialization_failure() { + assert!(from_str::(r#"{"from_device":"123"}"#).is_err()); + } +} diff --git a/src/lib.rs b/src/lib.rs index 99bc6880..218e7f0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,11 +118,16 @@ pub mod collections { pub mod only; } pub mod direct; +pub mod dummy; +pub mod forwarded_room_key; pub mod fully_read; pub mod ignored_user_list; +pub mod key; pub mod presence; pub mod receipt; pub mod room; +pub mod room_key; +pub mod room_key_request; pub mod sticker; pub mod stripped; pub mod tag; @@ -411,6 +416,84 @@ impl<'de> Deserialize<'de> for EventType { } } +/// An encryption algorithm to be used to encrypt messages sent to a room. +#[derive(Clone, Debug, PartialEq)] +pub enum Algorithm { + /// Olm version 1 using Curve25519, AES-256, and SHA-256. + OlmV1Curve25519AesSha2, + + /// Megolm version 1 using AES-256 and SHA-256. + MegolmV1AesSha2, + + /// Any algorithm that is not part of the specification. + Custom(String), + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + __Nonexhaustive, +} + +impl Display for Algorithm { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let algorithm_str = match *self { + Algorithm::OlmV1Curve25519AesSha2 => "m.olm.v1.curve25519-aes-sha2", + Algorithm::MegolmV1AesSha2 => "m.megolm.v1.aes-sha2", + Algorithm::Custom(ref algorithm) => algorithm, + Algorithm::__Nonexhaustive => { + panic!("__Nonexhaustive enum variant is not intended for use.") + } + }; + + write!(f, "{}", algorithm_str) + } +} + +impl<'a> From<&'a str> for Algorithm { + fn from(s: &'a str) -> Algorithm { + match s { + "m.olm.v1.curve25519-aes-sha2" => Algorithm::OlmV1Curve25519AesSha2, + "m.megolm.v1.aes-sha2" => Algorithm::MegolmV1AesSha2, + algorithm => Algorithm::Custom(algorithm.to_string()), + } + } +} + +impl Serialize for Algorithm { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for Algorithm { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct CancelCodeVisitor; + + impl<'de> Visitor<'de> for CancelCodeVisitor { + type Value = Algorithm; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "an encryption algorithm code as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + Ok(Algorithm::from(v)) + } + } + + deserializer.deserialize_str(CancelCodeVisitor) + } +} + /// Serde deserialization decorator to map empty Strings to None, /// and forward non-empty Strings to the Deserialize implementation for T. /// Useful for the typical @@ -445,7 +528,7 @@ fn default_true() -> bool { mod tests { use serde_json::{from_str, to_string}; - use super::EventType; + use super::{Algorithm, EventType}; #[test] fn event_types_serialize_to_display_form() { @@ -478,4 +561,36 @@ mod tests { EventType::Custom("io.ruma.test".to_string()) ) } + + #[test] + fn algorithms_serialize_to_display_form() { + assert_eq!( + to_string(&Algorithm::MegolmV1AesSha2).unwrap(), + r#""m.megolm.v1.aes-sha2""# + ); + } + + #[test] + fn custom_algorithms_serialize_to_display_form() { + assert_eq!( + to_string(&Algorithm::Custom("io.ruma.test".to_string())).unwrap(), + r#""io.ruma.test""# + ); + } + + #[test] + fn algorithms_deserialize_from_display_form() { + assert_eq!( + from_str::(r#""m.megolm.v1.aes-sha2""#).unwrap(), + Algorithm::MegolmV1AesSha2 + ); + } + + #[test] + fn custom_algorithms_deserialize_from_display_form() { + assert_eq!( + from_str::(r#""io.ruma.test""#).unwrap(), + Algorithm::Custom("io.ruma.test".to_string()) + ) + } } diff --git a/src/room.rs b/src/room.rs index 45398176..28073a50 100644 --- a/src/room.rs +++ b/src/room.rs @@ -10,6 +10,8 @@ pub mod aliases; pub mod avatar; pub mod canonical_alias; pub mod create; +pub mod encrypted; +pub mod encryption; pub mod guest_access; pub mod history_visibility; pub mod join_rules; diff --git a/src/room/encrypted.rs b/src/room/encrypted.rs new file mode 100644 index 00000000..4bf5a535 --- /dev/null +++ b/src/room/encrypted.rs @@ -0,0 +1,184 @@ +//! Types for the *m.room.encrypted* event. + +use ruma_identifiers::DeviceId; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{from_value, Value}; + +use crate::Algorithm; + +room_event! { + /// This event type is used when sending encrypted events. + /// + /// This type is to be used within a room. For a to-device event, use `EncryptedEventContent` + /// directly. + pub struct EncryptedEvent(EncryptedEventContent) {} +} + +/// The payload of an *m.room.encrypted* event. +#[derive(Clone, Debug, PartialEq)] +pub enum EncryptedEventContent { + /// An event encrypted with *m.olm.v1.curve25519-aes-sha2*. + OlmV1Curve25519AesSha2(OlmV1Curve25519AesSha2Content), + + /// An event encrypted with *m.megolm.v1.aes-sha2*. + MegolmV1AesSha2(MegolmV1AesSha2Content), + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + __Nonexhaustive, +} + +/// The payload of an *m.room.encrypted* event using the *m.olm.v1.curve25519-aes-sha2* algorithm. +#[derive(Clone, Debug, Serialize, PartialEq, Deserialize)] +pub struct OlmV1Curve25519AesSha2Content { + /// The encryption algorithm used to encrypt this event. + pub algorithm: Algorithm, + + /// The encrypted content of the event. + pub ciphertext: CiphertextInfo, + + /// The Curve25519 key of the sender. + pub sender_key: String, +} + +/// A map from the recipient Curve25519 identity key to ciphertext information. +/// +/// Used for messages encrypted with the *m.olm.v1.curve25519-aes-sha2* algorithm. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct CiphertextInfo { + /// The encrypted payload. + pub body: String, + + /// The Olm message type. + #[serde(rename = "type")] + pub message_type: u64, +} + +/// The payload of an *m.room.encrypted* event using the *m.megolm.v1.aes-sha2* algorithm. +#[derive(Clone, Debug, Serialize, PartialEq, Deserialize)] +pub struct MegolmV1AesSha2Content { + /// The encryption algorithm used to encrypt this event. + pub algorithm: Algorithm, + + /// The encrypted content of the event. + pub ciphertext: String, + + /// The Curve25519 key of the sender. + pub sender_key: String, + + /// The ID of the sending device. + pub device_id: DeviceId, + + /// The ID of the session used to encrypt the message. + pub session_id: String, +} + +impl Serialize for EncryptedEventContent { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + EncryptedEventContent::OlmV1Curve25519AesSha2(ref content) => { + content.serialize(serializer) + } + EncryptedEventContent::MegolmV1AesSha2(ref content) => content.serialize(serializer), + _ => panic!("Attempted to serialize __Nonexhaustive variant."), + } + } +} + +impl<'de> Deserialize<'de> for EncryptedEventContent { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: Value = Deserialize::deserialize(deserializer)?; + + let method_value = match value.get("algorithm") { + Some(value) => value.clone(), + None => return Err(D::Error::missing_field("algorithm")), + }; + + let method = match from_value::(method_value.clone()) { + Ok(method) => method, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + match method { + Algorithm::OlmV1Curve25519AesSha2 => { + let content = match from_value::(value) { + Ok(content) => content, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(EncryptedEventContent::OlmV1Curve25519AesSha2(content)) + } + Algorithm::MegolmV1AesSha2 => { + let content = match from_value::(value) { + Ok(content) => content, + Err(error) => return Err(D::Error::custom(error.to_string())), + }; + + Ok(EncryptedEventContent::MegolmV1AesSha2(content)) + } + Algorithm::Custom(_) => Err(D::Error::custom( + "Custom algorithms are not supported by `EncryptedEventContent`.", + )), + Algorithm::__Nonexhaustive => Err(D::Error::custom( + "Attempted to deserialize __Nonexhaustive variant.", + )), + } + } +} +#[cfg(test)] +mod tests { + use serde_json::{from_str, to_string}; + + use super::{Algorithm, EncryptedEventContent, MegolmV1AesSha2Content}; + + #[test] + fn serializtion() { + let key_verification_start_content = + EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content { + algorithm: Algorithm::MegolmV1AesSha2, + ciphertext: "ciphertext".to_string(), + sender_key: "sender_key".to_string(), + device_id: "device_id".to_string(), + session_id: "session_id".to_string(), + }); + + assert_eq!( + to_string(&key_verification_start_content).unwrap(), + r#"{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"ciphertext","sender_key":"sender_key","device_id":"device_id","session_id":"session_id"}"# + ); + } + + #[test] + fn deserialization() { + let key_verification_start_content = + EncryptedEventContent::MegolmV1AesSha2(MegolmV1AesSha2Content { + algorithm: Algorithm::MegolmV1AesSha2, + ciphertext: "ciphertext".to_string(), + sender_key: "sender_key".to_string(), + device_id: "device_id".to_string(), + session_id: "session_id".to_string(), + }); + + assert_eq!( + from_str::( + r#"{"algorithm":"m.megolm.v1.aes-sha2","ciphertext":"ciphertext","sender_key":"sender_key","device_id":"device_id","session_id":"session_id"}"# + ) + .unwrap(), + key_verification_start_content + ); + } + + #[test] + fn deserialization_failure() { + assert!( + from_str::(r#"{"algorithm":"m.megolm.v1.aes-sha2"}"#).is_err() + ); + } +} diff --git a/src/room/encryption.rs b/src/room/encryption.rs new file mode 100644 index 00000000..2a39ebdf --- /dev/null +++ b/src/room/encryption.rs @@ -0,0 +1,29 @@ +//! Types for the *m.room.encryption* event. + +use serde::{Deserialize, Serialize}; + +use crate::Algorithm; + +state_event! { + /// Defines how messages sent in this room should be encrypted. + pub struct EncryptionEvent(EncryptionEventContent) {} +} + +/// The payload of an *m.room.encryption* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct EncryptionEventContent { + /// The encryption algorithm to be used to encrypt messages sent in this room. + /// + /// Must be `m.megolm.v1.aes-sha2`. + pub algorithm: Algorithm, + + /// How long the session should be used before changing it. + /// + /// 604800000 (a week) is the recommended default. + pub rotation_period_ms: Option, + + /// How many messages should be sent before changing the session. + /// + /// 100 is the recommended default. + pub rotation_period_msgs: Option, +} diff --git a/src/room_key.rs b/src/room_key.rs new file mode 100644 index 00000000..cfebcd93 --- /dev/null +++ b/src/room_key.rs @@ -0,0 +1,31 @@ +//! Types for the *m.room_key* event. + +use ruma_identifiers::RoomId; +use serde::{Deserialize, Serialize}; + +use super::Algorithm; + +event! { + /// This event type is used to exchange keys for end-to-end encryption. + /// + /// Typically it is encrypted as an *m.room.encrypted* event, then sent as a to-device event. + pub struct RoomKeyEvent(RoomKeyEventContent) {} +} + +/// The payload of an *m.room_key* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RoomKeyEventContent { + /// The encryption algorithm the key in this event is to be used with. + /// + /// Must be `m.megolm.v1.aes-sha2`. + pub algorithm: Algorithm, + + /// The room where the key is used. + pub room_id: RoomId, + + /// The ID of the session that the key is for. + pub session_id: String, + + /// The key to be exchanged. + pub session_key: String, +} diff --git a/src/room_key_request.rs b/src/room_key_request.rs new file mode 100644 index 00000000..8aadcce5 --- /dev/null +++ b/src/room_key_request.rs @@ -0,0 +1,73 @@ +//! Types for the *m.room_key_request* event. + +use ruma_identifiers::{DeviceId, RoomId}; +use serde::{Deserialize, Serialize}; + +use super::Algorithm; + +event! { + /// This event type is used to request keys for end-to-end encryption. + /// + /// It is sent as an unencrypted to-device event. + pub struct RoomKeyRequestEvent(RoomKeyRequestEventContent) {} +} + +/// The payload of an *m.room_key_request* event. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RoomKeyRequestEventContent { + /// Whether this is a new key request or a cancellation of a previous request. + pub action: Action, + + /// Information about the requested key. + /// + /// Required when action is `request`. + pub body: Option, + + /// ID of the device requesting the key. + pub requesting_device_id: DeviceId, + + /// A random string uniquely identifying the request for a key. + /// + /// If the key is requested multiple times, it should be reused. It should also reused in order + /// to cancel a request. + pub request_id: String, +} + +/// A new key request or a cancellation of a previous request. +#[derive(Clone, Copy, Deserialize, Debug, PartialEq, Serialize)] +pub enum Action { + /// Request a key. + Request, + + /// Cancel a request for a key. + CancelRequest, + + /// Additional variants may be added in the future and will not be considered breaking changes + /// to `ruma-events`. + #[doc(hidden)] + #[serde(skip)] + __Nonexhaustive, +} + +impl_enum! { + Action { + Request => "request", + CancelRequest => "cancel_request", + } +} + +/// Information about a requested key. +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct RequestedKeyInfo { + /// The encryption algorithm the requested key in this event is to be used with. + pub algorithm: Algorithm, + + /// The room where the key is used. + pub room_id: RoomId, + + /// The Curve25519 key of the device which initiated the session originally. + pub sender_key: String, + + /// The ID of the session that the key is for. + pub session_id: String, +}