feat(crypto): Add a SecretsBundle type

This commit is contained in:
Damir Jelić
2024-05-08 13:56:22 +02:00
parent b57dbd8d1a
commit 402d620608
2 changed files with 147 additions and 36 deletions

View File

@@ -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

View File

@@ -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<S>(v: &$foo, serializer: S) -> Result<S::Ok, S::Error>
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<BackupSecrets>,
}
/// 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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
D: Deserializer<'de>,
{
let map: BTreeMap<OwnedUserId, BTreeMap<OwnedDeviceKeyId, String>> =
Deserialize::deserialize(deserializer)?;
@@ -372,7 +477,7 @@ impl<T: Ord + Serialize> Serialize for SigningKeys<T> {
impl<'de, T: Algorithm + Ord + Deserialize<'de>> Deserialize<'de> for SigningKeys<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
D: Deserializer<'de>,
{
let map: BTreeMap<T, String> = 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<Curve25519PublicKey, D::Error>
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<S>(key: &Curve25519PublicKey, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let key = key.to_base64();
s.serialize_str(&key)
}
pub(crate) fn deserialize_ed25519_key<'de, D>(de: D) -> Result<Ed25519PublicKey, D::Error>
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<S>(key: &Ed25519PublicKey, s: S) -> Result<S::Ok, S::Error>
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<Vec<Curve25519PublicKey>, D::Error>
where
D: serde::Deserializer<'de>,
D: Deserializer<'de>,
{
let keys: Vec<String> = Deserialize::deserialize(de)?;
let keys: Result<Vec<Curve25519PublicKey>, KeyError> =
@@ -447,3 +526,35 @@ where
let keys: Vec<String> = 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");
}
}