Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to export an Account to a libolm pickle #111

Merged
merged 4 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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