use std::borrow::Cow;
use js_int::{uint, UInt};
use ruma_common::{
serde::{Base64, JsonObject},
KeyDerivationAlgorithm,
};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
mod secret_encryption_algorithm_serde;
use crate::macros::EventContent;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct PassPhrase {
pub algorithm: KeyDerivationAlgorithm,
pub salt: String,
pub iterations: UInt,
#[serde(default = "default_bits", skip_serializing_if = "is_default_bits")]
pub bits: UInt,
}
impl PassPhrase {
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()
}
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[derive(Clone, Debug, Serialize, EventContent)]
#[ruma_event(type = "m.secret_storage.key.*", kind = GlobalAccountData)]
pub struct SecretStorageKeyEventContent {
#[ruma_event(type_fragment)]
#[serde(skip)]
pub key_id: String,
pub name: Option<String>,
#[serde(flatten)]
pub algorithm: SecretStorageEncryptionAlgorithm,
#[serde(skip_serializing_if = "Option::is_none")]
pub passphrase: Option<PassPhrase>,
}
impl SecretStorageKeyEventContent {
pub fn new(key_id: String, algorithm: SecretStorageEncryptionAlgorithm) -> Self {
Self { key_id, name: None, algorithm, passphrase: None }
}
}
#[derive(Debug, Clone)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum SecretStorageEncryptionAlgorithm {
V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties),
#[doc(hidden)]
_Custom(CustomSecretEncryptionAlgorithm),
}
impl SecretStorageEncryptionAlgorithm {
pub fn algorithm(&self) -> &str {
match self {
Self::V1AesHmacSha2(_) => "m.secret_storage.v1.aes-hmac-sha2",
Self::_Custom(c) => &c.algorithm,
}
}
pub fn properties(&self) -> Cow<'_, JsonObject> {
fn serialize<T: Serialize>(obj: &T) -> JsonObject {
match serde_json::to_value(obj).expect("secret properties serialization to succeed") {
JsonValue::Object(obj) => obj,
_ => panic!("all secret properties must serialize to objects"),
}
}
match self {
Self::V1AesHmacSha2(p) => Cow::Owned(serialize(p)),
Self::_Custom(c) => Cow::Borrowed(&c.properties),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct SecretStorageV1AesHmacSha2Properties {
pub iv: Option<Base64>,
pub mac: Option<Base64>,
}
impl SecretStorageV1AesHmacSha2Properties {
pub fn new(iv: Option<Base64>, mac: Option<Base64>) -> Self {
Self { iv, mac }
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CustomSecretEncryptionAlgorithm {
algorithm: String,
#[serde(flatten)]
properties: JsonObject,
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use js_int::uint;
use ruma_common::{serde::Base64, KeyDerivationAlgorithm};
use serde_json::{
from_value as from_json_value, json, to_value as to_json_value,
value::to_raw_value as to_raw_json_value,
};
use super::{
PassPhrase, SecretStorageEncryptionAlgorithm, SecretStorageKeyEventContent,
SecretStorageV1AesHmacSha2Properties,
};
use crate::{AnyGlobalAccountDataEvent, EventContentFromType, GlobalAccountDataEvent};
#[test]
fn key_description_serialization() {
let mut content = SecretStorageKeyEventContent::new(
"my_key".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
}),
);
content.name = Some("my_key".to_owned());
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 key_description_deserialization() {
let json = to_raw_json_value(&json!({
"name": "my_key",
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
"iv": "YWJjZGVmZ2hpamtsbW5vcA",
"mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
}))
.unwrap();
let content =
SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
assert_eq!(content.name.unwrap(), "my_key");
assert_matches!(content.passphrase, None);
assert_matches!(
content.algorithm,
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(iv),
mac: Some(mac)
})
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn key_description_deserialization_without_name() {
let json = to_raw_json_value(&json!({
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
"iv": "YWJjZGVmZ2hpamtsbW5vcA",
"mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
}))
.unwrap();
let content =
SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
assert!(content.name.is_none());
assert_matches!(content.passphrase, None);
assert_matches!(
content.algorithm,
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(iv),
mac: Some(mac)
})
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn key_description_with_passphrase_serialization() {
let mut content = SecretStorageKeyEventContent {
passphrase: Some(PassPhrase::new("rocksalt".into(), uint!(8))),
..SecretStorageKeyEventContent::new(
"my_key".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
SecretStorageV1AesHmacSha2Properties {
iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
},
),
)
};
content.name = Some("my_key".to_owned());
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 key_description_with_passphrase_deserialization() {
let json = to_raw_json_value(&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
}
}))
.unwrap();
let content =
SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
assert_eq!(content.name.unwrap(), "my_key");
let passphrase = content.passphrase.unwrap();
assert_eq!(passphrase.algorithm, KeyDerivationAlgorithm::Pbkfd2);
assert_eq!(passphrase.salt, "rocksalt");
assert_eq!(passphrase.iterations, uint!(8));
assert_eq!(passphrase.bits, uint!(256));
assert_matches!(
content.algorithm,
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(iv),
mac: Some(mac)
})
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn event_content_serialization() {
let mut content = SecretStorageKeyEventContent::new(
"my_key_id".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
}),
);
content.name = Some("my_key".to_owned());
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 event_serialization() {
let mut content = SecretStorageKeyEventContent::new(
"my_key_id".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap()),
mac: Some(Base64::parse("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U").unwrap()),
}),
);
content.name = Some("my_key".to_owned());
let event = GlobalAccountDataEvent { content };
let json = json!({
"type": "m.secret_storage.key.my_key_id",
"content": {
"name": "my_key",
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
"iv": "YWJjZGVmZ2hpamtsbW5vcA",
"mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
}
});
assert_eq!(to_json_value(&event).unwrap(), json);
}
#[test]
fn event_deserialization() {
let json = json!({
"type": "m.secret_storage.key.my_key_id",
"content": {
"name": "my_key",
"algorithm": "m.secret_storage.v1.aes-hmac-sha2",
"iv": "YWJjZGVmZ2hpamtsbW5vcA",
"mac": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
}
});
let any_ev = from_json_value::<AnyGlobalAccountDataEvent>(json).unwrap();
assert_matches!(any_ev, AnyGlobalAccountDataEvent::SecretStorageKey(ev));
assert_eq!(ev.content.key_id, "my_key_id");
assert_eq!(ev.content.name.unwrap(), "my_key");
assert_matches!(ev.content.passphrase, None);
assert_matches!(
ev.content.algorithm,
SecretStorageEncryptionAlgorithm::V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties {
iv: Some(iv),
mac: Some(mac)
})
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn custom_algorithm_key_description_deserialization() {
let json = to_raw_json_value(&json!({
"name": "my_key",
"algorithm": "io.ruma.custom_alg",
"io.ruma.custom_prop1": "YWJjZGVmZ2hpamtsbW5vcA",
"io.ruma.custom_prop2": "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U"
}))
.unwrap();
let content =
SecretStorageKeyEventContent::from_parts("m.secret_storage.key.test", &json).unwrap();
assert_eq!(content.name.unwrap(), "my_key");
assert_matches!(content.passphrase, None);
let algorithm = content.algorithm;
assert_eq!(algorithm.algorithm(), "io.ruma.custom_alg");
let properties = algorithm.properties();
assert_eq!(properties.len(), 2);
assert_eq!(
properties.get("io.ruma.custom_prop1").unwrap().as_str(),
Some("YWJjZGVmZ2hpamtsbW5vcA")
);
assert_eq!(
properties.get("io.ruma.custom_prop2").unwrap().as_str(),
Some("aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U")
);
}
}