From 402d620608a2a1b4b233d8238799e59fc0ca56bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 May 2024 13:56:22 +0200 Subject: [PATCH] feat(crypto): Add a SecretsBundle type --- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-crypto/src/types/mod.rs | 181 +++++++++++++++++----- 2 files changed, 147 insertions(+), 36 deletions(-) diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 17aa7c297..123ebb1b9 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -81,10 +81,10 @@ indoc = "2.0.1" matrix-sdk-test = { workspace = true } olm-rs = { version = "2.2.0", features = ["serde"] } proptest = { version = "1.0.0", default-features = false, features = ["std"] } +similar-asserts = "1.5.0" # required for async_test macro stream_assert = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } -similar-asserts = "1.5.0" [lints] workspace = true diff --git a/crates/matrix-sdk-crypto/src/types/mod.rs b/crates/matrix-sdk-crypto/src/types/mod.rs index 1d8e3e0b4..3173190f3 100644 --- a/crates/matrix-sdk-crypto/src/types/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2022 The Matrix.org Foundation C.I.C. +// Copyright 2022-2024 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,8 +35,9 @@ use as_variant::as_variant; use ruma::{ serde::StringEnum, DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, OwnedUserId, UserId, }; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use vodozemac::{Curve25519PublicKey, Ed25519PublicKey, Ed25519Signature, KeyError}; +use zeroize::{Zeroize, ZeroizeOnDrop}; mod backup; mod cross_signing; @@ -46,6 +47,110 @@ mod one_time_keys; pub mod qr_login; pub use self::{backup::*, cross_signing::*, device_keys::*, one_time_keys::*}; +use crate::store::BackupDecryptionKey; + +macro_rules! from_base64 { + ($foo:ident, $name:ident) => { + pub(crate) fn $name<'de, D>(deserializer: D) -> Result<$foo, D::Error> + where + D: Deserializer<'de>, + { + let mut string = String::deserialize(deserializer)?; + + let result = $foo::from_base64(&string); + string.zeroize(); + + result.map_err(serde::de::Error::custom) + } + }; +} + +macro_rules! to_base64 { + ($foo:ident, $name:ident) => { + pub(crate) fn $name(v: &$foo, serializer: S) -> Result + where + S: Serializer, + { + let mut string = v.to_base64(); + let ret = string.serialize(serializer); + + string.zeroize(); + + ret + } + }; +} + +/// Struct containing the bundle of secrets to fully activate a new devices for +/// end-to-end encryption. +#[derive(Debug, Deserialize, Clone, Serialize, ZeroizeOnDrop)] +pub struct SecretsBundle { + /// The cross-signing keys. + pub cross_signing: CrossSigningSecrets, + /// The backup key, if available. + pub backup: Option, +} + +/// Data for the secrets bundle containing the cross-signing keys. +#[derive(Deserialize, Clone, Serialize, ZeroizeOnDrop)] +pub struct CrossSigningSecrets { + /// The seed for the private part of the cross-signing master key, encoded + /// as base64. + pub master_key: String, + /// The seed for the private part of the cross-signing user-signing key, + /// encoded as base64. + pub user_signing_key: String, + /// The seed for the private part of the cross-signing self-signing key, + /// encoded as base64. + pub self_signing_key: String, +} + +impl std::fmt::Debug for CrossSigningSecrets { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CrossSigningSecrets") + .field("master_key", &"...") + .field("user_signing_key", &"...") + .field("self_signing_key", &"...") + .finish() + } +} + +/// Data for the secrets bundle containing the secret and version for a +/// `m.megolm_backup.v1.curve25519-aes-sha2` backup. +#[derive(Debug, Deserialize, Clone, Serialize, ZeroizeOnDrop)] +pub struct MegolmBackupV1Curve25519AesSha2Secrets { + /// The private half of the backup key, can be used to access and decrypt + /// room keys in the backup. Also called the recovery key in the + /// [spec](https://spec.matrix.org/v1.10/client-server-api/#recovery-key). + #[serde(serialize_with = "backup_key_to_base64", deserialize_with = "backup_key_from_base64")] + pub key: BackupDecryptionKey, + /// The backup version that is tied to the above backup key. + pub backup_version: String, +} + +from_base64!(BackupDecryptionKey, backup_key_from_base64); +to_base64!(BackupDecryptionKey, backup_key_to_base64); + +/// Enum for the algorithm-specific secrets for the room key backup. +#[derive(Debug, Clone, ZeroizeOnDrop, Serialize, Deserialize)] +#[serde(tag = "algorithm")] +pub enum BackupSecrets { + /// Backup secrets for the `m.megolm_backup.v1.curve25519-aes-sha2` backup + /// algorithm. + #[serde(rename = "m.megolm_backup.v1.curve25519-aes-sha2")] + MegolmBackupV1Curve25519AesSha2(MegolmBackupV1Curve25519AesSha2Secrets), +} + +impl BackupSecrets { + /// Get the algorithm of the secrets contained in the [`BackupSecrets`]. + pub fn algorithm(&self) -> &str { + match &self { + BackupSecrets::MegolmBackupV1Curve25519AesSha2(_) => { + "m.megolm_backup.v1.curve25519-aes-sha2" + } + } + } +} /// Represents a potentially decoded signature (but *not* a validated one). /// @@ -170,7 +275,7 @@ impl IntoIterator for Signatures { impl<'de> Deserialize<'de> for Signatures { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer<'de>, + D: Deserializer<'de>, { let map: BTreeMap> = Deserialize::deserialize(deserializer)?; @@ -372,7 +477,7 @@ impl Serialize for SigningKeys { impl<'de, T: Algorithm + Ord + Deserialize<'de>> Deserialize<'de> for SigningKeys { fn deserialize(deserializer: D) -> Result where - D: serde::Deserializer<'de>, + D: Deserializer<'de>, { let map: BTreeMap = Deserialize::deserialize(deserializer)?; @@ -394,41 +499,15 @@ impl<'de, T: Algorithm + Ord + Deserialize<'de>> Deserialize<'de> for SigningKey // likes to base64 encode all byte slices. // // This ensures that we serialize/deserialize in a Matrix-compatible way. -pub(crate) fn deserialize_curve_key<'de, D>(de: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let key: String = Deserialize::deserialize(de)?; - Curve25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom) -} +from_base64!(Curve25519PublicKey, deserialize_curve_key); +to_base64!(Curve25519PublicKey, serialize_curve_key); -pub(crate) fn serialize_curve_key(key: &Curve25519PublicKey, s: S) -> Result -where - S: Serializer, -{ - let key = key.to_base64(); - s.serialize_str(&key) -} - -pub(crate) fn deserialize_ed25519_key<'de, D>(de: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let key: String = Deserialize::deserialize(de)?; - Ed25519PublicKey::from_base64(&key).map_err(serde::de::Error::custom) -} - -pub(crate) fn serialize_ed25519_key(key: &Ed25519PublicKey, s: S) -> Result -where - S: Serializer, -{ - let key = key.to_base64(); - s.serialize_str(&key) -} +from_base64!(Ed25519PublicKey, deserialize_ed25519_key); +to_base64!(Ed25519PublicKey, serialize_ed25519_key); pub(crate) fn deserialize_curve_key_vec<'de, D>(de: D) -> Result, D::Error> where - D: serde::Deserializer<'de>, + D: Deserializer<'de>, { let keys: Vec = Deserialize::deserialize(de)?; let keys: Result, KeyError> = @@ -447,3 +526,35 @@ where let keys: Vec = keys.iter().map(|k| k.to_base64()).collect(); keys.serialize(s) } + +#[cfg(test)] +mod test { + use serde_json::json; + use similar_asserts::assert_eq; + + use super::*; + + #[test] + fn serialize_secrets_bundle() { + let json = json!({ + "cross_signing": { + "master_key": "rTtSv67XGS6k/rg6/yTG/m573cyFTPFRqluFhQY+hSw", + "self_signing_key": "4jbPt7jh5D2iyM4U+3IDa+WthgJB87IQN1ATdkau+xk", + "user_signing_key": "YkFKtkjcsTxF6UAzIIG/l6Nog/G2RigCRfWj3cjNWeM", + }, + "backup": { + "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2", + "backup_version": "2", + "key": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + }); + + let deserialized: SecretsBundle = serde_json::from_value(json.clone()) + .expect("We should be able to deserialize the secrets bundle"); + + let serialized = serde_json::to_value(&deserialized) + .expect("We should be able to serialize a secrets bundle"); + + assert_eq!(json, serialized, "A serialization cycle should yield the same result"); + } +}