diff --git a/crates/ruma-common/src/events.rs b/crates/ruma-common/src/events.rs index ebe457d7..87e87ab8 100644 --- a/crates/ruma-common/src/events.rs +++ b/crates/ruma-common/src/events.rs @@ -156,6 +156,7 @@ pub mod room; pub mod room_key; pub mod room_key_request; pub mod secret; +pub mod secret_storage; pub mod space; pub mod sticker; pub mod tag; diff --git a/crates/ruma-common/src/events/enums.rs b/crates/ruma-common/src/events/enums.rs index aebfc0ba..158b780f 100644 --- a/crates/ruma-common/src/events/enums.rs +++ b/crates/ruma-common/src/events/enums.rs @@ -18,6 +18,8 @@ event_enum! { "m.direct", "m.ignored_user_list", "m.push_rules", + "m.secret_storage.default_key", + "m.secret_storage.key.*", } /// Any room account data event. diff --git a/crates/ruma-common/src/events/secret_storage.rs b/crates/ruma-common/src/events/secret_storage.rs new file mode 100644 index 00000000..501d09f5 --- /dev/null +++ b/crates/ruma-common/src/events/secret_storage.rs @@ -0,0 +1,5 @@ +//! Module for events in the `m.secret_storage` namespace. + +pub mod default_key; +pub mod key; +pub mod secret; diff --git a/crates/ruma-common/src/events/secret_storage/default_key.rs b/crates/ruma-common/src/events/secret_storage/default_key.rs new file mode 100644 index 00000000..23df5b22 --- /dev/null +++ b/crates/ruma-common/src/events/secret_storage/default_key.rs @@ -0,0 +1,15 @@ +//! Types for the [`m.secret_storage.default_key`] event. +//! +//! [`m.secret_storage.default_key`]: https://spec.matrix.org/v1.2/client-server-api/#key-storage + +use ruma_common::events::macros::EventContent; +use serde::{Deserialize, Serialize}; + +/// The payload for `DefaultKeyEvent`. +#[derive(Clone, Debug, Deserialize, Serialize, EventContent)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[ruma_event(type = "m.secret_storage.default_key", kind = GlobalAccountData)] +pub struct SecretStorageDefaultKeyEventContent { + /// The ID of the default key. + pub key: String, +} diff --git a/crates/ruma-common/src/events/secret_storage/key.rs b/crates/ruma-common/src/events/secret_storage/key.rs new file mode 100644 index 00000000..d2706e61 --- /dev/null +++ b/crates/ruma-common/src/events/secret_storage/key.rs @@ -0,0 +1,221 @@ +//! Types for the [`m.secret_storage.key.*`] event. +//! +//! [`m.secret_storage.key.*`]: https://spec.matrix.org/v1.2/client-server-api/#key-storage + +use js_int::{uint, UInt}; +use serde::{Deserialize, Serialize}; + +use crate::{events::macros::EventContent, identifiers::KeyDerivationAlgorithm, serde::Base64}; + +/// A passphrase from which a key is to be derived. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct PassPhrase { + /// The algorithm to use to generate the key from the passphrase. + /// + /// Must be `m.pbkdf2`. + pub algorithm: KeyDerivationAlgorithm, + + /// The salt used in PBKDF2. + pub salt: String, + + /// The number of iterations to use in PBKDF2. + pub iterations: UInt, + + /// The number of bits to generate for the key. + /// + /// Defaults to 256 + #[serde(default = "default_bits", skip_serializing_if = "is_default_bits")] + pub bits: UInt, +} + +impl PassPhrase { + /// Creates a new `PassPhrase` with a given salt and number of iterations. + pub fn new(salt: String, iterations: UInt) -> Self { + Self { algorithm: KeyDerivationAlgorithm::Pbkfd2, salt, iterations, bits: default_bits() } + } +} + +fn default_bits() -> UInt { + uint!(256) +} + +fn is_default_bits(val: &UInt) -> bool { + *val == default_bits() +} + +/// A key description encrypted using a specified algorithm. +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[derive(Clone, Debug, Serialize, Deserialize, EventContent)] +#[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)] +pub struct SecretStorageKeyEventContent { + /// The ID of the key. + #[ruma_event(type_fragment)] + #[serde(skip)] + pub key_id: String, + + /// The name of the key. + pub name: String, + + /// The encryption algorithm used for this key. + /// + /// Currently, only `m.secret_storage.v1.aes-hmac-sha2` is supported. + #[serde(flatten)] + pub algorithm: SecretEncryptionAlgorithm, + + /// The passphrase from which to generate the key. + #[serde(skip_serializing_if = "Option::is_none")] + pub passphrase: Option, +} + +impl SecretStorageKeyEventContent { + /// Creates a `KeyDescription` with the given name. + pub fn new(key_id: String, name: String, algorithm: SecretEncryptionAlgorithm) -> Self { + Self { key_id, name, algorithm, passphrase: None } + } +} + +/// An algorithm and its properties, used to encrypt a secret. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(tag = "algorithm")] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub enum SecretEncryptionAlgorithm { + #[serde(rename = "m.secret_storage.v1.aes-hmac-sha2")] + /// Encrypted using the `m.secrect_storage.v1.aes-hmac-sha2` algorithm. + /// + /// Secrets using this method are encrypted using AES-CTR-256 and authenticated using + /// HMAC-SHA-256. + SecretStorageV1AesHmacSha2 { + /// The 16-byte initialization vector, encoded as base64. + iv: Base64, + + /// The MAC, encoded as base64. + mac: Base64, + }, +} + +#[cfg(test)] +mod tests { + use js_int::uint; + use matches::assert_matches; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::{PassPhrase, SecretEncryptionAlgorithm, SecretStorageKeyEventContent}; + use crate::{serde::Base64, KeyDerivationAlgorithm}; + + #[test] + fn test_key_description_serialization() { + let content = SecretStorageKeyEventContent::new( + "my_key".into(), + "my_key".into(), + SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { + iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), + mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), + }, + ); + + let json = json!({ + "name": "my_key", + "algorithm": "m.secret_storage.v1.aes-hmac-sha2", + "iv": "YWJjZGVmZ2hpamtsbW5vcA", + "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn test_key_description_deserialization() { + let json = json!({ + "name": "my_key", + "algorithm": "m.secret_storage.v1.aes-hmac-sha2", + "iv": "YWJjZGVmZ2hpamtsbW5vcA", + "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" + }); + + assert_matches!( + from_json_value(json).unwrap(), + SecretStorageKeyEventContent { + key_id: _, + name, + algorithm: SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { + iv, + mac, + }, + passphrase: None, + } + if name == *"my_key" + && iv == Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap() + && mac == Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap() + ) + } + + #[test] + fn test_key_description_with_passphrase_serialization() { + let content = SecretStorageKeyEventContent { + passphrase: Some(PassPhrase::new("rocksalt".into(), uint!(8))), + ..SecretStorageKeyEventContent::new( + "my_key".into(), + "my_key".into(), + SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { + iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), + mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), + }, + ) + }; + + let json = json!({ + "name": "my_key", + "algorithm": "m.secret_storage.v1.aes-hmac-sha2", + "iv": "YWJjZGVmZ2hpamtsbW5vcA", + "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U", + "passphrase": { + "algorithm": "m.pbkdf2", + "salt": "rocksalt", + "iterations": 8 + } + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn test_key_description_with_passphrase_deserialization() { + let json = json!({ + "name": "my_key", + "algorithm": "m.secret_storage.v1.aes-hmac-sha2", + "iv": "YWJjZGVmZ2hpamtsbW5vcA", + "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U", + "passphrase": { + "algorithm": "m.pbkdf2", + "salt": "rocksalt", + "iterations": 8, + "bits": 256 + } + }); + + assert_matches!( + from_json_value(json).unwrap(), + SecretStorageKeyEventContent { + key_id: _key, + name, + algorithm: SecretEncryptionAlgorithm::SecretStorageV1AesHmacSha2 { + iv, + mac, + }, + passphrase: Some(PassPhrase { + algorithm: KeyDerivationAlgorithm::Pbkfd2, + salt, + iterations, + bits + }) + } + if name == *"my_key" + && iv == Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap() + && mac == Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap() + && salt == *"rocksalt" + && iterations == uint!(8) + && bits == uint!(256) + ) + } +} diff --git a/crates/ruma-common/src/events/secret_storage/secret.rs b/crates/ruma-common/src/events/secret_storage/secret.rs new file mode 100644 index 00000000..5b7eae10 --- /dev/null +++ b/crates/ruma-common/src/events/secret_storage/secret.rs @@ -0,0 +1,108 @@ +//! Types for events used for secrets to be stored in the user's account_data. + +use std::collections::BTreeMap; + +use crate::serde::Base64; +use serde::{Deserialize, Serialize}; + +/// A secret and its encrypted contents. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SecretEventContent { + /// Map from key ID to the encrypted data. + /// + /// The exact format for the encrypted data is dependent on the key algorithm. + pub encrypted: BTreeMap, +} + +impl SecretEventContent { + /// Create a new `SecretEventContent` with the given encrypted content. + pub fn new(encrypted: BTreeMap) -> Self { + Self { encrypted } + } +} + +/// Encrypted data for a corresponding secret storage encryption algorithm. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(untagged)] +pub enum SecretEncryptedData { + /// Data encrypted using the *m.secret_storage.v1.aes-hmac-sha2* algorithm. + AesHmacSha2EncryptedData { + /// The 16-byte initialization vector, encoded as base64. + iv: Base64, + + /// The AES-CTR-encrypted data, encoded as base64. + ciphertext: Base64, + + /// The MAC, encoded as base64. + mac: Base64, + }, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use matches::assert_matches; + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use crate::serde::Base64; + + use super::{SecretEncryptedData, SecretEventContent}; + + #[test] + fn test_secret_serialization() { + let key_one_data = SecretEncryptedData::AesHmacSha2EncryptedData { + iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(), + ciphertext: Base64::parse("dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ").unwrap(), + mac: Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap(), + }; + + let mut encrypted = BTreeMap::::new(); + encrypted.insert("key_one".to_owned(), key_one_data); + + let content = SecretEventContent::new(encrypted); + + let json = json!({ + "encrypted": { + "key_one" : { + "iv": "YWJjZGVmZ2hpamtsbW5vcA", + "ciphertext": "dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ", + "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" + } + } + }); + + assert_eq!(to_json_value(&content).unwrap(), json); + } + + #[test] + fn test_secret_deserialization() { + let json = json!({ + "encrypted": { + "key_one" : { + "iv": "YWJjZGVmZ2hpamtsbW5vcA", + "ciphertext": "dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ", + "mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U" + } + } + }); + + let deserialized: SecretEventContent = from_json_value(json).unwrap(); + + if let Some(secret_data) = deserialized.encrypted.get("key_one") { + assert_matches!( + secret_data, + SecretEncryptedData::AesHmacSha2EncryptedData { + iv, + ciphertext, + mac + } + if iv == &Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap() + && ciphertext == &Base64::parse("dGhpc2lzZGVmaW5pdGVseWNpcGhlcnRleHQ").unwrap() + && mac == &Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap() + ) + } + } +} diff --git a/crates/ruma-common/src/identifiers.rs b/crates/ruma-common/src/identifiers.rs index a1b8a22f..2d379bf7 100644 --- a/crates/ruma-common/src/identifiers.rs +++ b/crates/ruma-common/src/identifiers.rs @@ -11,7 +11,9 @@ use serde::de::{self, Deserializer, Unexpected}; #[doc(inline)] pub use self::{ client_secret::{ClientSecret, OwnedClientSecret}, - crypto_algorithms::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, SigningKeyAlgorithm}, + crypto_algorithms::{ + DeviceKeyAlgorithm, EventEncryptionAlgorithm, KeyDerivationAlgorithm, SigningKeyAlgorithm, + }, device_id::{DeviceId, OwnedDeviceId}, device_key_id::{DeviceKeyId, OwnedDeviceKeyId}, event_id::{EventId, OwnedEventId}, diff --git a/crates/ruma-common/src/identifiers/crypto_algorithms.rs b/crates/ruma-common/src/identifiers/crypto_algorithms.rs index dd628e56..96f62388 100644 --- a/crates/ruma-common/src/identifiers/crypto_algorithms.rs +++ b/crates/ruma-common/src/identifiers/crypto_algorithms.rs @@ -53,6 +53,20 @@ pub enum EventEncryptionAlgorithm { _Custom(PrivOwnedStr), } +/// A key algorithm to be used to generate a key from a passphrase. +#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, StringEnum)] +#[non_exhaustive] +#[cfg_attr(feature = "serde", derive(DeserializeFromCowStr, SerializeAsRefStr))] +pub enum KeyDerivationAlgorithm { + /// PBKDF2 + #[ruma_enum(rename = "m.pbkdf2")] + Pbkfd2, + + #[doc(hidden)] + _Custom(PrivOwnedStr), +} + #[cfg(test)] mod tests { use super::{DeviceKeyAlgorithm, SigningKeyAlgorithm}; @@ -86,4 +100,14 @@ mod tests { ); serde_json_eq(EventEncryptionAlgorithm::from("io.ruma.test"), json!("io.ruma.test")); } + + #[test] + fn key_derivation_algorithm_serde() { + use serde_json::json; + + use super::KeyDerivationAlgorithm; + use crate::serde::test::serde_json_eq; + + serde_json_eq(KeyDerivationAlgorithm::Pbkfd2, json!("m.pbkdf2")); + } }