Skip to content

Commit

Permalink
make changes as requested by review, including changing encryption me…
Browse files Browse the repository at this point in the history
…thod
  • Loading branch information
uhoreg committed Dec 20, 2024
1 parent 048b0f3 commit 99be10a
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 116 deletions.
47 changes: 47 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,53 @@ pub enum LibolmPickleError {
Encode(#[from] matrix_pickle::EncodeError),
}

/// Error type describing the various ways dehydrated devices can fail to be
/// decoded.
#[derive(Debug, thiserror::Error)]
pub enum DehydratedDeviceError {
/// The pickle is missing a valid version.
#[error("The pickle doesn't contain a version")]
MissingVersion,
/// The pickle has a unsupported version.
#[error("The pickle uses an unsupported version, expected {0}, got {1}")]
Version(u32, u32),
/// Invalid nonce.
#[error("The nonce was invalid")]
InvalidNonce,
/// The pickle wasn't valid base64.
#[error("The pickle wasn't valid base64: {0}")]
Base64(#[from] Base64DecodeError),
/// The pickle could not have been decrypted.
#[error("The pickle couldn't be decrypted: {0}")]
Decryption(chacha20poly1305::aead::Error),
/// The pickle contains an invalid public key.
#[error("The pickle contained an invalid ed25519 public key {0}")]
PublicKey(#[from] KeyError),
/// 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),
}

impl From<LibolmPickleError> for DehydratedDeviceError {
fn from(err: LibolmPickleError) -> Self {
match err {
LibolmPickleError::PublicKey(e) => Self::PublicKey(e),
LibolmPickleError::Decode(e) => Self::Decode(e),
LibolmPickleError::Encode(e) => Self::Encode(e),
_ => panic!("Unexpected libolm pickle error {}", err),
}
}
}

impl From<chacha20poly1305::aead::Error> for DehydratedDeviceError {
fn from(error: chacha20poly1305::aead::Error) -> Self {
Self::Decryption(error)
}
}

/// Error type describing the different ways message decoding can fail.
#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
Expand Down
188 changes: 82 additions & 106 deletions src/olm/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ mod one_time_keys;

use std::collections::HashMap;

use chacha20poly1305::{
aead::{Aead, AeadCore, KeyInit},
ChaCha20Poly1305,
};
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use x25519_dalek::ReusableSecret;
use zeroize::Zeroize;

pub use self::one_time_keys::OneTimeKeyGenerationResult;
use self::{
Expand Down Expand Up @@ -440,43 +445,75 @@ impl Account {
pickle.try_into()
}

/// Create a dehydrated device pickle.
pub fn to_dehydrated_device(&self, key: &[u8; 32]) -> Result<String, crate::LibolmPickleError> {
use self::dehydrated_device::{expand_pickle_key, Pickle};
use crate::utilities::pickle_libolm;

let pickle_key = expand_pickle_key(key, &self.curve25519_key().to_base64());
/// Create a dehydrated device from the account.
///
/// A dehydrated device is a device that is stored encrypted on the server
/// that can receive messages when the user has no other active devices.
/// Upon login, the user can rehydrate the device (using
/// [`from_dehydrated_device`]) and decrypt the messages sent to the
/// dehydrated device.
///
/// Returns the ciphertext and nonce. `key` is a 256-bit (32-byte) key for
/// encrypting the device.
///
/// The format used here is defined in
/// [MSC3814](https://github.com/matrix-org/matrix-spec-proposals/pull/3814).
pub fn to_dehydrated_device(
&self,
key: &[u8; 32],
) -> Result<(String, String), crate::DehydratedDeviceError> {
use self::dehydrated_device::Pickle;
use crate::utilities::base64_encode;
use matrix_pickle::Encode;

let pickle: Pickle = self.into();
let mut encoded = pickle.encode_to_vec()?;
let cipher = ChaCha20Poly1305::new(key.into());
let rng = thread_rng();
let nonce = ChaCha20Poly1305::generate_nonce(rng);
let ciphertext = cipher.encrypt(&nonce, encoded.as_slice());
encoded.zeroize();
let ciphertext = ciphertext?;

pickle_libolm::<Pickle>(self.into(), pickle_key.as_ref())
Ok((base64_encode(ciphertext), base64_encode(nonce)))
}

/// Create an [`Account`] object by rehydrating a device.
/// Create an [`Account`] object from a dehydrated device.
///
/// `ciphertext` and `nonce` are the ciphertext and nonce returned by
/// [`to_dehydrated_device`]. `key` is a 256-bit (32-byte) key for
/// decrypting the device, and must be the same key used when
/// [`to_dehydrate_device`] was called.
pub fn from_dehydrated_device(
pickle: &str,
curve25519key: &str,
ciphertext: &str,
nonce: &str,
key: &[u8; 32],
) -> Result<Self, crate::LibolmPickleError> {
use self::dehydrated_device::{expand_pickle_key, Pickle, PICKLE_VERSION};
use crate::utilities::unpickle_libolm;
) -> Result<Self, crate::DehydratedDeviceError> {
use crate::utilities::{base64_decode, get_pickle_version};
use matrix_pickle::Decode;
use std::io::Cursor;

let pickle_key = expand_pickle_key(key, curve25519key);

#[cfg(feature = "libolm-compat")]
return unpickle_libolm::<Pickle, _>(pickle, pickle_key.as_ref(), PICKLE_VERSION).or_else(
|err| {
match err {
// If it failed as a dehydrated device due to a bad
// version, we try decoding it as a libolm pickle, which
// some older dehydrated devices used.
crate::LibolmPickleError::Version(..) => {
Self::from_libolm_pickle(pickle, pickle_key.as_ref())
}
_ => Err(err),
}
},
);
#[cfg(not(feature = "libolm-compat"))]
unpickle_libolm::<Pickle, _>(pickle, pickle_key.as_ref(), PICKLE_VERSION)
use self::dehydrated_device::{Pickle, PICKLE_VERSION};

let cipher = ChaCha20Poly1305::new(key.into());
let ciphertext = base64_decode(ciphertext)?;
let nonce = base64_decode(nonce)?;
if nonce.len() != 12 {
return Err(crate::DehydratedDeviceError::InvalidNonce);
}
let mut plaintext = cipher.decrypt(nonce.as_slice().into(), ciphertext.as_slice())?;
let version =
get_pickle_version(&plaintext).ok_or(crate::DehydratedDeviceError::MissingVersion)?;
if version != PICKLE_VERSION {
return Err(crate::DehydratedDeviceError::Version(PICKLE_VERSION, version));
}

let mut cursor = Cursor::new(&plaintext);
let pickle = Pickle::decode(&mut cursor);
plaintext.zeroize();
let pickle = pickle?;

Ok(pickle.try_into()?)
}
}

Expand Down Expand Up @@ -722,9 +759,7 @@ mod libolm {
}

mod dehydrated_device {
use hkdf::Hkdf;
use matrix_pickle::{Decode, DecodeError, Encode, EncodeError};
use sha2::Sha256;
use zeroize::{Zeroize, ZeroizeOnDrop};

use super::{
Expand Down Expand Up @@ -798,6 +833,11 @@ mod dehydrated_device {
}

#[derive(Encode, Decode, Zeroize, ZeroizeOnDrop)]
/// Pickle used for dehydrated devices.
///
/// Dehydrated devices are used for receiving encrypted messages when the
/// user has no other devices logged in, and are defined in
/// [MSC3814](https://github.com/matrix-org/matrix-spec-proposals/pull/3814).
pub(super) struct Pickle {
version: u32,
private_curve25519_key: Box<[u8; 32]>,
Expand All @@ -806,7 +846,7 @@ mod dehydrated_device {
opt_fallback_key: OptFallbackKey,
}

pub(super) const PICKLE_VERSION: u32 = 0x80000000;
pub(super) const PICKLE_VERSION: u32 = 1;

impl From<&Account> for Pickle {
fn from(account: &Account) -> Self {
Expand Down Expand Up @@ -859,16 +899,6 @@ mod dehydrated_device {
})
}
}

pub fn expand_pickle_key(key: &[u8; 32], identity_key: &str) -> Box<[u8; 32]> {
let kdf: Hkdf<Sha256> = Hkdf::new(Some(identity_key.as_bytes()), key);
let mut key = [0u8; 32];

kdf.expand(b"dehydrated-device-pickle-key", &mut key)
.expect("We should be able to expand the 32 byte pickle key");

Box::new(key)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -1396,7 +1426,7 @@ mod test {
alice.generate_one_time_keys(alice.max_number_of_one_time_keys());
alice.generate_fallback_key();

let alice_dehydrated =
let (alice_dehydrated, nonce) =
alice.to_dehydrated_device(&PICKLE_KEY).expect("Should be able to dehydrate device");

let mut bob_session = bob.create_outbound_session(
Expand All @@ -1413,12 +1443,9 @@ mod test {
let message = "It's a secret to everybody";
let olm_message = bob_session.encrypt(message);

let mut alice_rehydrated = Account::from_dehydrated_device(
&alice_dehydrated,
&alice.curve25519_key().to_base64(),
&PICKLE_KEY,
)
.expect("Should be able to rehydrate device");
let mut alice_rehydrated =
Account::from_dehydrated_device(&alice_dehydrated, &nonce, &PICKLE_KEY)
.expect("Should be able to rehydrate device");

if let OlmMessage::PreKey(m) = olm_message {
let InboundCreationResult { session: alice_session, plaintext } =
Expand All @@ -1433,75 +1460,24 @@ mod test {
Ok(())
}

#[test]
#[cfg(feature = "libolm-compat")]
fn decrypt_with_account_pickle_dehydrated_device() -> Result<()> {
// Can rehydrate an account from an older version of dehydrated devices
// that used an Account pickle.
let mut alice = Account::new();
let bob = Account::new();

alice.generate_one_time_keys(alice.max_number_of_one_time_keys());
alice.generate_fallback_key();

let pickle_key =
dehydrated_device::expand_pickle_key(&PICKLE_KEY, &alice.curve25519_key().to_base64());
let alice_dehydrated = alice
.to_libolm_pickle(pickle_key.as_ref())
.expect("Should be able to dehydrate device");

let mut bob_session = bob.create_outbound_session(
SessionConfig::version_1(),
alice.curve25519_key(),
*alice
.one_time_keys()
.iter()
.next()
.context("Failed getting alice's OTK, which should never happen here.")?
.1,
);

let message = "It's a secret to everybody";
let olm_message = bob_session.encrypt(message);

let mut alice_rehydrated = Account::from_dehydrated_device(
&alice_dehydrated,
&alice.curve25519_key().to_base64(),
&PICKLE_KEY,
)
.expect("Should be able to rehydrate device");

if let OlmMessage::PreKey(m) = olm_message {
let InboundCreationResult { session: alice_session, plaintext } =
alice_rehydrated.create_inbound_session(bob.curve25519_key(), &m)?;

assert_eq!(alice_session.session_id(), bob_session.session_id());
assert_eq!(message.as_bytes(), plaintext);
}

Ok(())
}

#[test]
fn fails_to_rehydrate_with_wrong_key() -> Result<()> {
let mut alice = Account::new();

alice.generate_one_time_keys(alice.max_number_of_one_time_keys());
alice.generate_fallback_key();

let alice_dehydrated =
let (alice_dehydrated, nonce) =
alice.to_dehydrated_device(&PICKLE_KEY).expect("Should be able to dehydrate device");
assert!(Account::from_dehydrated_device(&alice_dehydrated, &nonce, &[1; 32],).is_err());

assert!(Account::from_dehydrated_device(
&alice_dehydrated,
&alice.curve25519_key().to_base64(),
&[1; 32],
"WrongNonce123456",
&PICKLE_KEY,
)
.is_err());

assert!(Account::from_dehydrated_device(&alice_dehydrated, "WrongDeviceID", &PICKLE_KEY,)
.is_err());

Ok(())
}

Expand Down
16 changes: 8 additions & 8 deletions src/utilities/libolm_compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
use super::{base64_decode, base64_encode};
use crate::{cipher::Cipher, LibolmPickleError};

/// Fetch the pickle version from the given pickle source.
pub(crate) fn get_version(source: &[u8]) -> Option<u32> {
// Pickle versions are always u32 encoded as a fixed sized integer in
// big endian encoding.
let version = source.get(0..4)?;
Some(u32::from_be_bytes(version.try_into().ok()?))
}

/// Decrypt and decode the given pickle with the given pickle key.
///
/// # Arguments
Expand All @@ -33,14 +41,6 @@ pub(crate) fn unpickle_libolm<P: Decode, T: TryFrom<P, Error = LibolmPickleError
pickle_key: &[u8],
pickle_version: u32,
) -> Result<T, LibolmPickleError> {
/// Fetch the pickle version from the given pickle source.
fn get_version(source: &[u8]) -> Option<u32> {
// Pickle versions are always u32 encoded as a fixed sized integer in
// big endian encoding.
let version = source.get(0..4)?;
Some(u32::from_be_bytes(version.try_into().ok()?))
}

// libolm pickles are always base64 encoded, so first try to decode.
let decoded = base64_decode(pickle)?;

Expand Down
3 changes: 1 addition & 2 deletions src/utilities/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ use base64::{
engine::{general_purpose, GeneralPurpose},
Engine,
};
#[cfg(not(feature = "libolm-compat"))]
pub(crate) use libolm_compat::{pickle_libolm, unpickle_libolm};
pub(crate) use libolm_compat::get_version as get_pickle_version;
#[cfg(feature = "libolm-compat")]
pub(crate) use libolm_compat::{pickle_libolm, unpickle_libolm, LibolmEd25519Keypair};

Expand Down

0 comments on commit 99be10a

Please sign in to comment.