events: Add secret storage
This commit is contained in:
parent
2669be6087
commit
1fd47fa034
@ -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;
|
||||
|
@ -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.
|
||||
|
5
crates/ruma-common/src/events/secret_storage.rs
Normal file
5
crates/ruma-common/src/events/secret_storage.rs
Normal file
@ -0,0 +1,5 @@
|
||||
//! Module for events in the `m.secret_storage` namespace.
|
||||
|
||||
pub mod default_key;
|
||||
pub mod key;
|
||||
pub mod secret;
|
15
crates/ruma-common/src/events/secret_storage/default_key.rs
Normal file
15
crates/ruma-common/src/events/secret_storage/default_key.rs
Normal file
@ -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,
|
||||
}
|
221
crates/ruma-common/src/events/secret_storage/key.rs
Normal file
221
crates/ruma-common/src/events/secret_storage/key.rs
Normal file
@ -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<PassPhrase>,
|
||||
}
|
||||
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
108
crates/ruma-common/src/events/secret_storage/secret.rs
Normal file
108
crates/ruma-common/src/events/secret_storage/secret.rs
Normal file
@ -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<String, SecretEncryptedData>,
|
||||
}
|
||||
|
||||
impl SecretEventContent {
|
||||
/// Create a new `SecretEventContent` with the given encrypted content.
|
||||
pub fn new(encrypted: BTreeMap<String, SecretEncryptedData>) -> 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::<String, SecretEncryptedData>::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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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},
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user