2023-10-05 10:48:29 +02:00

287 lines
9.7 KiB
Rust

//! Types for the [`m.secret_storage.key.*`] event.
//!
//! [`m.secret_storage.key.*`]: https://spec.matrix.org/latest/client-server-api/#key-storage
use js_int::{uint, UInt};
use ruma_common::{serde::Base64, KeyDerivationAlgorithm};
use serde::{Deserialize, Serialize};
use crate::macros::EventContent;
/// 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, 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: Option<String>,
/// The encryption algorithm used for this key.
///
/// Currently, only `m.secret_storage.v1.aes-hmac-sha2` is supported.
#[serde(flatten)]
pub algorithm: SecretStorageEncryptionAlgorithm,
/// The passphrase from which to generate the key.
#[serde(skip_serializing_if = "Option::is_none")]
pub passphrase: Option<PassPhrase>,
}
impl SecretStorageKeyEventContent {
/// Creates a `KeyDescription` with the given name.
pub fn new(key_id: String, algorithm: SecretStorageEncryptionAlgorithm) -> Self {
Self { key_id, name: None, 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 SecretStorageEncryptionAlgorithm {
#[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.
V1AesHmacSha2 {
/// The 16-byte initialization vector, encoded as base64.
iv: Base64,
/// The MAC, encoded as base64.
mac: Base64,
},
}
#[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};
use crate::{EventContentFromType, GlobalAccountDataEvent};
#[test]
fn test_key_description_serialization() {
let mut content = SecretStorageKeyEventContent::new(
"my_key".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2 {
iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(),
mac: 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 test_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 { iv, mac }
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn test_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 { iv, mac }
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn test_key_description_with_passphrase_serialization() {
let mut content = SecretStorageKeyEventContent {
passphrase: Some(PassPhrase::new("rocksalt".into(), uint!(8))),
..SecretStorageKeyEventContent::new(
"my_key".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2 {
iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(),
mac: 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 test_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 { iv, mac }
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
#[test]
fn test_event_serialization() {
let mut content = SecretStorageKeyEventContent::new(
"my_key_id".into(),
SecretStorageEncryptionAlgorithm::V1AesHmacSha2 {
iv: Base64::parse("YWJjZGVmZ2hpamtsbW5vcA").unwrap(),
mac: 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 test_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 ev =
from_json_value::<GlobalAccountDataEvent<SecretStorageKeyEventContent>>(json).unwrap();
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 { iv, mac }
);
assert_eq!(iv.encode(), "YWJjZGVmZ2hpamtsbW5vcA");
assert_eq!(mac.encode(), "aWRvbnRrbm93d2hhdGFtYWNsb29rc2xpa2U");
}
}