diff --git a/matrix_sdk/Cargo.toml b/matrix_sdk/Cargo.toml index cc1ba580..608f6c57 100644 --- a/matrix_sdk/Cargo.toml +++ b/matrix_sdk/Cargo.toml @@ -31,7 +31,7 @@ docs = ["encryption", "sqlite_cryptostore", "messages"] async-trait = "0.1.41" dashmap = { version = "3.11.10", optional = true } http = "0.2.1" -serde_json = "1.0.58" +serde_json = "1.0.59" thiserror = "1.0.21" tracing = "0.1.21" url = "2.1.1" @@ -73,7 +73,7 @@ async-std = { version = "1.6.5", features = ["unstable"] } dirs = "3.0.1" matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" } tokio = { version = "0.2.22", features = ["rt-threaded", "macros"] } -serde_json = "1.0.58" +serde_json = "1.0.59" tracing-subscriber = "0.2.13" tempfile = "3.1.0" mockito = "0.27.0" diff --git a/matrix_sdk_base/Cargo.toml b/matrix_sdk_base/Cargo.toml index 7d8de457..5eb04c64 100644 --- a/matrix_sdk_base/Cargo.toml +++ b/matrix_sdk_base/Cargo.toml @@ -26,7 +26,7 @@ docs = ["encryption", "sqlite_cryptostore", "messages"] [dependencies] async-trait = "0.1.41" serde = "1.0.116" -serde_json = "1.0.58" +serde_json = "1.0.59" zeroize = "1.1.1" tracing = "0.1.21" diff --git a/matrix_sdk_crypto/Cargo.toml b/matrix_sdk_crypto/Cargo.toml index 023c9cb3..c437da65 100644 --- a/matrix_sdk_crypto/Cargo.toml +++ b/matrix_sdk_crypto/Cargo.toml @@ -28,7 +28,7 @@ matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } olm-rs = { version = "1.0.0", features = ["serde"] } getrandom = "0.2.0" serde = { version = "1.0.116", features = ["derive", "rc"] } -serde_json = "1.0.58" +serde_json = "1.0.59" cjson = "0.1.1" zeroize = { version = "1.1.1", features = ["zeroize_derive"] } url = "2.1.1" @@ -39,6 +39,7 @@ tracing = "0.1.21" atomic = "0.5.0" dashmap = "3.11.10" sha2 = "0.9.1" +aes-gcm = "0.7.0" aes-ctr = "0.5.0" pbkdf2 = { version = "0.5.0", default-features = false } hmac = "0.9.0" @@ -60,7 +61,7 @@ features = ["runtime-tokio", "sqlite", "macros"] tokio = { version = "0.2.22", features = ["rt-threaded", "macros"] } futures = "0.3.6" proptest = "0.10.1" -serde_json = "1.0.58" +serde_json = "1.0.59" tempfile = "3.1.0" http = "0.2.1" matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" } diff --git a/matrix_sdk_crypto/src/identities/user.rs b/matrix_sdk_crypto/src/identities/user.rs index a23dbc96..83055ab2 100644 --- a/matrix_sdk_crypto/src/identities/user.rs +++ b/matrix_sdk_crypto/src/identities/user.rs @@ -86,6 +86,24 @@ impl From for UserSigningPubkey { } } +impl Into for MasterPubkey { + fn into(self) -> CrossSigningKey { + self.0.as_ref().clone() + } +} + +impl Into for UserSigningPubkey { + fn into(self) -> CrossSigningKey { + self.0.as_ref().clone() + } +} + +impl Into for SelfSigningPubkey { + fn into(self) -> CrossSigningKey { + self.0.as_ref().clone() + } +} + impl AsRef for MasterPubkey { fn as_ref(&self) -> &CrossSigningKey { &self.0 @@ -135,7 +153,7 @@ impl<'a> From<&'a UserSigningPubkey> for CrossSigningSubKeys<'a> { } /// Enum over the cross signing sub-keys. -enum CrossSigningSubKeys<'a> { +pub(crate) enum CrossSigningSubKeys<'a> { /// The self signing subkey. SelfSigning(&'a SelfSigningPubkey), /// The user signing subkey. @@ -152,7 +170,7 @@ impl<'a> CrossSigningSubKeys<'a> { } /// Get the `CrossSigningKey` from an sub-keys enum - fn cross_signing_key(&self) -> &CrossSigningKey { + pub(crate) fn cross_signing_key(&self) -> &CrossSigningKey { match self { CrossSigningSubKeys::SelfSigning(key) => &key.0, CrossSigningSubKeys::UserSigning(key) => &key.0, @@ -198,7 +216,7 @@ impl MasterPubkey { /// /// Returns an empty result if the signature check succeeded, otherwise a /// SignatureError indicating why the check failed. - fn verify_subkey<'a>( + pub(crate) fn verify_subkey<'a>( &self, subkey: impl Into>, ) -> Result<(), SignatureError> { diff --git a/matrix_sdk_crypto/src/olm/mod.rs b/matrix_sdk_crypto/src/olm/mod.rs index eef28ff1..f2e39094 100644 --- a/matrix_sdk_crypto/src/olm/mod.rs +++ b/matrix_sdk_crypto/src/olm/mod.rs @@ -20,6 +20,7 @@ mod account; mod group_sessions; mod session; +mod signing; mod utility; pub(crate) use account::Account; diff --git a/matrix_sdk_crypto/src/olm/signing.rs b/matrix_sdk_crypto/src/olm/signing.rs new file mode 100644 index 00000000..db15bd95 --- /dev/null +++ b/matrix_sdk_crypto/src/olm/signing.rs @@ -0,0 +1,606 @@ +// Copyright 2020 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(dead_code)] + +use aes_gcm::{ + aead::{generic_array::GenericArray, Aead, NewAead}, + Aes256Gcm, +}; +use base64::{decode_config, encode_config, DecodeError, URL_SAFE_NO_PAD}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::BTreeMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, +}; +use zeroize::Zeroizing; + +use olm_rs::{errors::OlmUtilityError, pk::OlmPkSigning, utility::OlmUtility, PicklingMode}; + +use matrix_sdk_common::{ + api::r0::keys::{upload_signing_keys::Request as UploadRequest, CrossSigningKey, KeyUsage}, + identifiers::UserId, + locks::Mutex, +}; + +use crate::identities::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey}; + +fn encode>(input: T) -> String { + encode_config(input, URL_SAFE_NO_PAD) +} + +fn decode>(input: T) -> Result, DecodeError> { + decode_config(input, URL_SAFE_NO_PAD) +} + +#[derive(Clone)] +pub struct Signing { + inner: Arc>, + seed: Arc>>, + public_key: PublicSigningKey, +} + +impl std::fmt::Debug for Signing { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signing") + .field("public_key", &self.public_key.as_str()) + .finish() + } +} + +impl PartialEq for Signing { + fn eq(&self, other: &Signing) -> bool { + self.seed == other.seed + } +} + +#[derive(Clone, PartialEq, Debug)] +struct MasterSigning { + inner: Signing, + public_key: MasterPubkey, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct PickledMasterSigning { + pickle: PickledSigning, + public_key: CrossSigningKey, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct PickledUserSigning { + pickle: PickledSigning, + public_key: CrossSigningKey, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct PickledSelfSigning { + pickle: PickledSigning, + public_key: CrossSigningKey, +} + +impl MasterSigning { + async fn pickle(&self, pickle_mode: PicklingMode) -> PickledMasterSigning { + let pickle = self.inner.pickle(pickle_mode).await; + let public_key = self.public_key.clone().into(); + PickledMasterSigning { pickle, public_key } + } + + fn from_pickle(pickle: PickledMasterSigning, pickle_mode: PicklingMode) -> Self { + let inner = Signing::from_pickle(pickle.pickle, pickle_mode); + + Self { + inner, + public_key: pickle.public_key.into(), + } + } + + async fn sign_subkey<'a>(&self, subkey: &mut CrossSigningKey) { + // TODO create a borrowed version of a cross singing key. + let subkey_wihtout_signatures = CrossSigningKey { + user_id: subkey.user_id.clone(), + keys: subkey.keys.clone(), + usage: subkey.usage.clone(), + signatures: BTreeMap::new(), + }; + + let message = cjson::to_string(&subkey_wihtout_signatures) + .expect("Can't serialize cross signing subkey"); + let signature = self.inner.sign(&message).await; + + subkey + .signatures + .entry(self.public_key.user_id().to_owned()) + .or_insert_with(BTreeMap::new) + .insert( + format!("ed25519:{}", self.inner.public_key().as_str()), + signature.0, + ); + } +} + +impl UserSigning { + async fn pickle(&self, pickle_mode: PicklingMode) -> PickledUserSigning { + let pickle = self.inner.pickle(pickle_mode).await; + let public_key = self.public_key.clone().into(); + PickledUserSigning { pickle, public_key } + } + + fn from_pickle(pickle: PickledUserSigning, pickle_mode: PicklingMode) -> Self { + let inner = Signing::from_pickle(pickle.pickle, pickle_mode); + + Self { + inner, + public_key: pickle.public_key.into(), + } + } +} + +impl SelfSigning { + async fn pickle(&self, pickle_mode: PicklingMode) -> PickledSelfSigning { + let pickle = self.inner.pickle(pickle_mode).await; + let public_key = self.public_key.clone().into(); + PickledSelfSigning { pickle, public_key } + } + + fn from_pickle(pickle: PickledSelfSigning, pickle_mode: PicklingMode) -> Self { + let inner = Signing::from_pickle(pickle.pickle, pickle_mode); + + Self { + inner, + public_key: pickle.public_key.into(), + } + } +} + +#[derive(Clone, PartialEq, Debug)] +struct SelfSigning { + inner: Signing, + public_key: SelfSigningPubkey, +} + +#[derive(Clone, PartialEq, Debug)] +struct UserSigning { + inner: Signing, + public_key: UserSigningPubkey, +} + +#[derive(Clone, Debug)] +pub struct PrivateCrossSigningIdentity { + user_id: Arc, + shared: Arc, + master_key: Arc>>, + user_signing_key: Arc>>, + self_signing_key: Arc>>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PickledCrossSigningIdentity { + user_id: UserId, + shared: bool, + + master_key: Option, + user_signing_key: Option, + self_signing_key: Option, +} + +#[derive(Debug, Clone)] +pub struct Signature(String); + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PickledSigning(String); + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct InnerPickle { + version: u8, + nonce: String, + ciphertext: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct PublicSigningKey(Arc); + +impl Signature { + fn as_str(&self) -> &str { + &self.0 + } +} + +impl PickledSigning { + fn as_str(&self) -> &str { + &self.0 + } +} + +impl PublicSigningKey { + fn as_str(&self) -> &str { + &self.0 + } + + fn to_string(&self) -> String { + self.0.to_string() + } +} + +impl Signing { + fn new() -> Self { + let seed = OlmPkSigning::generate_seed(); + Self::from_seed(seed) + } + + fn from_seed(seed: Vec) -> Self { + let inner = OlmPkSigning::new(seed.clone()).expect("Unable to create pk signing object"); + let public_key = PublicSigningKey(Arc::new(inner.public_key().to_owned())); + + Signing { + inner: Arc::new(Mutex::new(inner)), + seed: Arc::new(Zeroizing::from(seed)), + public_key, + } + } + + fn from_pickle(pickle: PickledSigning, pickle_mode: PicklingMode) -> Self { + let mode = if let PicklingMode::Encrypted { key } = &pickle_mode { + key + } else { + "DEFAULT_PICKLE_PASSPHRASE_123456".as_bytes() + }; + + let pickled: InnerPickle = serde_json::from_str(pickle.as_str()).unwrap(); + + let key = GenericArray::from_slice(mode); + let cipher = Aes256Gcm::new(key); + + let nonce = decode(pickled.nonce).unwrap(); + let nonce = GenericArray::from_slice(&nonce); + let ciphertext = &decode(pickled.ciphertext).unwrap(); + + let seed = cipher + .decrypt(&nonce, ciphertext.as_slice()) + .expect("Can't decrypt pickle"); + + Self::from_seed(seed) + } + + async fn pickle(&self, pickle_mode: PicklingMode) -> PickledSigning { + let mode = if let PicklingMode::Encrypted { key } = &pickle_mode { + key + } else { + "DEFAULT_PICKLE_PASSPHRASE_123456".as_bytes() + }; + + let key = GenericArray::from_slice(mode); + let cipher = Aes256Gcm::new(key); + let nonce = GenericArray::from_slice(b"unique nonce"); + let ciphertext = cipher + .encrypt(nonce, self.seed.as_slice()) + .expect("Can't encrypt signing pickle"); + + let ciphertext = encode(ciphertext); + + let pickle = InnerPickle { + version: 1, + nonce: encode(nonce.as_slice()), + ciphertext, + }; + + PickledSigning(serde_json::to_string(&pickle).expect("Can't encode pickled signing")) + } + + fn public_key(&self) -> &PublicSigningKey { + &self.public_key + } + + fn cross_signing_key(&self, user_id: UserId, usage: KeyUsage) -> CrossSigningKey { + let mut keys = BTreeMap::new(); + + keys.insert( + format!("ed25519:{}", self.public_key().as_str()), + self.public_key().to_string(), + ); + + CrossSigningKey { + user_id, + usage: vec![usage], + keys, + signatures: BTreeMap::new(), + } + } + + async fn verify(&self, message: &str, signature: &Signature) -> Result { + let utility = OlmUtility::new(); + utility.ed25519_verify(self.public_key.as_str(), message, signature.as_str()) + } + + async fn sign(&self, message: &str) -> Signature { + Signature(self.inner.lock().await.sign(message)) + } +} + +impl PrivateCrossSigningIdentity { + async fn new(user_id: UserId) -> Self { + let master = Signing::new(); + + let public_key = master.cross_signing_key(user_id.clone(), KeyUsage::Master); + let master = MasterSigning { + inner: master, + public_key: public_key.into(), + }; + + let user = Signing::new(); + let mut public_key = user.cross_signing_key(user_id.clone(), KeyUsage::UserSigning); + master.sign_subkey(&mut public_key).await; + + let user = UserSigning { + inner: user, + public_key: public_key.into(), + }; + + let self_signing = Signing::new(); + let mut public_key = self_signing.cross_signing_key(user_id.clone(), KeyUsage::SelfSigning); + master.sign_subkey(&mut public_key).await; + + let self_signing = SelfSigning { + inner: self_signing, + public_key: public_key.into(), + }; + + Self { + user_id: Arc::new(user_id), + shared: Arc::new(AtomicBool::new(false)), + master_key: Arc::new(Mutex::new(Some(master))), + self_signing_key: Arc::new(Mutex::new(Some(self_signing))), + user_signing_key: Arc::new(Mutex::new(Some(user))), + } + } + + fn mark_as_shared(&self) { + self.shared.store(true, Ordering::SeqCst) + } + + pub fn shared(&self) -> bool { + self.shared.load(Ordering::SeqCst) + } + + async fn pickle(&self, pickle_mode: PicklingMode) -> PickledCrossSigningIdentity { + let pickle_key = if let PicklingMode::Encrypted { key } = &pickle_mode { + key + } else { + "DEFAULT_PICKLE_PASSPHRASE_123456".as_bytes() + } + .to_vec(); + + let master_key = if let Some(m) = self.master_key.lock().await.as_ref() { + Some( + m.pickle(PicklingMode::Encrypted { + key: pickle_key.clone(), + }) + .await, + ) + } else { + None + }; + + let self_signing_key = if let Some(m) = self.self_signing_key.lock().await.as_ref() { + Some( + m.pickle(PicklingMode::Encrypted { + key: pickle_key.clone(), + }) + .await, + ) + } else { + None + }; + + let user_signing_key = if let Some(m) = self.user_signing_key.lock().await.as_ref() { + Some( + m.pickle(PicklingMode::Encrypted { + key: pickle_key.clone(), + }) + .await, + ) + } else { + None + }; + + PickledCrossSigningIdentity { + user_id: self.user_id.as_ref().to_owned(), + shared: self.shared(), + master_key, + self_signing_key, + user_signing_key, + } + } + + async fn from_pickle(pickle: PickledCrossSigningIdentity, pickle_mode: PicklingMode) -> Self { + let pickle_key = if let PicklingMode::Encrypted { key } = &pickle_mode { + key + } else { + "DEFAULT_PICKLE_PASSPHRASE_123456".as_bytes() + } + .to_vec(); + + let master = if let Some(m) = pickle.master_key { + Some(MasterSigning::from_pickle( + m, + PicklingMode::Encrypted { + key: pickle_key.clone(), + }, + )) + } else { + None + }; + + let self_signing = if let Some(s) = pickle.self_signing_key { + Some(SelfSigning::from_pickle( + s, + PicklingMode::Encrypted { + key: pickle_key.clone(), + }, + )) + } else { + None + }; + + let user_signing = if let Some(u) = pickle.user_signing_key { + Some(UserSigning::from_pickle( + u, + PicklingMode::Encrypted { key: pickle_key }, + )) + } else { + None + }; + + Self { + user_id: Arc::new(pickle.user_id), + shared: Arc::new(AtomicBool::from(pickle.shared)), + master_key: Arc::new(Mutex::new(master)), + self_signing_key: Arc::new(Mutex::new(self_signing)), + user_signing_key: Arc::new(Mutex::new(user_signing)), + } + } + + async fn as_upload_request(&self) -> UploadRequest<'_> { + UploadRequest { + auth: None, + master_key: self + .master_key + .lock() + .await + .as_ref() + .cloned() + .map(|k| k.public_key.into()), + user_signing_key: self + .user_signing_key + .lock() + .await + .as_ref() + .cloned() + .map(|k| k.public_key.into()), + self_signing_key: self + .self_signing_key + .lock() + .await + .as_ref() + .cloned() + .map(|k| k.public_key.into()), + } + } +} + +#[cfg(test)] +mod test { + use super::{PrivateCrossSigningIdentity, Signing}; + + use matrix_sdk_common::identifiers::{user_id, UserId}; + use matrix_sdk_test::async_test; + use olm_rs::PicklingMode; + + fn user_id() -> UserId { + user_id!("@example:localhost") + } + + #[test] + fn signing_creation() { + let signing = Signing::new(); + assert!(!signing.public_key().as_str().is_empty()); + } + + #[async_test] + async fn signature_verification() { + let signing = Signing::new(); + + let message = "Hello world"; + + let signature = signing.sign(message).await; + assert!(signing.verify(message, &signature).await.is_ok()); + } + + #[async_test] + async fn pickling_signing() { + let signing = Signing::new(); + let pickled = signing.pickle(PicklingMode::Unencrypted).await; + + let unpickled = Signing::from_pickle(pickled, PicklingMode::Unencrypted); + + assert_eq!(signing.public_key(), unpickled.public_key()); + } + + #[async_test] + async fn private_identity_creation() { + let identity = PrivateCrossSigningIdentity::new(user_id()).await; + + assert!(identity + .master_key + .lock() + .await + .as_ref() + .unwrap() + .public_key + .verify_subkey( + &identity + .self_signing_key + .lock() + .await + .as_ref() + .unwrap() + .public_key, + ) + .is_ok()); + + assert!(identity + .master_key + .lock() + .await + .as_ref() + .unwrap() + .public_key + .verify_subkey( + &identity + .user_signing_key + .lock() + .await + .as_ref() + .unwrap() + .public_key, + ) + .is_ok()); + } + + #[async_test] + async fn identity_pickling() { + let identity = PrivateCrossSigningIdentity::new(user_id()).await; + + let pickled = identity.pickle(PicklingMode::Unencrypted).await; + + let unpickled = + PrivateCrossSigningIdentity::from_pickle(pickled, PicklingMode::Unencrypted).await; + + assert_eq!(identity.user_id, unpickled.user_id); + assert_eq!( + &*identity.master_key.lock().await, + &*unpickled.master_key.lock().await + ); + assert_eq!( + &*identity.user_signing_key.lock().await, + &*unpickled.user_signing_key.lock().await + ); + assert_eq!( + &*identity.self_signing_key.lock().await, + &*unpickled.self_signing_key.lock().await + ); + } +} diff --git a/matrix_sdk_test/Cargo.toml b/matrix_sdk_test/Cargo.toml index 108ad599..b6a7bcb9 100644 --- a/matrix_sdk_test/Cargo.toml +++ b/matrix_sdk_test/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/matrix-org/matrix-rust-sdk" version = "0.1.0" [dependencies] -serde_json = "1.0.58" +serde_json = "1.0.59" http = "0.2.1" matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" } matrix-sdk-test-macros = { version = "0.1.0", path = "../matrix_sdk_test_macros" }