diff --git a/Cargo.toml b/Cargo.toml index 74061d10..493595c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,12 +12,12 @@ version = "0.1.0" [features] default = [] -encryption = ["olm-rs", "serde/derive", "serde_json"] +encryption = ["olm-rs", "serde/derive", "serde_json", "cjson"] [dependencies] js_int = "0.1.2" futures = "0.3.4" -reqwest = "0.10.1" +reqwest = "0.10.2" http = "0.2.0" async-std = "1.5.0" ruma-api = "0.13.0" @@ -30,7 +30,8 @@ url = "2.1.1" olm-rs = { git = "https://gitlab.gnome.org/jhaye/olm-rs/", optional = true} serde = { version = "1.0.104", optional = true, features = ["derive"] } -serde_json = { version = "*", optional = true } +serde_json = { version = "1.0.48", optional = true } +cjson = { version = "0.1.0", optional = true } [dev-dependencies] tokio = { version = "0.2.11", features = ["full"] } diff --git a/src/crypto/error.rs b/src/crypto/error.rs new file mode 100644 index 00000000..69d947ae --- /dev/null +++ b/src/crypto/error.rs @@ -0,0 +1,47 @@ +// 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. + +use cjson::Error as CjsonError; +use std::fmt::{Display, Formatter, Result as FmtResult}; + +#[derive(Debug)] +pub enum SignatureError { + NotAnObject, + NoSignatureFound, + CanonicalJsonError(CjsonError), + VerificationError, +} + +impl Display for SignatureError { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + let message = match self { + SignatureError::NotAnObject => "The provided JSON value isn't an object.", + SignatureError::NoSignatureFound => { + "The provided JSON object doesn't contain a signatures field." + } + SignatureError::CanonicalJsonError(_) => { + "The provided JSON object can't be converted to a canonical representation." + } + SignatureError::VerificationError => "The signature didn't match the provided key.", + }; + + write!(f, "{}", message) + } +} + +impl From for SignatureError { + fn from(error: CjsonError) -> Self { + Self::CanonicalJsonError(error) + } +} diff --git a/src/crypto/machine.rs b/src/crypto/machine.rs index d23d770a..59568d6f 100644 --- a/src/crypto/machine.rs +++ b/src/crypto/machine.rs @@ -14,11 +14,17 @@ use std::convert::TryInto; +use super::error::SignatureError; use super::olm::Account; use crate::api; use api::r0::keys; +use cjson; +use olm_rs::utility::OlmUtility; +use serde_json::json; +use serde_json::value::Value; + struct OlmMachine { /// The unique user id that owns this account. user_id: String, @@ -34,6 +40,14 @@ struct OlmMachine { } impl OlmMachine { + const OLM_V1_ALGORITHM: &'static str = "m.olm.v1.curve25519-aes-sha2"; + const MEGOLM_V1_ALGORITHM: &'static str = "m.megolm.v1.aes-sha2"; + + const ALGORITHMS: &'static [&'static str] = &[ + OlmMachine::OLM_V1_ALGORITHM, + OlmMachine::MEGOLM_V1_ALGORITHM, + ]; + /// Create a new account. pub fn new(user_id: &str, device_id: &str) -> Self { OlmMachine { @@ -62,9 +76,9 @@ impl OlmMachine { } } - /// Receive a successfull keys upload response. + /// Receive a successful keys upload response. /// - /// # Arugments + /// # Arguments /// /// `response` - The keys upload response of the request that the client /// performed. @@ -108,7 +122,104 @@ impl OlmMachine { } } - fn device_keys() -> () {} + /// Sign the device keys and return a JSON Value to upload them. + fn device_keys(&self) -> Value { + let identity_keys = self.account.identity_keys(); + + let mut device_keys = json!({ + "user_id": self.user_id, + "device_id": self.device_id, + "algorithms": OlmMachine::ALGORITHMS, + "keys": { + format!("curve25519:{}", self.device_id): identity_keys.curve25519(), + format!("ed25519:{}", self.device_id): identity_keys.ed25519(), + }, + }); + + let signature = json!({ + self.user_id.clone(): { + format!("ed25519:{}", self.device_id): self.sign_json(&device_keys), + } + }); + + let device_keys_object = device_keys + .as_object_mut() + .expect("Device keys json value isn't an object"); + + device_keys_object.insert("signatures".to_string(), signature); + + device_keys + } + + /// Convert a JSON value to the canonical representation and sign the JSON string. + fn sign_json(&self, json: &Value) -> String { + let canonical_json = + cjson::to_string(json).expect(&format!("Can't serialize {} to canonical JSON", json)); + println!("HELLO SIGNING {}", canonical_json); + self.account.sign(&canonical_json) + } + + /// Verify a signed JSON object. + /// + /// The object must have a signatures key associated with an object of the + /// form `user_id: {key_id: signature}`. + /// + /// # Arguments + /// + /// * `user_id` - The user who signed the JSON object. + /// * `device_id` - The device that signed the JSON object. + /// * `user_key` - The public ed25519 key which was used to sign the JSON + /// object. + /// * `json` - The JSON object that should be verified. + /// + /// Returns Ok if the signature was successfully verified, otherwise an + /// SignatureError. + fn verify_json( + &self, + user_id: &str, + device_id: &str, + user_key: &str, + json: &mut Value, + ) -> Result<(), SignatureError> { + let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?; + let unsigned = json_object.remove("unsigned"); + let signatures = json_object.remove("signatures"); + + let canonical_json = cjson::to_string(json_object)?; + + if let Some(u) = unsigned { + json_object.insert("unsigned".to_string(), u); + } + + let key_id = format!("ed25519:{}", device_id); + + let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?; + let signature_object = signatures + .as_object() + .ok_or(SignatureError::NoSignatureFound)?; + let signature = signature_object + .get(user_id) + .ok_or(SignatureError::NoSignatureFound)?; + let signature = signature + .get(key_id) + .ok_or(SignatureError::NoSignatureFound)?; + let signature = signature.as_str().ok_or(SignatureError::NoSignatureFound)?; + + let utility = OlmUtility::new(); + + let ret = if utility + .ed25519_verify(&user_key, &canonical_json, signature) + .is_ok() + { + Ok(()) + } else { + Err(SignatureError::VerificationError) + }; + + json_object.insert("signatures".to_string(), signatures); + + ret + } } #[cfg(test)] @@ -194,4 +305,21 @@ mod test { machine.receive_keys_upload_response(&response).await; assert!(machine.generate_one_time_keys().is_err()); } + + #[test] + fn test_device_key_signing() { + let machine = OlmMachine::new(USER_ID, DEVICE_ID); + + let mut device_keys = machine.device_keys(); + let identity_keys = machine.account.identity_keys(); + let ed25519_key = identity_keys.ed25519(); + + let ret = machine.verify_json( + &machine.user_id, + &machine.device_id, + ed25519_key, + &mut device_keys, + ); + assert!(ret.is_ok()); + } } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index ab8cce3f..cf813c78 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. // TODO remove this. +mod error; #[allow(dead_code)] mod machine; #[allow(dead_code)] diff --git a/src/crypto/olm.rs b/src/crypto/olm.rs index 33617b68..e1a44b54 100644 --- a/src/crypto/olm.rs +++ b/src/crypto/olm.rs @@ -151,6 +151,10 @@ impl Account { pub fn mark_keys_as_published(&self) { self.inner.mark_keys_as_published(); } + + pub fn sign(&self, string: &str) -> String { + self.inner.sign(string) + } } #[cfg(test)]