mod fallback_keys;
mod one_time_keys;
use std::collections::HashMap;
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use x25519_dalek::ReusableSecret;
pub use self::one_time_keys::OneTimeKeyGenerationResult;
use self::{
fallback_keys::FallbackKeys,
one_time_keys::{OneTimeKeys, OneTimeKeysPickle},
};
use super::{
messages::PreKeyMessage,
session::{DecryptionError, Session},
session_keys::SessionKeys,
shared_secret::{RemoteShared3DHSecret, Shared3DHSecret},
SessionConfig,
};
use crate::{
types::{
Curve25519Keypair, Curve25519KeypairPickle, Curve25519PublicKey, Curve25519SecretKey,
Ed25519Keypair, Ed25519KeypairPickle, Ed25519PublicKey, KeyId,
},
utilities::{pickle, unpickle},
Ed25519Signature, PickleError,
};
const PUBLIC_MAX_ONE_TIME_KEYS: usize = 50;
#[derive(Error, Debug)]
pub enum SessionCreationError {
#[error("The pre-key message contained an unknown one-time key: {0}")]
MissingOneTimeKey(Curve25519PublicKey),
#[error(
"The given identity key doesn't match the one in the pre-key message: \
expected {0}, got {1}"
)]
MismatchedIdentityKey(Curve25519PublicKey, Curve25519PublicKey),
#[error("The message that was used to establish the Session couldn't be decrypted")]
Decryption(#[from] DecryptionError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct IdentityKeys {
pub ed25519: Ed25519PublicKey,
pub curve25519: Curve25519PublicKey,
}
#[derive(Debug)]
pub struct InboundCreationResult {
pub session: Session,
pub plaintext: Vec<u8>,
}
pub struct Account {
signing_key: Ed25519Keypair,
diffie_hellman_key: Curve25519Keypair,
one_time_keys: OneTimeKeys,
fallback_keys: FallbackKeys,
}
impl Account {
pub fn new() -> Self {
Self {
signing_key: Ed25519Keypair::new(),
diffie_hellman_key: Curve25519Keypair::new(),
one_time_keys: OneTimeKeys::new(),
fallback_keys: FallbackKeys::new(),
}
}
pub const fn identity_keys(&self) -> IdentityKeys {
IdentityKeys { ed25519: self.ed25519_key(), curve25519: self.curve25519_key() }
}
pub const fn ed25519_key(&self) -> Ed25519PublicKey {
self.signing_key.public_key()
}
pub const fn curve25519_key(&self) -> Curve25519PublicKey {
self.diffie_hellman_key.public_key()
}
pub fn sign(&self, message: impl AsRef<[u8]>) -> Ed25519Signature {
self.signing_key.sign(message.as_ref())
}
pub const fn max_number_of_one_time_keys(&self) -> usize {
PUBLIC_MAX_ONE_TIME_KEYS
}
pub fn create_outbound_session(
&self,
session_config: SessionConfig,
identity_key: Curve25519PublicKey,
one_time_key: Curve25519PublicKey,
) -> 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))
}
#[cfg(feature = "low-level-api")]
pub fn remove_one_time_key(
&mut self,
public_key: Curve25519PublicKey,
) -> Option<Curve25519SecretKey> {
self.remove_one_time_key_helper(public_key)
}
fn remove_one_time_key_helper(
&mut self,
public_key: Curve25519PublicKey,
) -> Option<Curve25519SecretKey> {
self.one_time_keys.remove_secret_key(&public_key)
}
pub fn create_inbound_session(
&mut self,
their_identity_key: Curve25519PublicKey,
pre_key_message: &PreKeyMessage,
) -> Result<InboundCreationResult, SessionCreationError> {
if their_identity_key != pre_key_message.identity_key() {
Err(SessionCreationError::MismatchedIdentityKey(
their_identity_key,
pre_key_message.identity_key(),
))
} else {
let public_otk = pre_key_message.one_time_key();
let private_otk = self
.find_one_time_key(&public_otk)
.ok_or(SessionCreationError::MissingOneTimeKey(public_otk))?;
let shared_secret = RemoteShared3DHSecret::new(
self.diffie_hellman_key.secret_key(),
private_otk,
&pre_key_message.identity_key(),
&pre_key_message.base_key(),
);
let session_keys = SessionKeys {
identity_key: pre_key_message.identity_key(),
base_key: pre_key_message.base_key(),
one_time_key: pre_key_message.one_time_key(),
};
let config = if pre_key_message.message.mac_truncated() {
SessionConfig::version_1()
} else {
SessionConfig::version_2()
};
let mut session = Session::new_remote(
config,
shared_secret,
pre_key_message.message.ratchet_key,
session_keys,
);
let plaintext = session.decrypt_decoded(&pre_key_message.message)?;
self.remove_one_time_key_helper(pre_key_message.one_time_key());
Ok(InboundCreationResult { session, plaintext })
}
}
pub fn generate_one_time_keys(&mut self, count: usize) -> OneTimeKeyGenerationResult {
self.one_time_keys.generate(count)
}
pub fn stored_one_time_key_count(&self) -> usize {
self.one_time_keys.private_keys.len()
}
pub fn one_time_keys(&self) -> HashMap<KeyId, Curve25519PublicKey> {
self.one_time_keys
.unpublished_public_keys
.iter()
.map(|(key_id, key)| (*key_id, *key))
.collect()
}
pub fn generate_fallback_key(&mut self) -> Option<Curve25519PublicKey> {
self.fallback_keys.generate_fallback_key()
}
pub fn fallback_key(&self) -> HashMap<KeyId, Curve25519PublicKey> {
let fallback_key = self.fallback_keys.unpublished_fallback_key();
if let Some(fallback_key) = fallback_key {
HashMap::from([(fallback_key.key_id(), fallback_key.public_key())])
} else {
HashMap::new()
}
}
pub fn forget_fallback_key(&mut self) -> bool {
self.fallback_keys.forget_previous_fallback_key().is_some()
}
pub fn mark_keys_as_published(&mut self) {
self.one_time_keys.mark_as_published();
self.fallback_keys.mark_as_published();
}
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(),
}
}
pub fn from_pickle(pickle: AccountPickle) -> Self {
pickle.into()
}
#[cfg(feature = "libolm-compat")]
pub fn from_libolm_pickle(
pickle: &str,
pickle_key: &[u8],
) -> Result<Self, crate::LibolmPickleError> {
use self::libolm::Pickle;
use crate::utilities::unpickle_libolm;
const PICKLE_VERSION: u32 = 4;
unpickle_libolm::<Pickle, _>(pickle, pickle_key, PICKLE_VERSION)
}
#[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"))]
#[doc(hidden)]
pub fn from_decrypted_libolm_pickle(pickle: &[u8]) -> Result<Self, crate::LibolmPickleError> {
use std::io::Cursor;
use matrix_pickle::Decode;
use self::libolm::Pickle;
let mut cursor = Cursor::new(&pickle);
let pickle = Pickle::decode(&mut cursor)?;
pickle.try_into()
}
}
impl Default for Account {
fn default() -> Self {
Self::new()
}
}
#[derive(Serialize, Deserialize)]
pub struct AccountPickle {
signing_key: Ed25519KeypairPickle,
diffie_hellman_key: Curve25519KeypairPickle,
one_time_keys: OneTimeKeysPickle,
fallback_keys: FallbackKeys,
}
impl AccountPickle {
pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
pickle(&self, pickle_key)
}
pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
unpickle(ciphertext, pickle_key)
}
}
impl From<AccountPickle> 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,
}
}
}
#[cfg(feature = "libolm-compat")]
mod libolm {
use matrix_pickle::{Decode, DecodeError, Encode, EncodeError};
use zeroize::{Zeroize, ZeroizeOnDrop};
use super::{
fallback_keys::{FallbackKey, FallbackKeys},
one_time_keys::OneTimeKeys,
Account,
};
use crate::{
types::{Curve25519Keypair, Curve25519SecretKey},
utilities::LibolmEd25519Keypair,
Curve25519PublicKey, Ed25519Keypair, KeyId,
};
#[derive(Encode, Decode, Zeroize, ZeroizeOnDrop)]
struct OneTimeKey {
key_id: u32,
published: bool,
public_key: [u8; 32],
private_key: Box<[u8; 32]>,
}
impl From<&OneTimeKey> for FallbackKey {
fn from(key: &OneTimeKey) -> Self {
FallbackKey {
key_id: KeyId(key.key_id.into()),
key: Curve25519SecretKey::from_slice(&key.private_key),
published: key.published,
}
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
struct FallbackKeysArray {
fallback_key: Option<OneTimeKey>,
previous_fallback_key: Option<OneTimeKey>,
}
impl Decode for FallbackKeysArray {
fn decode(reader: &mut impl std::io::Read) -> Result<Self, DecodeError> {
let count = u8::decode(reader)?;
let (fallback_key, previous_fallback_key) = if count >= 1 {
let fallback_key = OneTimeKey::decode(reader)?;
let previous_fallback_key =
if count >= 2 { Some(OneTimeKey::decode(reader)?) } else { None };
(Some(fallback_key), previous_fallback_key)
} else {
(None, None)
};
Ok(Self { fallback_key, previous_fallback_key })
}
}
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(Encode, Decode, Zeroize, ZeroizeOnDrop)]
pub(super) struct Pickle {
version: u32,
ed25519_keypair: LibolmEd25519Keypair,
public_curve25519_key: [u8; 32],
private_curve25519_key: Box<[u8; 32]>,
one_time_keys: Vec<OneTimeKey>,
fallback_keys: FallbackKeysArray,
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;
fn try_from(pickle: Pickle) -> Result<Self, Self::Error> {
let mut one_time_keys = OneTimeKeys::new();
for key in &pickle.one_time_keys {
let secret_key = Curve25519SecretKey::from_slice(&key.private_key);
let key_id = KeyId(key.key_id.into());
one_time_keys.insert_secret_key(key_id, secret_key, key.published);
}
one_time_keys.next_key_id = pickle.next_key_id.into();
let fallback_keys = FallbackKeys {
key_id: pickle
.fallback_keys
.fallback_key
.as_ref()
.map(|k| k.key_id.wrapping_add(1))
.unwrap_or(0) as u64,
fallback_key: pickle.fallback_keys.fallback_key.as_ref().map(|k| k.into()),
previous_fallback_key: pickle
.fallback_keys
.previous_fallback_key
.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,
})
}
}
}
#[cfg(test)]
mod test {
use anyhow::{bail, Context, Result};
#[cfg(feature = "libolm-compat")]
use matrix_pickle::Encode;
use olm_rs::{account::OlmAccount, session::OlmMessage as LibolmOlmMessage};
#[cfg(feature = "libolm-compat")]
use super::libolm::Pickle;
use super::{Account, InboundCreationResult, SessionConfig, SessionCreationError};
use crate::{
cipher::Mac,
olm::{
account::PUBLIC_MAX_ONE_TIME_KEYS,
messages::{OlmMessage, PreKeyMessage},
AccountPickle,
},
Curve25519PublicKey as PublicKey,
};
const PICKLE_KEY: [u8; 32] = [0u8; 32];
#[test]
fn max_number_of_one_time_keys_matches_global_constant() {
assert_eq!(Account::new().max_number_of_one_time_keys(), PUBLIC_MAX_ONE_TIME_KEYS);
}
#[test]
#[cfg(feature = "low-level-api")]
fn generate_and_remove_one_time_key() {
let mut alice = Account::new();
assert_eq!(alice.stored_one_time_key_count(), 0);
alice.generate_one_time_keys(1);
assert_eq!(alice.stored_one_time_key_count(), 1);
let public_key = alice
.one_time_keys()
.values()
.next()
.copied()
.expect("Should have an unpublished one-time key");
let secret_key_bytes = alice
.find_one_time_key(&public_key)
.expect("Should find secret key for public one-time key")
.to_bytes();
let removed_key_bytes = alice
.remove_one_time_key(public_key)
.expect("Should be able to remove one-time key")
.to_bytes();
assert_eq!(removed_key_bytes, secret_key_bytes);
assert_eq!(alice.stored_one_time_key_count(), 0);
}
#[test]
fn generate_and_forget_fallback_keys() {
let mut alice = Account::default();
assert!(!alice.forget_fallback_key());
alice.generate_fallback_key();
assert!(!alice.forget_fallback_key());
alice.generate_fallback_key();
assert!(alice.forget_fallback_key());
}
#[test]
fn vodozemac_libolm_communication() -> Result<()> {
let alice = Account::new();
let bob = OlmAccount::new();
bob.generate_one_time_keys(1);
let one_time_key = bob
.parsed_one_time_keys()
.curve25519()
.values()
.next()
.cloned()
.expect("Didn't find a valid one-time key");
bob.mark_keys_as_published();
let identity_keys = bob.parsed_identity_keys();
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);
let message = "It's a secret to everybody";
let olm_message: LibolmOlmMessage = alice_session.encrypt(message).into();
if let LibolmOlmMessage::PreKey(m) = olm_message.clone() {
let libolm_session =
bob.create_inbound_session_from(&alice.curve25519_key().to_base64(), m)?;
assert_eq!(alice_session.session_id(), libolm_session.session_id());
let plaintext = libolm_session.decrypt(olm_message)?;
assert_eq!(message, plaintext);
let second_text = "Here's another secret to everybody";
let olm_message = alice_session.encrypt(second_text).into();
let plaintext = libolm_session.decrypt(olm_message)?;
assert_eq!(second_text, plaintext);
let reply_plain = "Yes, take this, it's dangerous out there";
let reply = libolm_session.encrypt(reply_plain).into();
let plaintext = alice_session.decrypt(&reply)?;
assert_eq!(plaintext, reply_plain.as_bytes());
let another_reply = "Last one";
let reply = libolm_session.encrypt(another_reply).into();
let plaintext = alice_session.decrypt(&reply)?;
assert_eq!(plaintext, another_reply.as_bytes());
let last_text = "Nope, I'll have the last word";
let olm_message = alice_session.encrypt(last_text).into();
let plaintext = libolm_session.decrypt(olm_message)?;
assert_eq!(last_text, plaintext);
} else {
bail!("Received a invalid message type {:?}", olm_message);
}
Ok(())
}
#[test]
fn vodozemac_vodozemac_communication() -> Result<()> {
let alice = Account::new();
let mut bob = Account::new();
bob.generate_one_time_keys(1);
let mut alice_session = alice.create_outbound_session(
SessionConfig::version_2(),
bob.curve25519_key(),
*bob.one_time_keys()
.iter()
.next()
.context("Failed getting bob's OTK, which should never happen here.")?
.1,
);
assert!(!bob.one_time_keys().is_empty());
bob.mark_keys_as_published();
assert!(bob.one_time_keys().is_empty());
let message = "It's a secret to everybody";
let olm_message = alice_session.encrypt(message);
if let OlmMessage::PreKey(m) = olm_message {
assert_eq!(m.session_keys(), alice_session.session_keys());
assert_eq!(m.session_id(), alice_session.session_id());
let InboundCreationResult { session: mut bob_session, plaintext } =
bob.create_inbound_session(alice.curve25519_key(), &m)?;
assert_eq!(alice_session.session_id(), bob_session.session_id());
assert_eq!(m.session_keys(), bob_session.session_keys());
assert_eq!(message.as_bytes(), plaintext);
let second_text = "Here's another secret to everybody";
let olm_message = alice_session.encrypt(second_text);
let plaintext = bob_session.decrypt(&olm_message)?;
assert_eq!(second_text.as_bytes(), plaintext);
let reply_plain = "Yes, take this, it's dangerous out there";
let reply = bob_session.encrypt(reply_plain);
let plaintext = alice_session.decrypt(&reply)?;
assert_eq!(plaintext, reply_plain.as_bytes());
let another_reply = "Last one";
let reply = bob_session.encrypt(another_reply);
let plaintext = alice_session.decrypt(&reply)?;
assert_eq!(plaintext, another_reply.as_bytes());
let last_text = "Nope, I'll have the last word";
let olm_message = alice_session.encrypt(last_text);
let plaintext = bob_session.decrypt(&olm_message)?;
assert_eq!(last_text.as_bytes(), plaintext);
}
Ok(())
}
#[test]
fn inbound_session_creation() -> Result<()> {
let alice = OlmAccount::new();
let mut bob = Account::new();
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");
let alice_session = alice.create_outbound_session(
&bob.curve25519_key().to_base64(),
&one_time_key.to_base64(),
)?;
let text = "It's a secret to everybody";
let message = alice_session.encrypt(text).into();
let identity_key = PublicKey::from_base64(alice.parsed_identity_keys().curve25519())?;
let InboundCreationResult { session, plaintext } = if let OlmMessage::PreKey(m) = &message {
bob.create_inbound_session(identity_key, m)?
} else {
bail!("Got invalid message type from olm_rs {:?}", message);
};
assert_eq!(alice_session.session_id(), session.session_id());
assert!(bob.one_time_keys.private_keys.is_empty());
assert_eq!(text.as_bytes(), plaintext);
Ok(())
}
#[test]
fn inbound_session_creation_using_fallback_keys() -> Result<()> {
let alice = OlmAccount::new();
let mut bob = Account::new();
bob.generate_fallback_key();
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());
let alice_session = alice.create_outbound_session(
&bob.curve25519_key().to_base64(),
&one_time_key.to_base64(),
)?;
let text = "It's a secret to everybody";
let message = alice_session.encrypt(text).into();
let identity_key = PublicKey::from_base64(alice.parsed_identity_keys().curve25519())?;
if let OlmMessage::PreKey(m) = &message {
let InboundCreationResult { session, plaintext } =
bob.create_inbound_session(identity_key, m)?;
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_eq!(text.as_bytes(), plaintext);
} else {
bail!("Got invalid message type from olm_rs");
};
Ok(())
}
#[test]
fn account_pickling_roundtrip_is_identity() -> Result<()> {
let mut account = Account::new();
account.generate_one_time_keys(50);
account.generate_fallback_key();
account.generate_fallback_key();
let pickle = account.pickle().encrypt(&PICKLE_KEY);
let decrypted_pickle = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
let unpickled_account = Account::from_pickle(decrypted_pickle);
let repickle = unpickled_account.pickle();
assert_eq!(account.identity_keys(), unpickled_account.identity_keys());
let decrypted_pickle = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?;
let pickle = serde_json::to_value(decrypted_pickle)?;
let repickle = serde_json::to_value(repickle)?;
assert_eq!(pickle, repickle);
Ok(())
}
#[test]
#[cfg(feature = "libolm-compat")]
fn libolm_unpickling() -> Result<()> {
let olm = OlmAccount::new();
olm.generate_one_time_keys(10);
olm.generate_fallback_key();
let key = b"DEFAULT_PICKLE_KEY";
let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
let unpickled = Account::from_libolm_pickle(&pickle, key)?;
assert_eq!(olm.parsed_identity_keys().ed25519(), unpickled.ed25519_key().to_base64());
assert_eq!(olm.parsed_identity_keys().curve25519(), unpickled.curve25519_key().to_base64());
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();
assert_eq!(unpickled.one_time_keys.next_key_id, 11);
olm_one_time_keys.sort();
one_time_keys.sort();
assert_eq!(olm_one_time_keys, one_time_keys);
let olm_fallback_key =
olm.parsed_fallback_key().expect("libolm should have a fallback key");
assert_eq!(
olm_fallback_key.curve25519(),
unpickled
.fallback_key()
.values()
.next()
.expect("We should have a fallback key")
.to_base64()
);
Ok(())
}
#[test]
#[cfg(feature = "libolm-compat")]
fn pickle_cycle_with_one_fallback_key() {
let mut alice = Account::new();
alice.generate_fallback_key();
let mut encoded = Vec::<u8>::new();
let pickle = Pickle::from(&alice);
let size = pickle.encode(&mut encoded).expect("Should encode pickle");
assert_eq!(size, encoded.len());
let account =
Account::from_decrypted_libolm_pickle(&encoded).expect("Should unpickle account");
let key_bytes = alice
.fallback_key()
.values()
.next()
.expect("Should have a fallback key before encoding")
.to_bytes();
let decoded_key_bytes = account
.fallback_key()
.values()
.next()
.expect("Should have a fallback key after decoding")
.to_bytes();
assert_eq!(key_bytes, decoded_key_bytes);
}
#[test]
#[cfg(feature = "libolm-compat")]
fn pickle_cycle_with_two_fallback_keys() {
let mut alice = Account::new();
alice.generate_fallback_key();
alice.generate_fallback_key();
let mut encoded = Vec::<u8>::new();
let pickle = Pickle::from(&alice);
let size = pickle.encode(&mut encoded).expect("Should encode pickle");
assert_eq!(size, encoded.len());
let account =
Account::from_decrypted_libolm_pickle(&encoded).expect("Should unpickle account");
let key_bytes = alice
.fallback_key()
.values()
.next()
.expect("Should have a fallback key before encoding")
.to_bytes();
let decoded_key_bytes = account
.fallback_key()
.values()
.next()
.expect("Should have a fallback key after decoding")
.to_bytes();
assert_eq!(key_bytes, decoded_key_bytes);
}
#[test]
#[cfg(feature = "libolm-compat")]
fn signing_with_expanded_key() -> Result<()> {
let olm = OlmAccount::new();
olm.generate_one_time_keys(10);
olm.generate_fallback_key();
let key = b"DEFAULT_PICKLE_KEY";
let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });
let account_with_expanded_key = Account::from_libolm_pickle(&pickle, key)?;
#[allow(clippy::redundant_clone)]
let signing_key_clone = account_with_expanded_key.signing_key.clone();
signing_key_clone.sign("You met with a terrible fate, haven’t you?".as_bytes());
account_with_expanded_key.sign("You met with a terrible fate, haven’t you?".as_bytes());
Ok(())
}
#[test]
fn invalid_session_creation_does_not_remove_otk() -> Result<()> {
let mut alice = Account::new();
let malory = Account::new();
alice.generate_one_time_keys(1);
let mut session = malory.create_outbound_session(
SessionConfig::default(),
alice.curve25519_key(),
*alice.one_time_keys().values().next().expect("Should have one-time key"),
);
let message = session.encrypt("Test");
if let OlmMessage::PreKey(m) = message {
let mut message = m.to_bytes();
let message_len = message.len();
message[message_len - Mac::TRUNCATED_LEN..message_len]
.copy_from_slice(&[0u8; Mac::TRUNCATED_LEN]);
let message = PreKeyMessage::try_from(message)?;
match alice.create_inbound_session(malory.curve25519_key(), &message) {
Err(SessionCreationError::Decryption(_)) => {}
e => bail!("Expected a decryption error, got {:?}", e),
}
assert!(
!alice.one_time_keys.private_keys.is_empty(),
"The one-time key was removed when it shouldn't"
);
Ok(())
} else {
bail!("Invalid message type");
}
}
#[test]
#[cfg(feature = "libolm-compat")]
fn fuzz_corpus_unpickling() {
crate::run_corpus("olm-account-unpickling", |data| {
let _ = Account::from_decrypted_libolm_pickle(data);
});
}
#[test]
#[cfg(feature = "libolm-compat")]
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.as_bytes());
let olm_signature = crate::types::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(())
}
}