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

Simplify our serialization logic #18

Merged
merged 8 commits into from
Mar 2, 2022
35 changes: 18 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,10 @@ only *unpickling* is supported.

### Modern pickles

The modern pickling mechanism used by this crate. The exact serialization
format which will be used is undecided but for now we're pickling to JSON.
Since the pickling support is based on `serde`, changing the format is easy.
The crate also implements a modern pickling mechanism using
[Serde](https://serde.rs/). The exact serialization format is not mandated or
specified by this crate, but you can serialize to and deserialize from any
format supported by Serde.

The following structs support pickling:

Expand All @@ -131,30 +132,26 @@ The following structs support pickling:
- `GroupSession`
- `InboundGroupSession`

To pickle into a JSON string, simply call the `.pickle_to_json_string()` method,
which will return a special struct implementing `.as_str()`,
`Deref<Target=str>` and `AsRef<str>` which you can use to get to the actual
serialized string. This struct will zeroize its memory once it drops so that
any secrets within do not linger on.

For example, the following will print out the JSON representing the serialized
`Account` and will leave no new copies of the account's secrets in memory:

```rust
use anyhow::Result;
use vodozemac::olm::Account;
use vodozemac::olm::{Account, AccountPickle};

const PICKLE_KEY: [u8; 32] = [0u8; 32];

fn main() -> Result<()>{
let mut account = Account::new();

account.generate_one_time_keys(10);
account.generate_fallback_key();

let pickle = account.pickle_to_json_string();
let pickle = account.pickle().encrypt(&PICKLE_KEY);

print!("{}", pickle.as_str());
let account2: Account = AccountPickle::from_encrypted(&pickle, &PICKLE_KEY)?.into();

let account2 = pickle.unpickle()?; // this is the same as `account`
assert_eq!(account.identity_keys(), account2.identity_keys());

Ok(())
}
Expand All @@ -164,12 +161,16 @@ You can unpickle a pickle-able struct directly from its serialized form:

```rust
# use anyhow::Result;
# use vodozemac::olm::Account;
# use vodozemac::olm::{Account, AccountPickle};
# use zeroize::Zeroize;
#
# fn main() -> Result<()> {
# let some_account = Account::new();
let json_str = some_account.pickle_to_json_string();
let account: Account = serde_json::from_str(&json_str)?; // the same as `some_account`
let mut json_str = serde_json::to_string(&some_account.pickle())?;
// This will produce an account which is identical to `some_account`.
let account: Account = serde_json::from_str::<AccountPickle>(&json_str)?.into();
poljar marked this conversation as resolved.
Show resolved Hide resolved

json_str.zeroize();
#
# Ok(())
# }
Expand All @@ -188,7 +189,7 @@ use vodozemac::olm::Account;

fn main() -> Result<()> {
let account = Account::new();
let account: Account = account.pickle().unpickle()?; // this is identity
let account: Account = account.pickle().into(); // this is identity

Ok(())
}
Expand Down
10 changes: 9 additions & 1 deletion src/cipher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ impl Cipher {
cipher.decrypt_vec(ciphertext)
}

#[cfg(feature = "libolm-compat")]
pub fn decrypt_pickle(&self, ciphertext: &[u8]) -> Result<Vec<u8>, DecryptionError> {
if ciphertext.len() < Mac::TRUNCATED_LEN + 1 {
Err(DecryptionError::MacMissing)
Expand All @@ -119,6 +118,15 @@ impl Cipher {
}
}

pub fn encrypt_pickle(&self, plaintext: &[u8]) -> Vec<u8> {
let mut ciphertext = self.encrypt(plaintext);
let mac = self.mac(&ciphertext);

ciphertext.extend(mac.truncate());

ciphertext
}

pub fn verify_mac(&self, message: &[u8], tag: &[u8]) -> Result<(), MacError> {
let mut hmac = self.get_hmac();

Expand Down
12 changes: 11 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,19 @@ pub use types::{
SignatureError,
};

#[derive(Debug, thiserror::Error)]
pub enum PickleError {
#[error("The pickle wasn't valid base64: {0}")]
Base64(#[from] base64::DecodeError),
#[error("The pickle couldn't be decrypted: {0}")]
Decryption(#[from] crate::cipher::DecryptionError),
#[error("The pickle couldn't be deserialized: {0}")]
Serialization(#[from] serde_json::Error),
}

#[cfg(feature = "libolm-compat")]
#[derive(Debug, thiserror::Error)]
pub enum LibolmUnpickleError {
pub enum LibolmPickleError {
#[error("The pickle doesn't contain a version")]
MissingVersion,
#[error("The pickle uses an unsupported version, expected {0}, got {1}")]
Expand Down
103 changes: 28 additions & 75 deletions src/megolm/group_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::ops::Deref;

use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::Zeroize;

use super::{
message::MegolmMessage,
ratchet::{MegolmRatchetUnpicklingError, Ratchet, RatchetPickle},
SessionKey, SESSION_KEY_VERSION,
use super::{message::MegolmMessage, ratchet::Ratchet, SessionKey, SESSION_KEY_VERSION};
use crate::{
cipher::Cipher,
types::Ed25519Keypair,
utilities::{base64_encode, pickle, unpickle},
PickleError,
};
use crate::{cipher::Cipher, types::Ed25519Keypair, utilities::base64_encode};

/// A Megolm group session represents a single sending participant in an
/// encrypted group communication context containing multiple receiving parties.
Expand All @@ -39,8 +37,6 @@ use crate::{cipher::Cipher, types::Ed25519Keypair, utilities::base64_encode};
/// Such an inbound group session is typically sent by the outbound group
/// session owner to each of the receiving parties via a secure peer-to-peer
/// channel (e.g. an Olm channel).
#[derive(Deserialize)]
#[serde(try_from = "GroupSessionPickle")]
pub struct GroupSession {
ratchet: Ratchet,
signing_key: Ed25519Keypair,
Expand Down Expand Up @@ -131,86 +127,43 @@ impl GroupSession {
/// Convert the group session into a struct which implements
/// [`serde::Serialize`] and [`serde::Deserialize`].
pub fn pickle(&self) -> GroupSessionPickle {
GroupSessionPickle {
ratchet: self.ratchet.clone().into(),
signing_key: self.signing_key.clone(),
}
GroupSessionPickle { ratchet: self.ratchet.clone(), signing_key: self.signing_key.clone() }
}

/// Pickle the group session and serialize it to a JSON string.
///
/// The string is wrapped in [`GroupSessionPickledJSON`] which can be
/// derefed to access the content as a string slice. The string will zeroize
/// itself when it drops to prevent secrets contained inside from lingering
/// in memory.
///
/// [`GroupSessionPickledJSON`]: self::GroupSessionPickledJSON
pub fn pickle_to_json_string(&self) -> GroupSessionPickledJSON {
let pickle: GroupSessionPickle = self.pickle();
GroupSessionPickledJSON(
serde_json::to_string_pretty(&pickle).expect("Group session serialization failed."),
)
/// Restore a [`GroupSession`] from a previously saved
/// [`GroupSessionPickle`].
pub fn from_pickle(pickle: GroupSessionPickle) -> Self {
pickle.into()
}
}

/// A format suitable for serialization which implements [`serde::Serialize`]
/// and [`serde::Deserialize`]. Obtainable by calling [`GroupSession::pickle`].
#[derive(Serialize, Deserialize)]
pub struct GroupSessionPickle {
ratchet: RatchetPickle,
ratchet: Ratchet,
signing_key: Ed25519Keypair,
}

/// A format suitable for serialization which implements [`serde::Serialize`]
/// and [`serde::Deserialize`]. Obtainable by calling [`GroupSession::pickle`].
impl GroupSessionPickle {
/// Convert the pickle format back into a [`GroupSession`].
pub fn unpickle(self) -> Result<GroupSession, GroupSessionUnpicklingError> {
self.try_into()
}
}

impl TryFrom<GroupSessionPickle> for GroupSession {
type Error = GroupSessionUnpicklingError;

fn try_from(pickle: GroupSessionPickle) -> Result<Self, Self::Error> {
Ok(Self { ratchet: pickle.ratchet.try_into()?, signing_key: pickle.signing_key })
}
}

#[derive(Zeroize, Debug)]
#[zeroize(drop)]
pub struct GroupSessionPickledJSON(String);
Comment on lines -179 to -181
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of this type was to avoid people serializing to String which would then be left unzeroized since String doesn't implement zeroization on drop. Instead, this wrapper type offered to handle this for you while providing a way to access the raw serialized content via borrowing.

However, from internal discussions with @poljar, we're worried about the proliferation of types being hard to explain and hard to use. Instead, we're considering placing prominent warnings in the documentation that users of pickling functionality should take care to zeroize their serialization buffers themselves or use the encrypt method.

Can we please add a TODO to add such docs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened an issue #45


impl GroupSessionPickledJSON {
/// Access the serialized content as a string slice.
pub fn as_str(&self) -> &str {
&self.0
}

/// Try to convert the serialized JSON string back into a [`GroupSession`].
pub fn unpickle(self) -> Result<GroupSession, GroupSessionUnpicklingError> {
let pickle: GroupSessionPickle = serde_json::from_str(&self.0)?;
pickle.unpickle()
/// Serialize and encrypt the pickle using the given key.
///
/// This is the inverse of [`GroupSessionPickle::from_encrypted`].
pub fn encrypt(self, pickle_key: &[u8; 32]) -> String {
pickle(&self, pickle_key)
}
}

impl AsRef<str> for GroupSessionPickledJSON {
fn as_ref(&self) -> &str {
self.as_str()
/// Obtain a pickle from a ciphertext by decrypting and deserializing using
/// the given key.
///
/// This is the inverse of [`GroupSessionPickle::encrypt`].
pub fn from_encrypted(ciphertext: &str, pickle_key: &[u8; 32]) -> Result<Self, PickleError> {
unpickle(ciphertext, pickle_key)
}
}

impl Deref for GroupSessionPickledJSON {
type Target = str;

fn deref(&self) -> &Self::Target {
self.as_ref()
impl From<GroupSessionPickle> for GroupSession {
fn from(pickle: GroupSessionPickle) -> Self {
Self { ratchet: pickle.ratchet, signing_key: pickle.signing_key }
}
}

#[derive(Error, Debug)]
pub enum GroupSessionUnpicklingError {
#[error("Invalid ratchet")]
InvalidRatchet(#[from] MegolmRatchetUnpicklingError),
#[error("Pickle format corrupted: {0}")]
CorruptedPickle(#[from] serde_json::error::Error),
}
Loading