From 72f408c9eac0f1e2a95e907c2627b2d611437df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 25 Dec 2023 17:29:57 +0100 Subject: [PATCH 1/5] Add a struct for unpublished one-time keys --- src/olm/account/mod.rs | 22 +++++++++++++++------- src/olm/mod.rs | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/olm/account/mod.rs b/src/olm/account/mod.rs index 673c3340..bfcf3ad5 100644 --- a/src/olm/account/mod.rs +++ b/src/olm/account/mod.rs @@ -86,6 +86,10 @@ pub struct InboundCreationResult { pub plaintext: Vec, } +pub struct UnpublishedKeys { + pub curve25519: HashMap, +} + /// An Olm account manages all cryptographic keys used on a device. pub struct Account { /// A permanent Ed25519 key used for signing. Also known as the fingerprint @@ -293,12 +297,16 @@ impl Account { /// /// The one-time keys should be published to a server and marked as /// published using the `mark_keys_as_published()` method. - pub fn one_time_keys(&self) -> HashMap { - self.one_time_keys + pub fn one_time_keys(&self) -> UnpublishedKeys { + let curve25519 = self.one_time_keys .unpublished_public_keys .iter() .map(|(key_id, key)| (*key_id, *key)) - .collect() + .collect(); + + UnpublishedKeys { + curve25519 + } } /// Generate a single new fallback key. @@ -766,7 +774,7 @@ mod test { let mut alice_session = alice.create_outbound_session( SessionConfig::version_2(), bob.curve25519_key(), - *bob.one_time_keys() + *bob.one_time_keys().curve25519 .iter() .next() .context("Failed getting bob's OTK, which should never happen here.")? @@ -823,7 +831,7 @@ mod test { bob.generate_one_time_keys(1); let one_time_key = - bob.one_time_keys().values().next().cloned().expect("Didn't find a valid one-time key"); + bob.one_time_keys().curve25519.values().next().cloned().expect("Didn't find a valid one-time key"); let alice_session = alice.create_outbound_session( &bob.curve25519_key().to_base64(), @@ -931,7 +939,7 @@ mod test { let mut olm_one_time_keys: Vec<_> = olm.parsed_one_time_keys().curve25519().values().map(|k| k.to_owned()).collect(); let mut one_time_keys: Vec<_> = - unpickled.one_time_keys().values().map(|k| k.to_base64()).collect(); + unpickled.one_time_keys().curve25519.values().map(|k| k.to_base64()).collect(); // We generated 10 one-time keys on the libolm side, we expect the next key id // to be 11. @@ -986,7 +994,7 @@ mod test { let mut session = malory.create_outbound_session( SessionConfig::default(), alice.curve25519_key(), - *alice.one_time_keys().values().next().expect("Should have one-time key"), + *alice.one_time_keys().curve25519.values().next().expect("Should have one-time key"), ); let message = session.encrypt("Test"); diff --git a/src/olm/mod.rs b/src/olm/mod.rs index 16e68ca5..47ccb724 100644 --- a/src/olm/mod.rs +++ b/src/olm/mod.rs @@ -57,7 +57,7 @@ //! let mut bob = Account::new(); //! //! bob.generate_one_time_keys(1); -//! let bob_otk = *bob.one_time_keys().values().next().unwrap(); +//! let bob_otk = *bob.one_time_keys().curve25519.values().next().unwrap(); //! //! let mut alice_session = alice //! .create_outbound_session(SessionConfig::version_2(), bob.curve25519_key(), bob_otk); From 8f9cbdff2d7812c16d8ce5edbd0d8e907a97f9c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 8 Jan 2024 19:18:57 +0100 Subject: [PATCH 2/5] WIP PQXDH support --- Cargo.toml | 24 +- src/lib.rs | 2 +- src/olm/account/mod.rs | 514 +++++++++++++----- src/olm/account/one_time_keys.rs | 96 +++- src/olm/messages/mod.rs | 17 +- src/olm/messages/pre_key.rs | 38 +- src/olm/session/double_ratchet.rs | 21 +- src/olm/session/mod.rs | 128 ++++- src/olm/session/receiver_chain.rs | 11 +- src/olm/session_config.rs | 67 ++- .../3dh.rs} | 6 +- src/olm/shared_secret/mod.rs | 20 + src/olm/shared_secret/pqxdh.rs | 225 ++++++++ src/types/kyber.rs | 241 ++++++++ src/types/mod.rs | 5 +- 15 files changed, 1206 insertions(+), 209 deletions(-) rename src/olm/{shared_secret.rs => shared_secret/3dh.rs} (97%) create mode 100644 src/olm/shared_secret/mod.rs create mode 100644 src/olm/shared_secret/pqxdh.rs create mode 100644 src/types/kyber.rs diff --git a/Cargo.toml b/Cargo.toml index 8a59b64c..1b6f6e71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,31 +28,33 @@ low-level-api = [] [dependencies] aes = "0.8.3" arrayvec = { version = "0.7.4", features = ["serde"] } -base64 = "0.21.4" +base64 = "0.21.5" cbc = { version = "0.1.2", features = ["std"] } +chacha20poly1305 = "0.10.1" curve25519-dalek = { version = "4.1.1", default-features = false } ed25519-dalek = { version = "2.0.0", default-features = false, features = ["rand_core", "std", "serde", "hazmat"] } -getrandom = "0.2.10" -hkdf = "0.12.3" +getrandom = "0.2.11" +hkdf = "0.12.4" hmac = "0.12.1" matrix-pickle = { version = "0.1.1" } pkcs7 = "0.4.1" -prost = "0.12.1" +pqc_kyber = { version = "0.7.1", features = ["std", "kyber1024"] } +prost = "0.12.3" rand = "0.8.5" -serde = { version = "1.0.188", features = ["derive"] } +serde = { version = "1.0.193", features = ["derive"] } serde_bytes = "0.11.12" -serde_json = "1.0.107" +serde_json = "1.0.108" sha2 = "0.10.8" subtle = "2.5.0" -thiserror = "1.0.49" +thiserror = "1.0.51" x25519-dalek = { version = "2.0.0", features = ["serde", "reusable_secrets", "static_secrets"] } -zeroize = "1.6.0" +zeroize = "1.7.0" [dev-dependencies] -anyhow = "1.0.75" -assert_matches = "1.5.0" +anyhow = "1.0.76" +assert_matches2 = "0.1.2" olm-rs = "2.2.0" -proptest = "1.3.1" +proptest = "1.4.0" [patch.crates-io] olm-rs = { git = "https://github.com/poljar/olm-rs" } diff --git a/src/lib.rs b/src/lib.rs index 606b4d81..df77212d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,7 +196,7 @@ #![deny( clippy::mem_forget, clippy::unwrap_used, - dead_code, + // dead_code, trivial_casts, trivial_numeric_casts, unsafe_code, diff --git a/src/olm/account/mod.rs b/src/olm/account/mod.rs index bfcf3ad5..090eadfe 100644 --- a/src/olm/account/mod.rs +++ b/src/olm/account/mod.rs @@ -1,4 +1,5 @@ -// Copyright 2021 Damir Jelić, Denis Kasak +// Copyright 2021 Denis Kasak +// Copyright 2021-2024 Damir Jelić // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,19 +26,22 @@ use x25519_dalek::ReusableSecret; pub use self::one_time_keys::OneTimeKeyGenerationResult; use self::{ fallback_keys::FallbackKeys, - one_time_keys::{OneTimeKeys, OneTimeKeysPickle}, + one_time_keys::{KyberKeys, OneTimeKeys, OneTimeKeysPickle}, }; use super::{ - messages::PreKeyMessage, + messages::{PqPreKeyMessage, PreKeyMessage}, session::{DecryptionError, Session}, + session_config::Version, session_keys::SessionKeys, - shared_secret::{RemoteShared3DHSecret, Shared3DHSecret}, + shared_secret::{ + RemoteShared3DHSecret, RemoteSharedPqXDHSecret, Shared3DHSecret, SharedPqXDHSecret, + }, SessionConfig, }; use crate::{ types::{ Curve25519Keypair, Curve25519KeypairPickle, Curve25519PublicKey, Curve25519SecretKey, - Ed25519Keypair, Ed25519KeypairPickle, Ed25519PublicKey, KeyId, + Ed25519Keypair, Ed25519KeypairPickle, Ed25519PublicKey, KeyId, KyberPublicKey, }, utilities::{pickle, unpickle}, Ed25519Signature, PickleError, @@ -77,6 +81,61 @@ pub struct IdentityKeys { pub curve25519: Curve25519PublicKey, } +pub struct Curve25519Keys { + one_time_keys: OneTimeKeys, + /// The ephemeral Curve25519 keys used in lieu of a one-time key as part of + /// the 3DH, in case we run out of those. We keep track of both the current + /// and the previous fallback key in any given moment. + last_resort_keys: FallbackKeys, +} + +impl Curve25519Keys { + fn new() -> Self { + Self { one_time_keys: OneTimeKeys::new(), last_resort_keys: FallbackKeys::new() } + } + + fn find_one_time_key(&self, public_key: &Curve25519PublicKey) -> Option<&Curve25519SecretKey> { + self.one_time_keys + .get_secret_key(public_key) + .or_else(|| self.last_resort_keys.get_secret_key(public_key)) + } + + pub fn generate(&mut self, count: usize) -> OneTimeKeyGenerationResult { + self.one_time_keys.generate(count) + } + + fn remove_one_time_key( + &mut self, + public_key: Curve25519PublicKey, + ) -> Option { + self.one_time_keys.remove_secret_key(&public_key) + } +} + +pub struct Keys { + curve25519: Curve25519Keys, + kyber: KyberKeys, +} + +impl Keys { + fn new() -> Self { + Self { curve25519: Curve25519Keys::new(), kyber: Default::default() } + } + + fn mark_as_published(&mut self) { + self.curve25519.one_time_keys.mark_as_published(); + self.curve25519.last_resort_keys.mark_as_published(); + } + + pub fn kyber(&mut self) -> &mut KyberKeys { + &mut self.kyber + } + + pub fn curve25519(&self) -> &Curve25519Keys { + &self.curve25519 + } +} + /// Return type for the creation of inbound [`Session`] objects. #[derive(Debug)] pub struct InboundCreationResult { @@ -88,6 +147,7 @@ pub struct InboundCreationResult { pub struct UnpublishedKeys { pub curve25519: HashMap, + pub kyber: HashMap, } /// An Olm account manages all cryptographic keys used on a device. @@ -98,12 +158,7 @@ pub struct Account { /// The permanent Curve25519 key used for 3DH. Also known as the sender key /// or the identity key. diffie_hellman_key: Curve25519Keypair, - /// The ephemeral (one-time) Curve25519 keys used as part of the 3DH. - one_time_keys: OneTimeKeys, - /// The ephemeral Curve25519 keys used in lieu of a one-time key as part of - /// the 3DH, in case we run out of those. We keep track of both the current - /// and the previous fallback key in any given moment. - fallback_keys: FallbackKeys, + one_time_keys: Keys, } impl Account { @@ -112,8 +167,7 @@ impl Account { Self { signing_key: Ed25519Keypair::new(), diffie_hellman_key: Curve25519Keypair::new(), - one_time_keys: OneTimeKeys::new(), - fallback_keys: FallbackKeys::new(), + one_time_keys: Keys::new(), } } @@ -157,37 +211,49 @@ impl Account { } /// Create a `Session` with the given identity key and one-time key. - pub fn create_outbound_session( - &self, - session_config: SessionConfig, - identity_key: Curve25519PublicKey, - one_time_key: Curve25519PublicKey, - ) -> Session { + pub fn create_outbound_session(&self, session_config: SessionConfig) -> Session { let rng = thread_rng(); let base_key = ReusableSecret::random_from_rng(rng); let public_base_key = Curve25519PublicKey::from(&base_key); - let shared_secret = Shared3DHSecret::new( - self.diffie_hellman_key.secret_key(), - &base_key, - &identity_key, - &one_time_key, - ); - - let session_keys = SessionKeys { - identity_key: self.curve25519_key(), - base_key: public_base_key, - one_time_key, - }; - - Session::new(session_config, shared_secret, session_keys) - } - - fn find_one_time_key(&self, public_key: &Curve25519PublicKey) -> Option<&Curve25519SecretKey> { - self.one_time_keys - .get_secret_key(public_key) - .or_else(|| self.fallback_keys.get_secret_key(public_key)) + match &session_config.version { + Version::V1(session_keys) | Version::V2(session_keys) => { + let shared_secret = Shared3DHSecret::new( + self.diffie_hellman_key.secret_key(), + &base_key, + &session_keys.remote_identity_key, + &session_keys.one_time_key, + ); + + let session_keys = SessionKeys { + identity_key: self.curve25519_key(), + base_key: public_base_key, + one_time_key: session_keys.one_time_key, + }; + + Session::new(session_config, shared_secret, session_keys) + } + Version::VPQ(session_keys) => { + let shared_secret = SharedPqXDHSecret::new( + &self.diffie_hellman_key.secret_key, + &base_key, + &session_keys.remote_identity_key, + &session_keys.signed_pre_key, + session_keys.one_time_key.as_ref(), + &session_keys.kyber_key, + ); + + // TODO: The semantics of [`SessionKeys`] isn't correct here. + let session_keys = SessionKeys { + identity_key: self.curve25519_key(), + base_key: public_base_key, + one_time_key: session_keys.signed_pre_key, + }; + + Session::new_pq(shared_secret, session_config, session_keys) + } + } } /// Remove a one-time key that has previously been published but not yet @@ -205,11 +271,89 @@ impl Account { self.remove_one_time_key_helper(public_key) } - fn remove_one_time_key_helper( + pub fn create_inbound_session_pq( &mut self, - public_key: Curve25519PublicKey, - ) -> Option { - self.one_time_keys.remove_secret_key(&public_key) + pre_key_message: &PqPreKeyMessage, + ) -> Result { + // Find the matching private part of the OTK that the message claims + // was used to create the session that encrypted it. + let private_otk = pre_key_message + .one_time_key + .map(|public_key| { + self.one_time_keys + .curve25519 + .find_one_time_key(&public_key) + .ok_or(SessionCreationError::MissingOneTimeKey(public_key)) + }) + .transpose()?; + let signed_pre_key = self + .one_time_keys + .curve25519 + .last_resort_keys + .get_secret_key(&pre_key_message.signed_pre_key) + .ok_or(SessionCreationError::MissingOneTimeKey(pre_key_message.signed_pre_key))?; + let kyber_key = + self.one_time_keys + .kyber + .secret_keys() + .get(&pre_key_message.kyber_key_id) + .ok_or(SessionCreationError::MissingOneTimeKey(pre_key_message.signed_pre_key))?; + + // Construct a PQXDH shared secret from the various Curve25519 keys and the + // Kyber ciphertext. + // TODO: Remove the unwrap. + let shared_secret = RemoteSharedPqXDHSecret::new( + self.diffie_hellman_key.secret_key(), + signed_pre_key, + private_otk, + &pre_key_message.identity_key, + &pre_key_message.base_key, + kyber_key, + &pre_key_message.kyber_ciphertext, + ) + .unwrap(); + + // These will be used to uniquely identify the Session. + let session_keys = SessionKeys { + identity_key: pre_key_message.identity_key, + base_key: pre_key_message.base_key, + one_time_key: pre_key_message.signed_pre_key, + }; + + let config = SessionConfig::version_pq( + pre_key_message.identity_key, + pre_key_message.signed_pre_key, + pre_key_message.one_time_key, + kyber_key.public_key(), + pre_key_message.kyber_key_id, + ); + + // Create a Session, AKA a double ratchet, this one will have an + // inactive sending chain until we decide to encrypt a message. + let mut session = Session::new_remote_pq( + config, + shared_secret, + pre_key_message.message.ratchet_key, + session_keys, + ); + + // Decrypt the message to check if the Session is actually valid. + let plaintext = session.decrypt_decoded(&pre_key_message.message)?; + + // We only drop the one-time key now, this is why we can't use a + // one-time key type that takes `self`. If we didn't do this, + // someone could maliciously pretend to use up our one-time key and + // make us drop the private part. Unsuspecting users that actually + // try to use such an one-time key won't be able to commnuicate with + // us. This is strictly worse than the one-time key exhaustion + // scenario. + if let Some(one_time_key) = pre_key_message.one_time_key { + self.one_time_keys.curve25519.remove_one_time_key(one_time_key); + } + + let _ = self.keys().kyber.private_keys.remove(&pre_key_message.kyber_key_id); + + Ok(InboundCreationResult { session, plaintext }) } /// Create a [`Session`] from the given pre-key message and identity key @@ -228,6 +372,8 @@ impl Account { // was used to create the session that encrypted it. let public_otk = pre_key_message.one_time_key(); let private_otk = self + .one_time_keys + .curve25519 .find_one_time_key(&public_otk) .ok_or(SessionCreationError::MissingOneTimeKey(public_otk))?; @@ -247,9 +393,9 @@ impl Account { }; let config = if pre_key_message.message.mac_truncated() { - SessionConfig::version_1() + SessionConfig::version_1(their_identity_key, pre_key_message.one_time_key()) } else { - SessionConfig::version_2() + SessionConfig::version_2(their_identity_key, pre_key_message.one_time_key()) }; // Create a Session, AKA a double ratchet, this one will have an @@ -271,7 +417,7 @@ impl Account { // try to use such an one-time key won't be able to commnuicate with // us. This is strictly worse than the one-time key exhaustion // scenario. - self.remove_one_time_key_helper(pre_key_message.one_time_key()); + self.one_time_keys.curve25519.remove_one_time_key(pre_key_message.one_time_key()); Ok(InboundCreationResult { session, plaintext }) } @@ -285,12 +431,15 @@ impl Account { /// places for one-time keys, If we try to generate new ones while the store /// is completely populated, the oldest one-time keys will get discarded /// to make place for new ones. - pub fn generate_one_time_keys(&mut self, count: usize) -> OneTimeKeyGenerationResult { - self.one_time_keys.generate(count) + pub fn generate_one_time_keys( + &mut self, + count: usize, + ) -> OneTimeKeyGenerationResult { + self.one_time_keys.curve25519.generate(count) } pub fn stored_one_time_key_count(&self) -> usize { - self.one_time_keys.private_keys.len() + self.one_time_keys.curve25519.one_time_keys.private_keys.len() } /// Get the currently unpublished one-time keys. @@ -298,15 +447,28 @@ impl Account { /// The one-time keys should be published to a server and marked as /// published using the `mark_keys_as_published()` method. pub fn one_time_keys(&self) -> UnpublishedKeys { - let curve25519 = self.one_time_keys + let curve25519 = self + .one_time_keys + .curve25519 + .one_time_keys .unpublished_public_keys .iter() .map(|(key_id, key)| (*key_id, *key)) .collect(); - UnpublishedKeys { - curve25519 - } + let kyber = self + .one_time_keys + .kyber + .unpublished_public_keys + .iter() + .map(|(key_id, key)| (*key_id, key.clone())) + .collect(); + + UnpublishedKeys { curve25519, kyber } + } + + pub fn keys(&mut self) -> &mut Keys { + &mut self.one_time_keys } /// Generate a single new fallback key. @@ -318,7 +480,7 @@ impl Account { /// is, the one that will get removed from the [`Account`] when this method /// is called. This return value is mostly useful for logging purposes. pub fn generate_fallback_key(&mut self) -> Option { - self.fallback_keys.generate_fallback_key() + self.one_time_keys.curve25519.last_resort_keys.generate_fallback_key() } /// Get the currently unpublished fallback key. @@ -327,7 +489,8 @@ impl Account { /// it has been successfully published it needs to be marked as published /// using the `mark_keys_as_published()` method as well. pub fn fallback_key(&self) -> HashMap { - let fallback_key = self.fallback_keys.unpublished_fallback_key(); + let fallback_key = + self.one_time_keys.curve25519.last_resort_keys.unpublished_fallback_key(); if let Some(fallback_key) = fallback_key { HashMap::from([(fallback_key.key_id(), fallback_key.public_key())]) @@ -339,24 +502,24 @@ impl Account { /// The `Account` stores at most two private parts of the fallback key. This /// method lets us forget the previously used fallback key. pub fn forget_fallback_key(&mut self) -> bool { - self.fallback_keys.forget_previous_fallback_key().is_some() + self.one_time_keys.curve25519.last_resort_keys.forget_previous_fallback_key().is_some() } /// Mark all currently unpublished one-time and fallback keys as published. pub fn mark_keys_as_published(&mut self) { self.one_time_keys.mark_as_published(); - self.fallback_keys.mark_as_published(); } /// Convert the account into a struct which implements [`serde::Serialize`] /// and [`serde::Deserialize`]. pub fn pickle(&self) -> AccountPickle { - AccountPickle { - signing_key: self.signing_key.clone().into(), - diffie_hellman_key: self.diffie_hellman_key.clone().into(), - one_time_keys: self.one_time_keys.clone().into(), - fallback_keys: self.fallback_keys.clone(), - } + todo!() + // AccountPickle { + // signing_key: self.signing_key.clone().into(), + // diffie_hellman_key: self.diffie_hellman_key.clone().into(), + // one_time_keys: self.one_time_keys.clone().into(), + // fallback_keys: self.fallback_keys.clone(), + // } } /// Restore an [`Account`] from a previously saved [`AccountPickle`]. @@ -474,12 +637,15 @@ impl AccountPickle { impl From for Account { fn from(pickle: AccountPickle) -> Self { - Self { - signing_key: pickle.signing_key.into(), - diffie_hellman_key: pickle.diffie_hellman_key.into(), - one_time_keys: pickle.one_time_keys.into(), - fallback_keys: pickle.fallback_keys, - } + todo!() + // Self { + // signing_key: pickle.signing_key.into(), + // diffie_hellman_key: pickle.diffie_hellman_key.into(), + // one_time_keys: pickle.one_time_keys.into(), + // fallback_keys: pickle.fallback_keys, + // // TODO: Support pickling. + // kyber_keys: Default::default(), + // } } } @@ -493,11 +659,7 @@ mod libolm { one_time_keys::OneTimeKeys, Account, }; - use crate::{ - types::{Curve25519Keypair, Curve25519SecretKey}, - utilities::LibolmEd25519Keypair, - Curve25519PublicKey, Ed25519Keypair, KeyId, - }; + use crate::{types::Curve25519SecretKey, utilities::LibolmEd25519Keypair, KeyId}; #[derive(Debug, Zeroize, Encode, Decode)] #[zeroize(drop)] @@ -594,47 +756,54 @@ mod libolm { impl From<&Account> for Pickle { fn from(account: &Account) -> Self { - let one_time_keys: Vec<_> = account - .one_time_keys - .secret_keys() - .iter() - .filter_map(|(key_id, secret_key)| { - Some(OneTimeKey { - key_id: key_id.0.try_into().ok()?, - published: account.one_time_keys.is_secret_key_published(key_id), - public_key: Curve25519PublicKey::from(secret_key).to_bytes(), - private_key: secret_key.to_bytes(), - }) - }) - .collect(); - - let fallback_keys = FallbackKeysArray { - fallback_key: account - .fallback_keys - .fallback_key - .as_ref() - .and_then(|f| f.try_into().ok()), - previous_fallback_key: account - .fallback_keys - .previous_fallback_key - .as_ref() - .and_then(|f| f.try_into().ok()), - }; - - let next_key_id = account.one_time_keys.next_key_id.try_into().unwrap_or_default(); - - Self { - version: 4, - ed25519_keypair: LibolmEd25519Keypair { - private_key: account.signing_key.expanded_secret_key(), - public_key: account.signing_key.public_key().as_bytes().to_owned(), - }, - public_curve25519_key: account.diffie_hellman_key.public_key().to_bytes(), - private_curve25519_key: account.diffie_hellman_key.secret_key().to_bytes(), - one_time_keys, - fallback_keys, - next_key_id, - } + todo!() + // let one_time_keys: Vec<_> = account + // .one_time_keys + // .secret_keys() + // .iter() + // .filter_map(|(key_id, secret_key)| { + // Some(OneTimeKey { + // key_id: key_id.0.try_into().ok()?, + // published: + // account.one_time_keys.is_secret_key_published(key_id), + // public_key: + // Curve25519PublicKey::from(secret_key).to_bytes(), + // private_key: secret_key.to_bytes(), + // }) + // }) + // .collect(); + // + // let fallback_keys = FallbackKeysArray { + // fallback_key: account + // .fallback_keys + // .fallback_key + // .as_ref() + // .and_then(|f| f.try_into().ok()), + // previous_fallback_key: account + // .fallback_keys + // .previous_fallback_key + // .as_ref() + // .and_then(|f| f.try_into().ok()), + // }; + // + // let next_key_id = + // account.one_time_keys.next_key_id.try_into().unwrap_or_default(); + // + // Self { + // version: 4, + // ed25519_keypair: LibolmEd25519Keypair { + // private_key: account.signing_key.expanded_secret_key(), + // public_key: + // account.signing_key.public_key().as_bytes().to_owned(), + // }, + // public_curve25519_key: + // account.diffie_hellman_key.public_key().to_bytes(), + // private_curve25519_key: + // account.diffie_hellman_key.secret_key().to_bytes(), + // one_time_keys, + // fallback_keys, + // next_key_id, + // } } } @@ -666,17 +835,19 @@ mod libolm { .as_ref() .map(|k| k.into()), }; - - Ok(Self { - signing_key: Ed25519Keypair::from_expanded_key( - &pickle.ed25519_keypair.private_key, - )?, - diffie_hellman_key: Curve25519Keypair::from_secret_key( - &pickle.private_curve25519_key, - ), - one_time_keys, - fallback_keys, - }) + todo!() + + // Ok(Self { + // signing_key: Ed25519Keypair::from_expanded_key( + // &pickle.ed25519_keypair.private_key, + // )?, + // diffie_hellman_key: Curve25519Keypair::from_secret_key( + // &pickle.private_curve25519_key, + // ), + // one_time_keys, + // fallback_keys, + // kyber_keys: Default::default(), + // }) } } } @@ -684,6 +855,7 @@ mod libolm { #[cfg(test)] mod test { use anyhow::{bail, Context, Result}; + use assert_matches2::assert_let; use olm_rs::{account::OlmAccount, session::OlmMessage as LibolmOlmMessage}; use super::{Account, InboundCreationResult, SessionConfig, SessionCreationError}; @@ -721,7 +893,7 @@ mod test { let curve25519_key = PublicKey::from_base64(identity_keys.curve25519())?; let one_time_key = PublicKey::from_base64(&one_time_key)?; let mut alice_session = - alice.create_outbound_session(SessionConfig::version_1(), curve25519_key, one_time_key); + alice.create_outbound_session(SessionConfig::version_1(curve25519_key, one_time_key)); let message = "It's a secret to everybody"; let olm_message: LibolmOlmMessage = alice_session.encrypt(message).into(); @@ -771,15 +943,15 @@ mod test { bob.generate_one_time_keys(1); - let mut alice_session = alice.create_outbound_session( - SessionConfig::version_2(), + let mut alice_session = alice.create_outbound_session(SessionConfig::version_2( bob.curve25519_key(), - *bob.one_time_keys().curve25519 - .iter() + bob.one_time_keys() + .curve25519 + .into_iter() .next() .context("Failed getting bob's OTK, which should never happen here.")? .1, - ); + )); bob.mark_keys_as_published(); @@ -830,8 +1002,13 @@ mod test { bob.generate_one_time_keys(1); - let one_time_key = - bob.one_time_keys().curve25519.values().next().cloned().expect("Didn't find a valid one-time key"); + let one_time_key = bob + .one_time_keys() + .curve25519 + .values() + .next() + .cloned() + .expect("Didn't find a valid one-time key"); let alice_session = alice.create_outbound_session( &bob.curve25519_key().to_base64(), @@ -850,7 +1027,7 @@ mod test { }; assert_eq!(alice_session.session_id(), session.session_id()); - assert!(bob.one_time_keys.private_keys.is_empty()); + assert!(bob.one_time_keys.curve25519.one_time_keys.private_keys.is_empty()); assert_eq!(text.as_bytes(), plaintext); @@ -866,7 +1043,7 @@ mod test { let one_time_key = bob.fallback_key().values().next().cloned().expect("Didn't find a valid fallback key"); - assert!(bob.one_time_keys.private_keys.is_empty()); + assert!(bob.one_time_keys.curve25519.one_time_keys.private_keys.is_empty()); let alice_session = alice.create_outbound_session( &bob.curve25519_key().to_base64(), @@ -884,7 +1061,7 @@ mod test { assert_eq!(m.session_keys(), session.session_keys()); assert_eq!(alice_session.session_id(), session.session_id()); - assert!(bob.fallback_keys.fallback_key.is_some()); + assert!(bob.one_time_keys.curve25519.last_resort_keys.fallback_key.is_some()); assert_eq!(text.as_bytes(), plaintext); } else { @@ -943,7 +1120,7 @@ mod test { // We generated 10 one-time keys on the libolm side, we expect the next key id // to be 11. - assert_eq!(unpickled.one_time_keys.next_key_id, 11); + assert_eq!(unpickled.one_time_keys.curve25519.one_time_keys.next_key_id, 11); olm_one_time_keys.sort(); one_time_keys.sort(); @@ -991,11 +1168,10 @@ mod test { let malory = Account::new(); alice.generate_one_time_keys(1); - let mut session = malory.create_outbound_session( - SessionConfig::default(), + let mut session = malory.create_outbound_session(SessionConfig::version_1( alice.curve25519_key(), *alice.one_time_keys().curve25519.values().next().expect("Should have one-time key"), - ); + )); let message = session.encrypt("Test"); @@ -1015,7 +1191,7 @@ mod test { e => bail!("Expected a decryption error, got {:?}", e), } assert!( - !alice.one_time_keys.private_keys.is_empty(), + !alice.one_time_keys.curve25519.one_time_keys.private_keys.is_empty(), "The one-time key was removed when it shouldn't" ); @@ -1085,4 +1261,58 @@ mod test { Ok(()) } + + #[test] + fn inbound_session_creation_pq() { + let alice = Account::new(); + let mut bob = Account::new(); + + bob.generate_one_time_keys(1); + bob.generate_fallback_key(); + bob.keys().kyber().generate(1); + + let one_time_keys = bob.one_time_keys(); + + let one_time_key = one_time_keys + .curve25519 + .values() + .next() + .cloned() + .expect("Didn't find a valid one-time key"); + + let signed_pre_key = + bob.fallback_key().into_values().next().expect("Didn't find a valid fallback key"); + let (kyber_key_id, kyber_key) = one_time_keys + .kyber + .into_iter() + .next() + .expect("Didn't find a valid keyber one-time key"); + + let session_config = SessionConfig::version_pq( + bob.identity_keys().curve25519, + signed_pre_key, + Some(one_time_key), + kyber_key, + kyber_key_id, + ); + let mut alice_session = alice.create_outbound_session(session_config); + + let text = "It's a secret to everybody"; + let message = alice_session.encrypt(text); + + assert_let!(OlmMessage::PqPreKey(message) = message); + + let InboundCreationResult { mut session, plaintext } = bob + .create_inbound_session_pq(&message) + .expect("We should be able to create a new inbound PQ session"); + + assert_eq!(text.as_bytes(), plaintext.as_slice()); + + let second_message = "Another secret"; + let second_encrypted = session.encrypt(second_message); + + let second_decrypted = alice_session.decrypt(&second_encrypted).expect("We should be able to decrypt the second message"); + + assert_eq!(second_message.as_bytes(), second_decrypted.as_slice()); + } } diff --git a/src/olm/account/one_time_keys.rs b/src/olm/account/one_time_keys.rs index 1ec2a9f2..fe96424a 100644 --- a/src/olm/account/one_time_keys.rs +++ b/src/olm/account/one_time_keys.rs @@ -18,10 +18,96 @@ use serde::{Deserialize, Serialize}; use super::PUBLIC_MAX_ONE_TIME_KEYS; use crate::{ - types::{Curve25519SecretKey, KeyId}, + types::{ + kyber::{KyberKeyPair, KyberSecretKey}, + Curve25519SecretKey, KeyId, KyberPublicKey, + }, Curve25519PublicKey, }; +pub struct KyberKeys { + pub(super) next_key_id: u64, + pub(super) unpublished_public_keys: BTreeMap, + pub(super) private_keys: BTreeMap, +} + +impl Default for KyberKeys { + fn default() -> Self { + Self::new() + } +} + +impl KyberKeys { + const MAX_ONE_TIME_KEYS: usize = 50; + + pub(super) fn new() -> Self { + Self { + next_key_id: Default::default(), + unpublished_public_keys: Default::default(), + private_keys: Default::default(), + } + } + + fn generate_one_time_key(&mut self) -> (KyberPublicKey, Option) { + let key_id = KeyId(self.next_key_id); + let key_pair = KyberKeyPair::new(); + + // If we hit the max number of one-time keys we'd like to keep, first remove one + // before we create a new one. + let removed = if self.private_keys.len() >= Self::MAX_ONE_TIME_KEYS { + if let Some(key_id) = self.private_keys.keys().next().copied() { + self.unpublished_public_keys.remove(&key_id); + + if let Some(private_key) = self.private_keys.remove(&key_id) { + let public_key = private_key.public_key(); + + Some(public_key) + } else { + None + } + } else { + None + } + } else { + None + }; + + let KyberKeyPair { secret_key, public_key } = key_pair; + + self.private_keys.insert(key_id, secret_key); + self.unpublished_public_keys.insert(key_id, public_key.clone()); + + (public_key, removed) + } + + pub(crate) fn secret_keys(&self) -> &BTreeMap { + &self.private_keys + } + + pub(crate) fn is_secret_key_published(&self, key_id: &KeyId) -> bool { + !self.unpublished_public_keys.contains_key(key_id) + } + + pub fn generate(&mut self, count: usize) -> OneTimeKeyGenerationResult { + let mut removed_keys = Vec::new(); + let mut created_keys = Vec::new(); + + for _ in 0..count { + let (created, removed) = self.generate_one_time_key(); + + created_keys.push(created); + + if let Some(removed) = removed { + removed_keys.push(removed); + } + + self.next_key_id = self.next_key_id.wrapping_add(1); + } + + OneTimeKeyGenerationResult { created: created_keys, removed: removed_keys } + } +} + #[derive(Serialize, Deserialize, Clone)] #[serde(from = "OneTimeKeysPickle")] #[serde(into = "OneTimeKeysPickle")] @@ -33,12 +119,12 @@ pub(super) struct OneTimeKeys { } /// The result type for the one-time key generation operation. -pub struct OneTimeKeyGenerationResult { +pub struct OneTimeKeyGenerationResult { /// The public part of the one-time keys that were newly generated. - pub created: Vec, + pub created: Vec, /// The public part of the one-time keys that had to be removed to make /// space for the new ones. - pub removed: Vec, + pub removed: Vec, } impl OneTimeKeys { @@ -126,7 +212,7 @@ impl OneTimeKeys { !self.unpublished_public_keys.contains_key(key_id) } - pub fn generate(&mut self, count: usize) -> OneTimeKeyGenerationResult { + pub fn generate(&mut self, count: usize) -> OneTimeKeyGenerationResult { let mut removed_keys = Vec::new(); let mut created_keys = Vec::new(); diff --git a/src/olm/messages/mod.rs b/src/olm/messages/mod.rs index 33471177..d6ac18e7 100644 --- a/src/olm/messages/mod.rs +++ b/src/olm/messages/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2021 Damir Jelić +// Copyright 2021-2024 Damir Jelić // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ mod message; mod pre_key; pub use message::Message; -pub use pre_key::PreKeyMessage; +pub use pre_key::{PqPreKeyMessage, PreKeyMessage}; use serde::{Deserialize, Serialize}; use crate::DecodeError; @@ -40,6 +40,7 @@ pub enum OlmMessage { /// /// [`Session`]: crate::olm::Session PreKey(PreKeyMessage), + PqPreKey(PqPreKeyMessage), } impl From for OlmMessage { @@ -99,6 +100,7 @@ impl OlmMessage { match self { OlmMessage::Normal(m) => &m.ciphertext, OlmMessage::PreKey(m) => &m.message.ciphertext, + OlmMessage::PqPreKey(m) => &m.message.ciphertext, } } @@ -107,6 +109,7 @@ impl OlmMessage { match self { OlmMessage::Normal(_) => MessageType::Normal, OlmMessage::PreKey(_) => MessageType::PreKey, + OlmMessage::PqPreKey(_) => MessageType::PreKey, } } @@ -118,6 +121,7 @@ impl OlmMessage { match self { OlmMessage::Normal(m) => (message_type.into(), m.to_base64()), OlmMessage::PreKey(m) => (message_type.into(), m.to_base64()), + OlmMessage::PqPreKey(m) => todo!(), } } } @@ -169,6 +173,7 @@ impl From for LibolmMessage { .expect("Can't create a valid libolm message"), OlmMessage::PreKey(m) => LibolmMessage::from_type_and_ciphertext(0, m.to_base64()) .expect("Can't create a valid libolm pre-key message"), + OlmMessage::PqPreKey(_) => todo!(), } } } @@ -176,7 +181,7 @@ impl From for LibolmMessage { #[cfg(test)] mod tests { use anyhow::Result; - use assert_matches::assert_matches; + use assert_matches2::assert_matches; use serde_json::json; use super::*; @@ -218,7 +223,7 @@ mod tests { }); let message: OlmMessage = serde_json::from_value(value.clone())?; - assert_matches!(message, OlmMessage::PreKey(_)); + assert_matches!(&message, OlmMessage::PreKey(_)); let serialized = serde_json::to_value(message)?; assert_eq!(value, serialized, "The serialization cycle isn't a noop"); @@ -229,7 +234,7 @@ mod tests { }); let message: OlmMessage = serde_json::from_value(value.clone())?; - assert_matches!(message, OlmMessage::Normal(_)); + assert_matches!(&message, OlmMessage::Normal(_)); let serialized = serde_json::to_value(message)?; assert_eq!(value, serialized, "The serialization cycle isn't a noop"); @@ -240,7 +245,7 @@ mod tests { #[test] fn from_parts() -> Result<()> { let message = OlmMessage::from_parts(0, PRE_KEY_MESSAGE)?; - assert_matches!(message, OlmMessage::PreKey(_)); + assert_matches!(&message, OlmMessage::PreKey(_)); assert_eq!( message.message_type(), MessageType::PreKey, diff --git a/src/olm/messages/pre_key.rs b/src/olm/messages/pre_key.rs index 409ae9c4..4aa24f9b 100644 --- a/src/olm/messages/pre_key.rs +++ b/src/olm/messages/pre_key.rs @@ -1,4 +1,4 @@ -// Copyright 2021 Damir Jelić +// Copyright 2021-2024 Damir Jelić // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,8 +20,9 @@ use serde::{Deserialize, Serialize}; use super::Message; use crate::{ olm::SessionKeys, + types::kyber::KyberCipherText, utilities::{base64_decode, base64_encode}, - Curve25519PublicKey, DecodeError, + Curve25519PublicKey, DecodeError, KeyId, }; /// An encrypted Olm pre-key message. @@ -231,3 +232,36 @@ struct ProtoBufPreKeyMessage { #[prost(bytes, tag = "4")] message: Vec, } + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PqPreKeyMessage { + pub(crate) identity_key: Curve25519PublicKey, + pub(crate) base_key: Curve25519PublicKey, + pub(crate) one_time_key: Option, + pub(crate) signed_pre_key: Curve25519PublicKey, + pub(crate) kyber_ciphertext: KyberCipherText, + pub(crate) kyber_key_id: KeyId, + pub(crate) message: Message, +} + +impl PqPreKeyMessage { + pub fn new( + identity_key: Curve25519PublicKey, + base_key: Curve25519PublicKey, + one_time_key: Option, + signed_pre_key: Curve25519PublicKey, + kyber_ciphertext: KyberCipherText, + kyber_key_id: KeyId, + message: Message, + ) -> Self { + Self { + identity_key, + base_key, + one_time_key, + signed_pre_key, + kyber_ciphertext, + kyber_key_id, + message, + } + } +} diff --git a/src/olm/session/double_ratchet.rs b/src/olm/session/double_ratchet.rs index d76820dc..7d090e8e 100644 --- a/src/olm/session/double_ratchet.rs +++ b/src/olm/session/double_ratchet.rs @@ -1,4 +1,4 @@ -// Copyright 2021 Damir Jelić +// Copyright 2021-2024 Damir Jelić // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,10 @@ use super::{ receiver_chain::ReceiverChain, root_key::{RemoteRootKey, RootKey}, }; -use crate::olm::{messages::Message, shared_secret::Shared3DHSecret}; +use crate::olm::{ + messages::Message, + shared_secret::{Shared3DHSecret, SharedPqXDHSecret}, +}; #[derive(Serialize, Deserialize, Clone)] #[serde(transparent)] @@ -59,6 +62,20 @@ impl DoubleRatchet { self.next_message_key().encrypt_truncated_mac(plaintext) } + pub fn active_pq(shared_secret: &SharedPqXDHSecret) -> Self { + let (root_key, chain_key) = shared_secret.expand(); + + let root_key = RootKey::new(root_key); + let chain_key = ChainKey::new(chain_key); + + let ratchet = ActiveDoubleRatchet { + active_ratchet: Ratchet::new(root_key), + symmetric_key_ratchet: chain_key, + }; + + Self { inner: ratchet.into() } + } + pub fn active(shared_secret: Shared3DHSecret) -> Self { let (root_key, chain_key) = shared_secret.expand(); diff --git a/src/olm/session/mod.rs b/src/olm/session/mod.rs index 8d74c953..ebd4de77 100644 --- a/src/olm/session/mod.rs +++ b/src/olm/session/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2021 Damir Jelić +// Copyright 2021-2024 Damir Jelić // Copyright 2021 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -37,13 +37,16 @@ use zeroize::Zeroize; use super::{ session_config::Version, session_keys::SessionKeys, - shared_secret::{RemoteShared3DHSecret, Shared3DHSecret}, + shared_secret::{ + RemoteShared3DHSecret, RemoteSharedPqXDHSecret, Shared3DHSecret, SharedPqXDHSecret, + }, SessionConfig, }; #[cfg(feature = "low-level-api")] use crate::hazmat::olm::MessageKey; use crate::{ - olm::messages::{Message, OlmMessage, PreKeyMessage}, + olm::messages::{Message, OlmMessage, PqPreKeyMessage, PreKeyMessage}, + types::kyber::KyberCipherText, utilities::{pickle, unpickle}, Curve25519PublicKey, PickleError, }; @@ -149,12 +152,18 @@ pub struct Session { session_keys: SessionKeys, sending_ratchet: DoubleRatchet, receiving_chains: ChainStore, + pre_key_info: Option, config: SessionConfig, } +struct PreKeyInfo { + kyber_ciphertext: KyberCipherText, +} + impl Debug for Session { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { session_keys: _, sending_ratchet, receiving_chains, config } = self; + let Self { session_keys: _, pre_key_info: _, sending_ratchet, receiving_chains, config } = + self; f.debug_struct("Session") .field("session_id", &self.session_id()) @@ -174,6 +183,7 @@ impl Session { let local_ratchet = DoubleRatchet::active(shared_secret); Self { + pre_key_info: None, session_keys, sending_ratchet: local_ratchet, receiving_chains: Default::default(), @@ -181,6 +191,55 @@ impl Session { } } + pub(super) fn new_pq( + shared_secret: SharedPqXDHSecret, + config: SessionConfig, + session_keys: SessionKeys, + ) -> Self { + // TODO: As per [spec], we should create and remember some associated data here. This + // associated data should then be used in an AEAD which is keyed by each message key. + // [spec]: https://signal.org/docs/specifications/pqxdh/#sending-the-initial-message + let sending_ratchet = DoubleRatchet::active_pq(&shared_secret); + let kyber_ciphertext = shared_secret.kyber_ciphertext.to_owned(); + let pre_key_info = Some(PreKeyInfo { kyber_ciphertext }); + + Self { + session_keys, + pre_key_info, + sending_ratchet, + receiving_chains: Default::default(), + config, + } + } + + pub(super) fn new_remote_pq( + config: SessionConfig, + shared_secret: RemoteSharedPqXDHSecret, + remote_ratchet_key: Curve25519PublicKey, + session_keys: SessionKeys, + ) -> Self { + let (root_key, remote_chain_key) = shared_secret.expand(); + + // TODO: Again, no associated data is created and no AEAD is used. + let remote_ratchet_key = RemoteRatchetKey::from(remote_ratchet_key); + let root_key = RemoteRootKey::new(root_key); + let remote_chain_key = RemoteChainKey::new(remote_chain_key); + + let local_ratchet = DoubleRatchet::inactive(root_key, remote_ratchet_key); + let remote_ratchet = ReceiverChain::new(remote_ratchet_key, remote_chain_key); + + let mut ratchet_store = ChainStore::new(); + ratchet_store.push(remote_ratchet); + + Self { + session_keys, + sending_ratchet: local_ratchet, + receiving_chains: ratchet_store, + config, + pre_key_info: None, + } + } + pub(super) fn new_remote( config: SessionConfig, shared_secret: RemoteShared3DHSecret, @@ -204,6 +263,7 @@ impl Session { sending_ratchet: local_ratchet, receiving_chains: ratchet_store, config, + pre_key_info: None, } } @@ -230,16 +290,47 @@ impl Session { /// message from the other side. pub fn encrypt(&mut self, plaintext: impl AsRef<[u8]>) -> OlmMessage { let message = match self.config.version { - Version::V1 => self.sending_ratchet.encrypt_truncated_mac(plaintext.as_ref()), - Version::V2 => self.sending_ratchet.encrypt(plaintext.as_ref()), + Version::V1(_) => self.sending_ratchet.encrypt_truncated_mac(plaintext.as_ref()), + Version::V2(_) => self.sending_ratchet.encrypt(plaintext.as_ref()), + // TODO: The PQXDH spec requires the use of an AEAD which the + // [`DoubleRatchet::encrypt()`] method does not use. + Version::VPQ(_) => self.sending_ratchet.encrypt(plaintext.as_ref()), }; if self.has_received_message() { OlmMessage::Normal(message) } else { - let message = PreKeyMessage::new(self.session_keys, message); + match &self.config.version { + Version::V1(_) | Version::V2(_) => { + let message = PreKeyMessage::new(self.session_keys, message); + OlmMessage::PreKey(message) + } + Version::VPQ(config) => { + let identity_key = self.session_keys.identity_key; + let base_key = self.session_keys.base_key; + let one_time_key = config.one_time_key; + let signed_pre_key = config.signed_pre_key; + let kyber_ciphertext = self + .pre_key_info + .as_ref() + .expect("We should have a pre-key info if we didn't yet receive a message") + .kyber_ciphertext + .to_owned(); + let kyber_id = config.kyber_key_id; + + let message = PqPreKeyMessage::new( + identity_key, + base_key, + one_time_key, + signed_pre_key, + kyber_ciphertext, + kyber_id, + message, + ); - OlmMessage::PreKey(message) + OlmMessage::PqPreKey(message) + } + } } } @@ -248,8 +339,8 @@ impl Session { self.session_keys } - pub fn session_config(&self) -> SessionConfig { - self.config + pub fn session_config(&self) -> &SessionConfig { + &self.config } /// Get the [`MessageKey`] to encrypt the next message. @@ -274,6 +365,7 @@ impl Session { let decrypted = match message { OlmMessage::Normal(m) => self.decrypt_decoded(m)?, OlmMessage::PreKey(m) => self.decrypt_decoded(&m.message)?, + OlmMessage::PqPreKey(m) => self.decrypt_decoded(&m.message)?, }; Ok(decrypted) @@ -306,7 +398,7 @@ impl Session { session_keys: self.session_keys, sending_ratchet: self.sending_ratchet.clone(), receiving_chains: self.receiving_chains.clone(), - config: self.config, + config: self.config.clone(), } } @@ -441,7 +533,8 @@ impl Session { session_keys: pickle.session_keys, sending_ratchet, receiving_chains, - config: SessionConfig::version_1(), + pre_key_info: None, + config: todo!(), }) } else if let Some(chain) = receiving_chains.get(0) { let sending_ratchet = DoubleRatchet::inactive( @@ -453,7 +546,8 @@ impl Session { session_keys: pickle.session_keys, sending_ratchet, receiving_chains, - config: SessionConfig::version_1(), + pre_key_info: None, + config: todo!(), }) } else { Err(crate::LibolmPickleError::InvalidSession) @@ -473,14 +567,9 @@ pub struct SessionPickle { session_keys: SessionKeys, sending_ratchet: DoubleRatchet, receiving_chains: ChainStore, - #[serde(default = "default_config")] config: SessionConfig, } -fn default_config() -> SessionConfig { - SessionConfig::version_1() -} - impl SessionPickle { /// Serialize and encrypt the pickle using the given key. /// @@ -504,6 +593,7 @@ impl From for Session { session_keys: pickle.session_keys, sending_ratchet: pickle.sending_ratchet, receiving_chains: pickle.receiving_chains, + pre_key_info: todo!(), config: pickle.config, } } @@ -542,7 +632,7 @@ mod test { let curve25519_key = Curve25519PublicKey::from_base64(identity_keys.curve25519())?; let one_time_key = Curve25519PublicKey::from_base64(&one_time_key)?; let mut alice_session = - alice.create_outbound_session(SessionConfig::version_1(), curve25519_key, one_time_key); + alice.create_outbound_session(SessionConfig::version_1(curve25519_key, one_time_key)); let message = "It's a secret to everybody"; diff --git a/src/olm/session/receiver_chain.rs b/src/olm/session/receiver_chain.rs index 0f654684..9a5b12f6 100644 --- a/src/olm/session/receiver_chain.rs +++ b/src/olm/session/receiver_chain.rs @@ -1,4 +1,5 @@ -// Copyright 2021 Damir Jelić, Denis Kasak +// Copyright 2021-2024 Damir Jelić +// Copyright 2021 Denis Kasak // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -81,9 +82,11 @@ impl FoundMessageKey<'_> { FoundMessageKey::New(m) => &m.2, }; - match config.version { - Version::V1 => message_key.decrypt_truncated_mac(message), - Version::V2 => message_key.decrypt(message), + match &config.version { + Version::V1(_) => message_key.decrypt_truncated_mac(message), + Version::V2(_) => message_key.decrypt(message), + // TODO: Again we would need an AEAD here to be PQXDH spec compliant. + Version::VPQ(_) => message_key.decrypt(message), } } } diff --git a/src/olm/session_config.rs b/src/olm/session_config.rs index 9310871c..63259e08 100644 --- a/src/olm/session_config.rs +++ b/src/olm/session_config.rs @@ -1,3 +1,4 @@ +// Copyright 2024 Damir Jelić // Copyright 2022 The Matrix.org Foundation C.I.C. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,42 +15,82 @@ use serde::{Deserialize, Serialize}; +use crate::{types::KyberPublicKey, Curve25519PublicKey, KeyId}; + /// A struct to configure how Olm sessions should work under the hood. /// Currently only the MAC truncation behaviour can be configured. -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct SessionConfig { pub(super) version: Version, } -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub(super) enum Version { - V1 = 1, - V2 = 2, + V1(SessionKeysV1), + V2(SessionKeysV1), + VPQ(SessionKeysPQ), +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionKeysV1 { + pub remote_identity_key: Curve25519PublicKey, + pub one_time_key: Curve25519PublicKey, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct SessionKeysPQ { + pub remote_identity_key: Curve25519PublicKey, + pub one_time_key: Option, + pub signed_pre_key: Curve25519PublicKey, + pub kyber_key: KyberPublicKey, + pub kyber_key_id: KeyId, } impl SessionConfig { /// Get the numeric version of this `SessionConfig`. pub fn version(&self) -> u8 { - self.version as u8 + match self.version { + Version::V1(_) => 1, + Version::V2(_) => 2, + Version::VPQ(_) => 3, + } } /// Create a `SessionConfig` for the Olm version 1. This version of Olm will /// use AES-256 and HMAC with a truncated MAC to encrypt individual /// messages. The MAC will be truncated to 8 bytes. - pub fn version_1() -> Self { - SessionConfig { version: Version::V1 } + pub fn version_1( + remote_identity_key: Curve25519PublicKey, + one_time_key: Curve25519PublicKey, + ) -> Self { + SessionConfig { version: Version::V1(SessionKeysV1 { remote_identity_key, one_time_key }) } } /// Create a `SessionConfig` for the Olm version 2. This version of Olm will /// use AES-256 and HMAC to encrypt individual messages. The MAC won't be /// truncated. - pub fn version_2() -> Self { - SessionConfig { version: Version::V2 } + pub fn version_2( + remote_identity_key: Curve25519PublicKey, + one_time_key: Curve25519PublicKey, + ) -> Self { + SessionConfig { version: Version::V2(SessionKeysV1 { remote_identity_key, one_time_key }) } } -} -impl Default for SessionConfig { - fn default() -> Self { - Self::version_2() + pub fn version_pq( + remote_identity_key: Curve25519PublicKey, + signed_pre_key: Curve25519PublicKey, + one_time_key: Option, + kyber_key: KyberPublicKey, + kyber_key_id: KeyId, + ) -> Self { + Self { + version: Version::VPQ(SessionKeysPQ { + remote_identity_key, + one_time_key, + signed_pre_key, + kyber_key, + kyber_key_id, + }), + } } } diff --git a/src/olm/shared_secret.rs b/src/olm/shared_secret/3dh.rs similarity index 97% rename from src/olm/shared_secret.rs rename to src/olm/shared_secret/3dh.rs index 9507dbf4..4bd99c95 100644 --- a/src/olm/shared_secret.rs +++ b/src/olm/shared_secret/3dh.rs @@ -46,7 +46,7 @@ pub struct Shared3DHSecret(Box<[u8; 96]>); #[zeroize(drop)] pub struct RemoteShared3DHSecret(Box<[u8; 96]>); -fn expand(shared_secret: &[u8; 96]) -> (Box<[u8; 32]>, Box<[u8; 32]>) { +pub(super) fn expand(shared_secret: &[u8]) -> (Box<[u8; 32]>, Box<[u8; 32]>) { let hkdf: Hkdf = Hkdf::new(Some(&[0]), shared_secret); let mut root_key = Box::new([0u8; 32]); let mut chain_key = Box::new([0u8; 32]); @@ -93,7 +93,7 @@ impl RemoteShared3DHSecret { } pub fn expand(self) -> (Box<[u8; 32]>, Box<[u8; 32]>) { - expand(&self.0) + expand(self.0.as_slice()) } } @@ -112,7 +112,7 @@ impl Shared3DHSecret { } pub fn expand(self) -> (Box<[u8; 32]>, Box<[u8; 32]>) { - expand(&self.0) + expand(self.0.as_slice()) } } diff --git a/src/olm/shared_secret/mod.rs b/src/olm/shared_secret/mod.rs new file mode 100644 index 00000000..79989d4e --- /dev/null +++ b/src/olm/shared_secret/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024 Damir Jelić +// +// 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. + +mod pqxdh; +#[path = "3dh.rs"] +mod triple_dh; + +pub(crate) use pqxdh::*; +pub use triple_dh::*; diff --git a/src/olm/shared_secret/pqxdh.rs b/src/olm/shared_secret/pqxdh.rs new file mode 100644 index 00000000..14bdec97 --- /dev/null +++ b/src/olm/shared_secret/pqxdh.rs @@ -0,0 +1,225 @@ +// Copyright 2024 Damir Jelić +// +// 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 hkdf::Hkdf; +use sha2::Sha512; +use x25519_dalek::{ReusableSecret, SharedSecret}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::{ + olm::shared_secret::expand, + types::{ + kyber::{KyberCipherText, KyberSecretKey, KyberSharedSecret}, + KyberPublicKey, + }, + Curve25519PublicKey, Curve25519SecretKey, +}; + +const PROTOCOL_NAME: &[u8] = b"OLM_CURVE25519_SHA-512_CRYSTALS-KYBER-1024"; + +#[derive(Zeroize, ZeroizeOnDrop)] +pub(crate) struct SharedPqXDHSecret { + pub secret_key: Box<[u8; 32]>, + #[zeroize(skip)] + pub kyber_ciphertext: KyberCipherText, +} + +impl SharedPqXDHSecret { + pub(crate) fn new( + identity_key: &Curve25519SecretKey, + base_key: &ReusableSecret, + remote_identity_key: &Curve25519PublicKey, + remote_signed_prekey: &Curve25519PublicKey, + remote_one_time_key: Option<&Curve25519PublicKey>, + kyber_key: &KyberPublicKey, + ) -> Self { + let first_secret = identity_key.diffie_hellman(remote_signed_prekey); + let second_secret = base_key.diffie_hellman(&remote_identity_key.inner); + let third_secret = base_key.diffie_hellman(&remote_signed_prekey.inner); + let fourth_secret = remote_one_time_key.map(|otk| base_key.diffie_hellman(&otk.inner)); + + let encapsulation_result = kyber_key.encapsulate(); + let fifth_secret = encapsulation_result.shared_secret; + + let secret_key = + merge_secrets(first_secret, second_secret, third_secret, fourth_secret, fifth_secret); + + Self { secret_key, kyber_ciphertext: encapsulation_result.ciphertext } + } + + pub(crate) fn expand(&self) -> (Box<[u8; 32]>, Box<[u8; 32]>) { + expand(self.secret_key.as_slice()) + } +} + +#[derive(Zeroize, ZeroizeOnDrop)] +pub(crate) struct RemoteSharedPqXDHSecret { + pub secret_key: Box<[u8; 32]>, +} + +impl RemoteSharedPqXDHSecret { + pub(crate) fn new( + identity_key: &Curve25519SecretKey, + signed_prekey: &Curve25519SecretKey, + one_time_key: Option<&Curve25519SecretKey>, + remote_identity_key: &Curve25519PublicKey, + remote_base_key: &Curve25519PublicKey, + kyber_key: &KyberSecretKey, + kyber_ciphertext: &KyberCipherText, + ) -> Result { + let first_secret = signed_prekey.diffie_hellman(remote_identity_key); + let second_secret = identity_key.diffie_hellman(remote_base_key); + let third_secret = signed_prekey.diffie_hellman(remote_base_key); + let fourth_secret = one_time_key.map(|otk| otk.diffie_hellman(remote_base_key)); + + let fifth_secret = kyber_key.decapsulate(kyber_ciphertext)?; + + let secret_key = + merge_secrets(first_secret, second_secret, third_secret, fourth_secret, fifth_secret); + + Ok(Self { secret_key }) + } + + pub(crate) fn expand(&self) -> (Box<[u8; 32]>, Box<[u8; 32]>) { + expand(self.secret_key.as_slice()) + } +} + +fn merge_secrets( + first_secret: SharedSecret, + second_secret: SharedSecret, + third_secret: SharedSecret, + fourth_secret: Option, + fifth_secret: KyberSharedSecret, +) -> Box<[u8; 32]> { + let mut merged_secret = Vec::with_capacity(5 * 32); + + merged_secret.extend_from_slice(&[0xFFu8; 32]); + merged_secret.extend_from_slice(first_secret.as_bytes()); + merged_secret.extend_from_slice(second_secret.as_bytes()); + merged_secret.extend_from_slice(third_secret.as_bytes()); + + if let Some(s) = fourth_secret { + merged_secret.extend_from_slice(s.as_bytes()); + } + + merged_secret.extend_from_slice(fifth_secret.as_bytes()); + + let salt = [0u8; 32]; + + let hkdf: Hkdf = Hkdf::new(Some(&salt), &merged_secret); + let mut secret_key = Box::new([0u8; 32]); + + hkdf.expand(PROTOCOL_NAME, secret_key.as_mut_slice()) + .expect("We should be able to expand the merged PQXDH secrets into a 32 byte secret key"); + + secret_key +} + +#[cfg(test)] +mod test { + use rand::thread_rng; + + use super::*; + + #[test] + fn pqxdh() { + let mut rng = thread_rng(); + + let alice_identity = Curve25519SecretKey::new(); + let alice_base = ReusableSecret::random_from_rng(&mut rng); + + let alice_identity_public = Curve25519PublicKey::from(&alice_identity); + let alice_base_public = Curve25519PublicKey::from(&alice_base); + + let bob_identity = Curve25519SecretKey::new(); + let bob_one_time = Curve25519SecretKey::new(); + let bob_signed_pre_key = Curve25519SecretKey::new(); + let bob_kyber = KyberSecretKey::new(); + + let bob_identity_public = Curve25519PublicKey::from(&bob_identity); + let bob_one_time_public = Curve25519PublicKey::from(&bob_one_time); + let bob_signed_pre_key_public = Curve25519PublicKey::from(&bob_signed_pre_key); + + let shared_secret = SharedPqXDHSecret::new( + &alice_identity, + &alice_base, + &bob_identity_public, + &bob_signed_pre_key_public, + Some(&bob_one_time_public), + &bob_kyber.public_key(), + ); + + let remote_shared_secret = RemoteSharedPqXDHSecret::new( + &bob_identity, + &bob_signed_pre_key, + Some(&bob_one_time), + &alice_identity_public, + &alice_base_public, + &bob_kyber, + &shared_secret.kyber_ciphertext, + ) + .expect("We should be able to create a RemoteSharedPqXDHSecret from our input keys"); + + assert_eq!( + shared_secret.secret_key, remote_shared_secret.secret_key, + "We should have derived the same secret key" + ); + + assert_ne!(&*shared_secret.secret_key, &[0u8; 32]) + } + + #[test] + fn pqxdh_without_one_time_key() { + let mut rng = thread_rng(); + + let alice_identity = Curve25519SecretKey::new(); + let alice_base = ReusableSecret::random_from_rng(&mut rng); + + let alice_identity_public = Curve25519PublicKey::from(&alice_identity); + let alice_base_public = Curve25519PublicKey::from(&alice_base); + + let bob_identity = Curve25519SecretKey::new(); + let bob_signed_pre_key = Curve25519SecretKey::new(); + let bob_kyber = KyberSecretKey::new(); + + let bob_identity_public = Curve25519PublicKey::from(&bob_identity); + let bob_signed_pre_key_public = Curve25519PublicKey::from(&bob_signed_pre_key); + + let shared_secret = SharedPqXDHSecret::new( + &alice_identity, + &alice_base, + &bob_identity_public, + &bob_signed_pre_key_public, + None, + &bob_kyber.public_key(), + ); + + let remote_shared_secret = RemoteSharedPqXDHSecret::new( + &bob_identity, + &bob_signed_pre_key, + None, + &alice_identity_public, + &alice_base_public, + &bob_kyber, + &shared_secret.kyber_ciphertext, + ) + .expect("We should be able to create a RemoteSharedPqXDHSecret from our input keys"); + + assert_eq!( + shared_secret.secret_key, remote_shared_secret.secret_key, + "We should have derived the same secret key" + ); + } +} diff --git a/src/types/kyber.rs b/src/types/kyber.rs new file mode 100644 index 00000000..2466502d --- /dev/null +++ b/src/types/kyber.rs @@ -0,0 +1,241 @@ +// Copyright 2024 Damir Jelić +// +// 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 base64::decoded_len_estimate; +use rand::thread_rng; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use crate::{base64_decode, base64_encode, KeyError}; + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct KyberSharedSecret { + inner: Box, +} + +impl KyberSharedSecret { + pub fn as_bytes(&self) -> &[u8; 32] { + &self.inner + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct KyberCipherText { + inner: [u8; pqc_kyber::KYBER_CIPHERTEXTBYTES], +} + +pub struct EncapsulationResult { + pub(crate) shared_secret: KyberSharedSecret, + pub(crate) ciphertext: KyberCipherText, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct KyberPublicKey { + inner: Box, +} + +impl Serialize for KyberPublicKey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + todo!() + } +} + +impl<'de> Deserialize<'de> for KyberPublicKey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + todo!() + } +} + +impl std::fmt::Debug for KyberPublicKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} + +impl KyberPublicKey { + /// The number of bytes a Kyber public key has. + pub const LENGTH: usize = pqc_kyber::KYBER_PUBLICKEYBYTES; + + const BASE64_LENGTH: usize = 2091; + const PADDED_BASE64_LENGTH: usize = Self::BASE64_LENGTH + 1; + + pub fn encapsulate(&self) -> EncapsulationResult { + let mut rng = thread_rng(); + let mut shared_secret = + KyberSharedSecret { inner: Box::new([0u8; pqc_kyber::KYBER_SSBYTES]) }; + + // TODO: remove this unwrap + let mut result = pqc_kyber::encapsulate(self.inner.as_slice(), &mut rng).unwrap(); + + shared_secret.inner.copy_from_slice(&result.1); + let ciphertext = KyberCipherText { inner: result.0 }; + + result.zeroize(); + + EncapsulationResult { shared_secret, ciphertext } + } + + pub fn to_base64(&self) -> String { + base64_encode(self.inner.as_slice()) + } + + pub fn fingerprint(&self) -> String { + let sha = Sha256::new(); + let digest = sha.chain_update(self.inner.as_slice()).finalize(); + + base64_encode(digest) + } + + pub fn from_base64(input: &str) -> Result { + if input.len() != Self::BASE64_LENGTH && input.len() != Self::PADDED_BASE64_LENGTH { + Err(crate::KeyError::InvalidKeyLength { + key_type: "Kyber1024", + expected_length: Self::LENGTH, + length: decoded_len_estimate(input.len()), + }) + } else { + let mut bytes = base64_decode(input)?; + let mut key_bytes = [0u8; Self::LENGTH]; + + key_bytes.copy_from_slice(&bytes); + let key = Self::from_bytes(&key_bytes); + + bytes.zeroize(); + key_bytes.zeroize(); + + Ok(key) + } + } + + pub fn as_bytes(&self) -> &[u8; Self::LENGTH] { + &self.inner + } + + pub fn from_bytes(slice: &[u8; Self::LENGTH]) -> Self { + // TODO: Can we just take any random bytes or does a public key need to + // contain some structure? + let mut public_key = Box::new([0u8; Self::LENGTH]); + + public_key.copy_from_slice(slice); + + Self { inner: public_key } + } +} + +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct KyberSecretKey { + inner: Box, +} + +impl KyberSecretKey { + pub fn new() -> Self { + let KyberKeyPair { secret_key, .. } = KyberKeyPair::new(); + + secret_key + } + + pub fn decapsulate(&self, ciphertext: &KyberCipherText) -> Result { + let mut shared_secret = Box::new([0u8; pqc_kyber::KYBER_SSBYTES]); + + // TODO: remove this unwrap + let mut result = pqc_kyber::decapsulate(&ciphertext.inner, self.inner.as_slice()).unwrap(); + shared_secret.copy_from_slice(&result); + + result.zeroize(); + + Ok(KyberSharedSecret { inner: shared_secret }) + } + + pub fn public_key(&self) -> KyberPublicKey { + let public_key = pqc_kyber::public(self.inner.as_slice()); + + KyberPublicKey { inner: Box::new(public_key) } + } + + pub fn fingerprint(&self) -> String { + let sha = Sha256::new(); + let public_key = self.public_key(); + let digest = sha.chain_update(public_key.inner.as_slice()).finalize(); + + base64_encode(digest) + } +} + +pub struct KyberKeyPair { + pub(crate) secret_key: KyberSecretKey, + pub(crate) public_key: KyberPublicKey, +} + +impl KyberKeyPair { + pub fn new() -> Self { + let mut rng = thread_rng(); + + let mut public_key = + KyberPublicKey { inner: Box::new([0; pqc_kyber::KYBER_PUBLICKEYBYTES]) }; + let mut secret_key = + KyberSecretKey { inner: Box::new([0; pqc_kyber::KYBER_SECRETKEYBYTES]) }; + + // This only fails if the RNG fails. + let mut keypair = pqc_kyber::keypair(&mut rng).expect( + "We should be always able to generate enough random bytes for a Kyber keypair.", + ); + + public_key.inner.copy_from_slice(&keypair.public); + secret_key.inner.copy_from_slice(&keypair.secret); + + keypair.secret.zeroize(); + + Self { secret_key, public_key } + } + + pub fn public_key(&self) -> &KyberPublicKey { + &self.public_key + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn encapsulation_roundtrip() { + let alice = KyberKeyPair::new(); + + let result = alice.public_key().encapsulate(); + + let shared_secret = alice.secret_key.decapsulate(&result.ciphertext).unwrap(); + + assert_eq!(shared_secret.inner, result.shared_secret.inner); + } + + #[test] + fn base64_encoding() { + let alice = KyberKeyPair::new(); + + let encoded = alice.public_key().to_base64(); + let decoded = KyberPublicKey::from_base64(&encoded) + .expect("We should be able to decode a Kyber public key"); + + assert_eq!(alice.public_key(), &decoded); + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index 1e1b1e2e..f60b1f54 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,4 +1,5 @@ -// Copyright 2021 Denis Kasak, Damir Jelić +// Copyright 2021 Denis Kasak +// Copyright 2021-2024 Damir Jelić // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +15,7 @@ mod curve25519; mod ed25519; +pub mod kyber; pub(crate) use curve25519::{Curve25519Keypair, Curve25519KeypairPickle}; pub use curve25519::{Curve25519PublicKey, Curve25519SecretKey}; @@ -21,6 +23,7 @@ pub use ed25519::{ Ed25519Keypair, Ed25519KeypairPickle, Ed25519PublicKey, Ed25519SecretKey, Ed25519Signature, SignatureError, }; +pub use kyber::KyberPublicKey; use serde::{Deserialize, Serialize}; use thiserror::Error; pub use x25519_dalek::SharedSecret; From e56fc8f98c187c56657f4657df85d0791e04f895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 8 Jan 2024 19:19:21 +0100 Subject: [PATCH 3/5] Move things into integration tests --- src/olm/account/mod.rs | 54 ----------------------------------------- src/olm/session/mod.rs | 6 ++--- tests/olm.rs | 55 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 57 deletions(-) create mode 100644 tests/olm.rs diff --git a/src/olm/account/mod.rs b/src/olm/account/mod.rs index 090eadfe..95c42e43 100644 --- a/src/olm/account/mod.rs +++ b/src/olm/account/mod.rs @@ -1261,58 +1261,4 @@ mod test { Ok(()) } - - #[test] - fn inbound_session_creation_pq() { - let alice = Account::new(); - let mut bob = Account::new(); - - bob.generate_one_time_keys(1); - bob.generate_fallback_key(); - bob.keys().kyber().generate(1); - - let one_time_keys = bob.one_time_keys(); - - let one_time_key = one_time_keys - .curve25519 - .values() - .next() - .cloned() - .expect("Didn't find a valid one-time key"); - - let signed_pre_key = - bob.fallback_key().into_values().next().expect("Didn't find a valid fallback key"); - let (kyber_key_id, kyber_key) = one_time_keys - .kyber - .into_iter() - .next() - .expect("Didn't find a valid keyber one-time key"); - - let session_config = SessionConfig::version_pq( - bob.identity_keys().curve25519, - signed_pre_key, - Some(one_time_key), - kyber_key, - kyber_key_id, - ); - let mut alice_session = alice.create_outbound_session(session_config); - - let text = "It's a secret to everybody"; - let message = alice_session.encrypt(text); - - assert_let!(OlmMessage::PqPreKey(message) = message); - - let InboundCreationResult { mut session, plaintext } = bob - .create_inbound_session_pq(&message) - .expect("We should be able to create a new inbound PQ session"); - - assert_eq!(text.as_bytes(), plaintext.as_slice()); - - let second_message = "Another secret"; - let second_encrypted = session.encrypt(second_message); - - let second_decrypted = alice_session.decrypt(&second_encrypted).expect("We should be able to decrypt the second message"); - - assert_eq!(second_message.as_bytes(), second_decrypted.as_slice()); - } } diff --git a/src/olm/session/mod.rs b/src/olm/session/mod.rs index ebd4de77..94747da5 100644 --- a/src/olm/session/mod.rs +++ b/src/olm/session/mod.rs @@ -196,9 +196,9 @@ impl Session { config: SessionConfig, session_keys: SessionKeys, ) -> Self { - // TODO: As per [spec], we should create and remember some associated data here. This - // associated data should then be used in an AEAD which is keyed by each message key. - // [spec]: https://signal.org/docs/specifications/pqxdh/#sending-the-initial-message + // TODO: As per [spec], we should create and remember some associated data here. + // This associated data should then be used in an AEAD which is keyed by + // each message key. [spec]: https://signal.org/docs/specifications/pqxdh/#sending-the-initial-message let sending_ratchet = DoubleRatchet::active_pq(&shared_secret); let kyber_ciphertext = shared_secret.kyber_ciphertext.to_owned(); let pre_key_info = Some(PreKeyInfo { kyber_ciphertext }); diff --git a/tests/olm.rs b/tests/olm.rs new file mode 100644 index 00000000..fec47e62 --- /dev/null +++ b/tests/olm.rs @@ -0,0 +1,55 @@ +use assert_matches2::assert_let; +use vodozemac::olm::{Account, InboundCreationResult, OlmMessage, SessionConfig}; + +#[test] +fn inbound_session_creation_post_quantum() { + let alice = Account::new(); + let mut bob = Account::new(); + + bob.generate_one_time_keys(1); + bob.generate_fallback_key(); + bob.keys().kyber().generate(1); + + let one_time_keys = bob.one_time_keys(); + + let one_time_key = one_time_keys + .curve25519 + .values() + .next() + .cloned() + .expect("Didn't find a valid one-time key"); + + let signed_pre_key = + bob.fallback_key().into_values().next().expect("Didn't find a valid fallback key"); + let (kyber_key_id, kyber_key) = + one_time_keys.kyber.into_iter().next().expect("Didn't find a valid keyber one-time key"); + + let session_config = SessionConfig::version_pq( + bob.identity_keys().curve25519, + signed_pre_key, + Some(one_time_key), + kyber_key, + kyber_key_id, + ); + let mut alice_session = alice.create_outbound_session(session_config); + + let text = "It's a secret to everybody"; + let message = alice_session.encrypt(text); + + assert_let!(OlmMessage::PqPreKey(message) = message); + + let InboundCreationResult { mut session, plaintext } = bob + .create_inbound_session_pq(&message) + .expect("We should be able to create a new inbound PQ session"); + + assert_eq!(text.as_bytes(), plaintext.as_slice()); + + let second_message = "Another secret"; + let second_encrypted = session.encrypt(second_message); + + let second_decrypted = alice_session + .decrypt(&second_encrypted) + .expect("We should be able to decrypt the second message"); + + assert_eq!(second_message.as_bytes(), second_decrypted.as_slice()); +} From 5ade4f811b88843dcd74a4afed4d89edefe12592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 30 Jan 2024 10:00:23 +0100 Subject: [PATCH 4/5] Add some comments explaining a except call --- src/types/kyber.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/types/kyber.rs b/src/types/kyber.rs index 2466502d..7fcc8832 100644 --- a/src/types/kyber.rs +++ b/src/types/kyber.rs @@ -84,8 +84,13 @@ impl KyberPublicKey { let mut shared_secret = KyberSharedSecret { inner: Box::new([0u8; pqc_kyber::KYBER_SSBYTES]) }; - // TODO: remove this unwrap - let mut result = pqc_kyber::encapsulate(self.inner.as_slice(), &mut rng).unwrap(); + // The encapsulation only fails if we can't generate enough randomness or if the + // public key has not the correct size, the [`KyberPublicKey`] type + // ensures the correct size and we do tread RNG errors as panics. + let mut result = pqc_kyber::encapsulate(self.inner.as_slice(), &mut rng).expect( + "We should be able to perform the encapsulation operation, the key guaranteed \ + to be the correct size.", + ); shared_secret.inner.copy_from_slice(&result.1); let ciphertext = KyberCipherText { inner: result.0 }; @@ -134,6 +139,12 @@ impl KyberPublicKey { pub fn from_bytes(slice: &[u8; Self::LENGTH]) -> Self { // TODO: Can we just take any random bytes or does a public key need to // contain some structure? + // The public key is a pair (b, a) where a is a polynomial in the ring Rq, while + // b is computed as b=a×s+e, s being the secret key and e is a noise + // polynomial. + // On the other hand, the public key unpacking method inside the kyber codebase + // only requires the key to be the correct size, which we guarantee + // here. let mut public_key = Box::new([0u8; Self::LENGTH]); public_key.copy_from_slice(slice); From cb67385ef53cc644f778d23ec6bb047187672e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 30 Jan 2024 10:00:42 +0100 Subject: [PATCH 5/5] Implement Serialize/Deserialize and Debug for the KyberPublicKey type --- src/types/kyber.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/types/kyber.rs b/src/types/kyber.rs index 7fcc8832..80bf8660 100644 --- a/src/types/kyber.rs +++ b/src/types/kyber.rs @@ -17,6 +17,7 @@ use base64::decoded_len_estimate; use rand::thread_rng; use serde::{Deserialize, Serialize}; +use serde_bytes::{ByteBuf as SerdeByteBuf, Bytes as SerdeBytes}; use sha2::{Digest, Sha256}; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -53,7 +54,8 @@ impl Serialize for KyberPublicKey { where S: serde::Serializer, { - todo!() + let bytes = self.as_bytes(); + SerdeBytes::new(bytes).serialize(serializer) } } @@ -62,13 +64,29 @@ impl<'de> Deserialize<'de> for KyberPublicKey { where D: serde::Deserializer<'de>, { - todo!() + let bytes = ::deserialize(deserializer)?; + let length = bytes.len(); + + let expected_length = Self::LENGTH; + + if length != expected_length { + Err(serde::de::Error::custom(format!( + "Invalid public key length: expected {expected_length} bytes, got {length}" + ))) + } else { + let mut slice = Box::new([0u8; Self::LENGTH]); + slice.copy_from_slice(&bytes); + + let key = Self { inner: slice }; + + Ok(key) + } } } impl std::fmt::Debug for KyberPublicKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - todo!() + write!(f, "{}", self.fingerprint()) } }