diff --git a/src/lib.rs b/src/lib.rs index 2a4e953a..539a6ca2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -266,6 +266,9 @@ pub enum LibolmPickleError { /// The payload of the pickle could not be decoded. #[error(transparent)] Decode(#[from] matrix_pickle::DecodeError), + /// The object could not be encoded as a pickle. + #[error(transparent)] + Encode(#[from] matrix_pickle::EncodeError), } /// Error type describing the different ways message decoding can fail. diff --git a/src/olm/account/mod.rs b/src/olm/account/mod.rs index 4257def6..5f5a45d7 100644 --- a/src/olm/account/mod.rs +++ b/src/olm/account/mod.rs @@ -373,6 +373,45 @@ impl Account { unpickle_libolm::(pickle, pickle_key, PICKLE_VERSION) } + /// Pickle an [`Account`] into a libolm pickle format. + /// + /// This pickle can be restored using the `[Account::from_libolm_pickle]` + /// method, or can be used in the [`libolm`] C library. + /// + /// The pickle will be encryptd using the pickle key. + /// + /// *Note*: This method might be lossy, the vodozemac [`Account`] has the + /// ability to hold more one-time keys compared to the [`libolm`] + /// variant. + /// + /// ⚠️ *Security warning*: The pickle key will get expanded into a AES key + /// and IV in a deterministic manner, this might lead to IV reuse if the + /// same pickle key is used multiple times. + /// + /// [`libolm`]: https://gitlab.matrix.org/matrix-org/olm/ + /// + /// # Examples + /// ``` + /// use vodozemac::olm::Account; + /// use olm_rs::{account::OlmAccount, PicklingMode}; + /// let account = Account::new(); + /// + /// let export = account + /// .to_libolm_pickle(&[0u8; 32]) + /// .expect("We should be able to pickle a freshly created Account"); + /// + /// let unpickled = OlmAccount::unpickle( + /// export, + /// PicklingMode::Encrypted { key: [0u8; 32].to_vec() }, + /// ).expect("We should be able to unpickle our exported Account"); + /// ``` + #[cfg(feature = "libolm-compat")] + pub fn to_libolm_pickle(&self, pickle_key: &[u8]) -> Result { + use self::libolm::Pickle; + use crate::utilities::pickle_libolm; + pickle_libolm::(self.into(), pickle_key) + } + #[cfg(all(any(fuzzing, test), feature = "libolm-compat"))] pub fn from_decrypted_libolm_pickle(pickle: &[u8]) -> Result { use std::io::Cursor; @@ -436,7 +475,7 @@ impl From for Account { #[cfg(feature = "libolm-compat")] mod libolm { - use matrix_pickle::{Decode, DecodeError}; + use matrix_pickle::{Decode, DecodeError, Encode, EncodeError}; use zeroize::Zeroize; use super::{ @@ -447,10 +486,10 @@ mod libolm { use crate::{ types::{Curve25519Keypair, Curve25519SecretKey}, utilities::LibolmEd25519Keypair, - Ed25519Keypair, KeyId, + Curve25519PublicKey, Ed25519Keypair, KeyId, }; - #[derive(Debug, Zeroize, Decode)] + #[derive(Debug, Zeroize, Encode, Decode)] #[zeroize(drop)] struct OneTimeKey { key_id: u32, @@ -495,7 +534,30 @@ mod libolm { } } - #[derive(Zeroize, Decode)] + impl Encode for FallbackKeysArray { + fn encode(&self, writer: &mut impl std::io::Write) -> Result { + let ret = match (&self.fallback_key, &self.previous_fallback_key) { + (None, None) => 0u8.encode(writer)?, + (Some(key), None) | (None, Some(key)) => { + let mut ret = 1u8.encode(writer)?; + ret += key.encode(writer)?; + + ret + } + (Some(key), Some(previous_key)) => { + let mut ret = 2u8.encode(writer)?; + ret += key.encode(writer)?; + ret += previous_key.encode(writer)?; + + ret + } + }; + + Ok(ret) + } + } + + #[derive(Zeroize, Encode, Decode)] #[zeroize(drop)] pub(super) struct Pickle { version: u32, @@ -507,6 +569,65 @@ mod libolm { next_key_id: u32, } + impl TryFrom<&FallbackKey> for OneTimeKey { + type Error = (); + + fn try_from(key: &FallbackKey) -> Result { + Ok(OneTimeKey { + key_id: key.key_id.0.try_into().map_err(|_| ())?, + published: key.published(), + public_key: key.public_key().to_bytes(), + private_key: key.secret_key().to_bytes().into(), + }) + } + } + + 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().into(), + }) + }) + .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().into(), + one_time_keys, + fallback_keys, + next_key_id, + } + } + } + impl TryFrom for Account { type Error = crate::LibolmPickleError; @@ -562,7 +683,7 @@ mod test { messages::{OlmMessage, PreKeyMessage}, AccountPickle, }, - run_corpus, Curve25519PublicKey as PublicKey, + run_corpus, Curve25519PublicKey as PublicKey, Ed25519Signature, }; const PICKLE_KEY: [u8; 32] = [0u8; 32]; @@ -901,4 +1022,57 @@ mod test { let _ = Account::from_decrypted_libolm_pickle(data); }); } + + #[test] + fn libolm_pickle_cycle() -> Result<()> { + let message = "It's a secret to everybody"; + + let olm = OlmAccount::new(); + olm.generate_one_time_keys(10); + olm.generate_fallback_key(); + + let olm_signature = olm.sign(message); + + let key = b"DEFAULT_PICKLE_KEY"; + let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() }); + + let account = Account::from_libolm_pickle(&pickle, key).unwrap(); + let vodozemac_pickle = account.to_libolm_pickle(key).unwrap(); + let _ = Account::from_libolm_pickle(&vodozemac_pickle, key).unwrap(); + + let vodozemac_signature = account.sign(message); + let olm_signature = Ed25519Signature::from_base64(&olm_signature) + .expect("We should be able to parse a signature produced by libolm"); + account + .identity_keys() + .ed25519 + .verify(message.as_bytes(), &olm_signature) + .expect("We should be able to verify the libolm signature with our vodozemac Account"); + + let unpickled = OlmAccount::unpickle( + vodozemac_pickle, + olm_rs::PicklingMode::Encrypted { key: key.to_vec() }, + ) + .unwrap(); + + let utility = olm_rs::utility::OlmUtility::new(); + utility + .ed25519_verify( + unpickled.parsed_identity_keys().ed25519(), + message, + vodozemac_signature.to_base64(), + ) + .expect("We should be able to verify the signature vodozemac created"); + utility + .ed25519_verify( + unpickled.parsed_identity_keys().ed25519(), + message, + olm_signature.to_base64(), + ) + .expect("We should be able to verify the original signature from libolm"); + + assert_eq!(olm.parsed_identity_keys(), unpickled.parsed_identity_keys()); + + Ok(()) + } } diff --git a/src/olm/account/one_time_keys.rs b/src/olm/account/one_time_keys.rs index 17d0d6f4..1ec2a9f2 100644 --- a/src/olm/account/one_time_keys.rs +++ b/src/olm/account/one_time_keys.rs @@ -118,6 +118,14 @@ impl OneTimeKeys { self.insert_secret_key(key_id, key, false) } + 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(); diff --git a/src/types/ed25519.rs b/src/types/ed25519.rs index d6d746a4..e67342ff 100644 --- a/src/types/ed25519.rs +++ b/src/types/ed25519.rs @@ -148,6 +148,24 @@ impl Ed25519Keypair { Ok(Self { secret_key: secret_key.into(), public_key }) } + #[cfg(feature = "libolm-compat")] + pub(crate) fn expanded_secret_key(&self) -> Box<[u8; 64]> { + use sha2::Digest; + + let mut expanded = Box::new([0u8; 64]); + + match &self.secret_key { + SecretKeys::Normal(k) => { + let mut k = k.to_bytes(); + Sha512::new().chain_update(k).finalize_into(expanded.as_mut_slice().into()); + k.zeroize(); + } + SecretKeys::Expanded(k) => expanded.copy_from_slice(k.as_bytes()), + } + + expanded + } + /// Get the public Ed25519 key of this keypair. pub fn public_key(&self) -> Ed25519PublicKey { self.public_key diff --git a/src/utilities/libolm_compat.rs b/src/utilities/libolm_compat.rs index 5f91ca6f..23128a32 100644 --- a/src/utilities/libolm_compat.rs +++ b/src/utilities/libolm_compat.rs @@ -14,10 +14,10 @@ use std::io::Cursor; -use matrix_pickle::Decode; +use matrix_pickle::{Decode, Encode}; use zeroize::Zeroize; -use super::base64_decode; +use super::{base64_decode, base64_encode}; use crate::{cipher::Cipher, LibolmPickleError}; /// Decrypt and decode the given pickle with the given pickle key. @@ -65,10 +65,40 @@ pub(crate) fn unpickle_libolm(pickle: P, pickle_key: &[u8]) -> Result +where + P: Encode, +{ + let mut encoded = pickle.encode_to_vec()?; + + let cipher = Cipher::new_pickle(pickle_key); + let encrypted = cipher.encrypt_pickle(&encoded); + encoded.zeroize(); + + Ok(base64_encode(encrypted)) +} + +#[derive(Zeroize, Encode, Decode)] #[zeroize(drop)] pub(crate) struct LibolmEd25519Keypair { pub public_key: [u8; 32], #[secret] pub private_key: Box<[u8; 64]>, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn encode_cycle() { + let key_pair = + LibolmEd25519Keypair { public_key: [10u8; 32], private_key: [20u8; 64].into() }; + + let encoded = key_pair.encode_to_vec().unwrap(); + let decoded = LibolmEd25519Keypair::decode_from_slice(&encoded).unwrap(); + + assert_eq!(key_pair.public_key, decoded.public_key); + assert_eq!(key_pair.private_key, decoded.private_key); + } +} diff --git a/src/utilities/mod.rs b/src/utilities/mod.rs index 94990cde..55531f9a 100644 --- a/src/utilities/mod.rs +++ b/src/utilities/mod.rs @@ -23,7 +23,7 @@ use base64::{ Engine, }; #[cfg(feature = "libolm-compat")] -pub(crate) use libolm_compat::{unpickle_libolm, LibolmEd25519Keypair}; +pub(crate) use libolm_compat::{pickle_libolm, unpickle_libolm, LibolmEd25519Keypair}; const STANDARD_NO_PAD: GeneralPurpose = GeneralPurpose::new( &alphabet::STANDARD,