Skip to content

Commit

Permalink
feat(olm): Add support to export an Account to a libolm pickle (#111)
Browse files Browse the repository at this point in the history
Co-authored-by: Denis Kasak <[email protected]>
  • Loading branch information
poljar and dkasak authored Aug 4, 2023
1 parent cedc9a0 commit e3b6585
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 13 deletions.
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
186 changes: 181 additions & 5 deletions src/olm/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,47 @@ impl Account {
unpickle_libolm::<Pickle, _>(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 encrypted 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 both an
/// AES key and an IV in a deterministic manner. If the same pickle key
/// is reused, this will lead to IV reuse. To prevent this, users have
/// to ensure that they always use a globally (probabilistically) unique
/// pickle key.
///
/// [`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<String, crate::LibolmPickleError> {
use self::libolm::Pickle;
use crate::utilities::pickle_libolm;
pickle_libolm::<Pickle>(self.into(), pickle_key)
}

#[cfg(all(any(fuzzing, test), feature = "libolm-compat"))]
pub fn from_decrypted_libolm_pickle(pickle: &[u8]) -> Result<Self, crate::LibolmPickleError> {
use std::io::Cursor;
Expand Down Expand Up @@ -436,7 +477,7 @@ impl From<AccountPickle> 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::{
Expand All @@ -447,10 +488,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,
Expand Down Expand Up @@ -495,7 +536,30 @@ mod libolm {
}
}

#[derive(Zeroize, Decode)]
impl Encode for FallbackKeysArray {
fn encode(&self, writer: &mut impl std::io::Write) -> Result<usize, EncodeError> {
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,
Expand All @@ -507,6 +571,65 @@ mod libolm {
next_key_id: u32,
}

impl TryFrom<&FallbackKey> for OneTimeKey {
type Error = ();

fn try_from(key: &FallbackKey) -> Result<Self, ()> {
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(),
})
}
}

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,
}
}
}

impl TryFrom<Pickle> for Account {
type Error = crate::LibolmPickleError;

Expand Down Expand Up @@ -562,7 +685,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];
Expand Down Expand Up @@ -901,4 +1024,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(())
}
}
8 changes: 8 additions & 0 deletions src/olm/account/one_time_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ impl OneTimeKeys {
self.insert_secret_key(key_id, key, false)
}

pub(crate) fn secret_keys(&self) -> &BTreeMap<KeyId, Curve25519SecretKey> {
&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();
Expand Down
11 changes: 9 additions & 2 deletions src/types/curve25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use matrix_pickle::{Decode, DecodeError};
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use x25519_dalek::{EphemeralSecret, PublicKey, ReusableSecret, SharedSecret, StaticSecret};
use zeroize::Zeroize;

use super::KeyError;
use crate::utilities::{base64_decode, base64_encode};
Expand Down Expand Up @@ -53,8 +54,14 @@ impl Curve25519SecretKey {
///
/// **Note**: This creates a copy of the key which won't be zeroized, the
/// caller of the method needs to make sure to zeroize the returned array.
pub fn to_bytes(&self) -> [u8; 32] {
self.0.to_bytes()
pub fn to_bytes(&self) -> Box<[u8; 32]> {
let mut key = Box::new([0u8; 32]);
let mut bytes = self.0.to_bytes();
key.copy_from_slice(&bytes);

bytes.zeroize();

key
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/types/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 33 additions & 3 deletions src/utilities/libolm_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -65,10 +65,40 @@ pub(crate) fn unpickle_libolm<P: Decode, T: TryFrom<P, Error = LibolmPickleError
}
}

#[derive(Zeroize, Decode)]
pub(crate) fn pickle_libolm<P>(pickle: P, pickle_key: &[u8]) -> Result<String, LibolmPickleError>
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);
}
}
5 changes: 2 additions & 3 deletions src/utilities/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -136,8 +136,7 @@ impl VarInt for u32 {
impl VarInt for u64 {
#[inline]
fn to_var_int(self) -> Vec<u8> {
let mut v = Vec::new();
v.resize(required_encoded_space_unsigned(self), 0);
let mut v = vec![0u8; required_encoded_space_unsigned(self)];

let mut n = self;
let mut i = 0;
Expand Down

0 comments on commit e3b6585

Please sign in to comment.