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 pickling logic to pk_encryption #173

Closed
wants to merge 15 commits into from
117 changes: 117 additions & 0 deletions src/pk_encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use aes::cipher::{
BlockDecryptMut as _, BlockEncryptMut as _, KeyIvInit as _,
};
use hmac::{digest::MacError, Mac as _};
use matrix_pickle::{Decode, Encode};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::{
base64_decode,
Expand All @@ -30,6 +32,7 @@ use crate::{
};

const MAC_LENGTH: usize = 8;
const PICKLE_VERSION: u32 = 1;

pub struct PkDecryption {
key: Curve25519SecretKey,
Expand Down Expand Up @@ -86,6 +89,56 @@ impl PkDecryption {
pub fn to_bytes(&self) -> Box<[u8; 32]> {
self.key.to_bytes()
}

/// Create a [`PkDecryption`] object by unpickling a PkDecryption pickle in libolm
/// legacy pickle format.
///
/// Such pickles are encrypted and need to first be decrypted using
devonh marked this conversation as resolved.
Show resolved Hide resolved
/// `pickle_key`.
pub fn from_libolm_pickle(
pickle: &str,
pickle_key: &[u8],
) -> Result<Self, crate::LibolmPickleError> {
use crate::utilities::unpickle_libolm;

unpickle_libolm::<PkDecryptionPickle, _>(pickle, pickle_key, PICKLE_VERSION)
}

/// Pickle a [`PkDecryption`] into a libolm pickle format.
///
/// This pickle can be restored using the `[PkDecryption::from_libolm_pickle]`
/// method, or can be used in the [`libolm`] C library.
///
/// The pickle will be encrypted using the pickle key.
///
/// ⚠️ ***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::pk_encryption::PkDecryption;
/// use olm_rs::{pk::OlmPkDecryption, PicklingMode};
///
/// let decrypt = PkDecryption::new();
///
/// let pickle = decrypt
/// .to_libolm_pickle(&[0u8; 32])
/// .expect("We should be able to pickle a freshly created PkDecryption");
///
/// let unpickled = OlmPkDecryption::unpickle(
/// pickle,
/// PicklingMode::Encrypted { key: [0u8; 32].to_vec() },
/// ).expect("We should be able to unpickle our exported PkDecryption");
/// ```
pub fn to_libolm_pickle(&self, pickle_key: &[u8]) -> Result<String, crate::LibolmPickleError> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Err, does anybody use that? We just need the seed for a Curve25519, AFAIK even in libolm days, Element Web didn't use this to store the key.

Persisting the public key is unnecessary, and then there's the whole key re-use trap this might propagate.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is currently how Matrix Content Scanner saves it's keys. I have no objections to saving them in a different format going forward. I kept this in out of convenience to minimize changes in the content scanner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll go ahead and remove the to_libolm_pickle function here 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks, that would be great.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm I'm not sure if removing this entirely will work. The Matrix Content Scanner backend relies on saving it's key locally in pickled form.

I could change this to be pickle and create a pickled PkDecryption which only contains the secret and omits the public key. Either way we will need something that allows us to create a new key and save it somewhere.

@poljar Is there a better way to go about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Those would allow us to save the key yes. We would lose the additional encryption we get with the current approach though. (Since the libolm pickle is encrypted with an additional key)

I'm not sure if the encrypted pickle adds much, since the key for it is stored in the Content Scanner's config file.

Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, you don't have encryption for storage. Let me sleep on that since it's quite late where I'm at.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fuck it, let's keep it then. Somebody else might depend on this as well and then I'll have this conversation again if we don't keep it.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the scanner itself it seems fine to me to just ignore the pickled key and create a new one, it would just break on update for a very small number of reqs that fetched the public key before the update, and it will just work on retry I believe (new public key will be fetch and used).

Copy link
Contributor

@MatMaul MatMaul Sep 6, 2024

Choose a reason for hiding this comment

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

I'm not sure if the encrypted pickle adds much, since the key for it is stored in the Content Scanner's config file.

Agreed, it seems fairly useless to me.

But poljar point applies, now that it's done here we may as well merge it if others need it, it's not a lot of code.

Do we need to plug the pickle logic in the content scanner or just remove it altogether (and generate a new key on first start) is another story :)

use crate::utilities::pickle_libolm;
pickle_libolm::<PkDecryptionPickle>(self.into(), pickle_key)
}
}

impl Default for PkDecryption {
Expand All @@ -94,6 +147,34 @@ impl Default for PkDecryption {
}
}

impl TryFrom<PkDecryptionPickle> for PkDecryption {
type Error = crate::LibolmPickleError;

fn try_from(pickle: PkDecryptionPickle) -> Result<Self, Self::Error> {
Ok(Self {
key: Curve25519SecretKey::from_slice(&pickle.private_curve25519_key),
public_key: Curve25519PublicKey::from_slice(&pickle.public_curve25519_key)?,
devonh marked this conversation as resolved.
Show resolved Hide resolved
})
}
}

#[derive(Encode, Decode, Zeroize, ZeroizeOnDrop)]
struct PkDecryptionPickle {
version: u32,
public_curve25519_key: [u8; 32],
private_curve25519_key: Box<[u8; 32]>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

}

impl From<&PkDecryption> for PkDecryptionPickle {
fn from(decrypt: &PkDecryption) -> Self {
Self {
version: PICKLE_VERSION,
public_curve25519_key: decrypt.public_key.to_bytes(),
private_curve25519_key: decrypt.key.to_bytes(),
}
}
}

pub struct PkEncryption {
public_key: Curve25519PublicKey,
}
Expand Down Expand Up @@ -270,4 +351,40 @@ mod tests {
"The public keys of the restored and original PK decryption should match"
);
}

#[test]
fn libolm_unpickling() -> anyhow::Result<()> {
let olm = OlmPkDecryption::new();

let key = b"DEFAULT_PICKLE_KEY";
let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });

let unpickled = PkDecryption::from_libolm_pickle(&pickle, key)?;

assert_eq!(olm.public_key(), unpickled.public_key().to_base64());

Ok(())
}

#[test]
fn libolm_pickle_cycle() -> anyhow::Result<()> {
let olm = OlmPkDecryption::new();

let key = b"DEFAULT_PICKLE_KEY";
let pickle = olm.pickle(olm_rs::PicklingMode::Encrypted { key: key.to_vec() });

let decrypt = PkDecryption::from_libolm_pickle(&pickle, key).unwrap();
let vodozemac_pickle = decrypt.to_libolm_pickle(key).unwrap();
let _ = PkDecryption::from_libolm_pickle(&vodozemac_pickle, key).unwrap();

let unpickled = OlmPkDecryption::unpickle(
vodozemac_pickle,
olm_rs::PicklingMode::Encrypted { key: key.to_vec() },
)
.unwrap();

assert_eq!(olm.public_key(), unpickled.public_key());

Ok(())
}
}