diff --git a/.deny.toml b/.deny.toml new file mode 100644 index 00000000..d4e4959b --- /dev/null +++ b/.deny.toml @@ -0,0 +1,36 @@ +# https://embarkstudios.github.io/cargo-deny/checks/cfg.html +[graph] +all-features = true +exclude = [ + # dev only dependency + "criterion" +] + +[advisories] +version = 2 +ignore = [ + { id = "RUSTSEC-2024-0368", reason = "We're only using olm-sys for unit tests" }, +] + +[licenses] +version = 2 +allow = [ + "Apache-2.0", + "BSD-3-Clause", + "MIT", +] +exceptions = [ + { allow = ["Unicode-DFS-2016"], crate = "unicode-ident" }, +] + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +allow-git = [ + "https://github.com/poljar/olm-rs", +] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6a51fe4a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +charset = utf-8 + +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +indent_style = space +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml deleted file mode 100644 index fc48e716..00000000 --- a/.github/workflows/audit.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: Security audit -on: - workflow_dispatch: - schedule: - - cron: '0 0 * * *' -jobs: - audit: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions-rs/audit-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deny.yml b/.github/workflows/deny.yml new file mode 100644 index 00000000..5871155c --- /dev/null +++ b/.github/workflows/deny.yml @@ -0,0 +1,14 @@ +name: Lint dependencies (for licences, allowed sources, banned dependencies, vulnerabilities) +on: + pull_request: + paths: + - '**/Cargo.toml' + workflow_dispatch: + schedule: + - cron: '0 0 * * *' +jobs: + cargo-deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..8d5f7893 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "rust-analyzer.checkOnSave.command": "clippy", + "rust-analyzer.cargo.features": "all", + "rust-analyzer.rustfmt": { + "extraArgs": ["+nightly"] + } +} diff --git a/README.md b/README.md index e44a64cb..bd32598d 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,20 @@
vodozemac is an implementation of Olm (Double Ratchet) and Megolm

- - +

- - + - - + - - +
- - + - - +

diff --git a/contrib/zemi.png b/contrib/zemi.png index 8a2dec5d..7a8cb6fa 100644 Binary files a/contrib/zemi.png and b/contrib/zemi.png differ diff --git a/src/olm/account/mod.rs b/src/olm/account/mod.rs index 20a97d09..18e7b05e 100644 --- a/src/olm/account/mod.rs +++ b/src/olm/account/mod.rs @@ -129,8 +129,8 @@ impl Account { } /// Sign the given message using our Ed25519 fingerprint key. - pub fn sign(&self, message: &str) -> Ed25519Signature { - self.signing_key.sign(message.as_bytes()) + pub fn sign(&self, message: impl AsRef<[u8]>) -> Ed25519Signature { + self.signing_key.sign(message.as_ref()) } /// Get the maximum number of one-time keys the client should keep on the @@ -1076,7 +1076,7 @@ mod test { #[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?"); + account_with_expanded_key.sign("You met with a terrible fate, haven’t you?".as_bytes()); Ok(()) } @@ -1146,7 +1146,7 @@ mod test { 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 vodozemac_signature = account.sign(message.as_bytes()); let olm_signature = Ed25519Signature::from_base64(&olm_signature) .expect("We should be able to parse a signature produced by libolm"); account diff --git a/src/olm/messages/mod.rs b/src/olm/messages/mod.rs index 88b70f26..90f8bae2 100644 --- a/src/olm/messages/mod.rs +++ b/src/olm/messages/mod.rs @@ -19,7 +19,7 @@ pub use message::Message; pub use pre_key::PreKeyMessage; use serde::{Deserialize, Serialize}; -use crate::DecodeError; +use crate::{base64_decode, base64_encode, DecodeError}; /// Enum over the different Olm message types. /// @@ -67,9 +67,8 @@ impl Serialize for OlmMessage { where S: serde::Serializer, { - let (message_type, ciphertext) = self.clone().to_parts(); - - let message = MessageSerdeHelper { message_type, ciphertext }; + let (message_type, ciphertext) = self.to_parts(); + let message = MessageSerdeHelper { message_type, ciphertext: base64_encode(ciphertext) }; message.serialize(serializer) } @@ -78,18 +77,19 @@ impl Serialize for OlmMessage { impl<'de> Deserialize<'de> for OlmMessage { fn deserialize>(d: D) -> Result { let value = MessageSerdeHelper::deserialize(d)?; + let ciphertext_bytes = base64_decode(value.ciphertext).map_err(serde::de::Error::custom)?; - OlmMessage::from_parts(value.message_type, &value.ciphertext) + OlmMessage::from_parts(value.message_type, ciphertext_bytes.as_slice()) .map_err(serde::de::Error::custom) } } impl OlmMessage { /// Create a `OlmMessage` from a message type and a ciphertext. - pub fn from_parts(message_type: usize, ciphertext: &str) -> Result { + pub fn from_parts(message_type: usize, ciphertext: &[u8]) -> Result { match message_type { - 0 => Ok(Self::PreKey(PreKeyMessage::try_from(ciphertext)?)), - 1 => Ok(Self::Normal(Message::try_from(ciphertext)?)), + 0 => Ok(Self::PreKey(PreKeyMessage::from_bytes(ciphertext)?)), + 1 => Ok(Self::Normal(Message::from_bytes(ciphertext)?)), m => Err(DecodeError::MessageType(m)), } } @@ -110,14 +110,13 @@ impl OlmMessage { } } - /// Convert the `OlmMessage` into a message type, and base64 encoded message - /// tuple. - pub fn to_parts(self) -> (usize, String) { + /// Convert the `OlmMessage` into a message type, and message bytes tuple. + pub fn to_parts(&self) -> (usize, Vec) { let message_type = self.message_type(); match self { - OlmMessage::Normal(m) => (message_type.into(), m.to_base64()), - OlmMessage::PreKey(m) => (message_type.into(), m.to_base64()), + OlmMessage::Normal(m) => (message_type.into(), m.to_bytes()), + OlmMessage::PreKey(m) => (message_type.into(), m.to_bytes()), } } } @@ -156,8 +155,10 @@ use olm_rs::session::OlmMessage as LibolmMessage; impl From for OlmMessage { fn from(other: LibolmMessage) -> Self { let (message_type, ciphertext) = other.to_tuple(); + let ciphertext_bytes = base64_decode(ciphertext).expect("Can't decode base64"); - Self::from_parts(message_type.into(), &ciphertext).expect("Can't decode a libolm message") + Self::from_parts(message_type.into(), ciphertext_bytes.as_slice()) + .expect("Can't decode a libolm message") } } @@ -247,7 +248,7 @@ mod tests { #[test] fn from_parts() -> Result<()> { - let message = OlmMessage::from_parts(0, PRE_KEY_MESSAGE)?; + let message = OlmMessage::from_parts(0, base64_decode(PRE_KEY_MESSAGE)?.as_slice())?; assert_matches!(message, OlmMessage::PreKey(_)); assert_eq!( message.message_type(), @@ -255,9 +256,13 @@ mod tests { "Expected message to be recognized as a pre-key Olm message." ); assert_eq!(message.message(), PRE_KEY_MESSAGE_CIPHERTEXT); - assert_eq!(message.to_parts(), (0, PRE_KEY_MESSAGE.to_string()), "Roundtrip not identity."); + assert_eq!( + message.to_parts(), + (0, base64_decode(PRE_KEY_MESSAGE)?), + "Roundtrip not identity." + ); - let message = OlmMessage::from_parts(1, MESSAGE)?; + let message = OlmMessage::from_parts(1, base64_decode(MESSAGE)?.as_slice())?; assert_matches!(message, OlmMessage::Normal(_)); assert_eq!( message.message_type(), @@ -265,9 +270,9 @@ mod tests { "Expected message to be recognized as a normal Olm message." ); assert_eq!(message.message(), MESSAGE_CIPHERTEXT); - assert_eq!(message.to_parts(), (1, MESSAGE.to_string()), "Roundtrip not identity."); + assert_eq!(message.to_parts(), (1, base64_decode(MESSAGE)?), "Roundtrip not identity."); - OlmMessage::from_parts(3, PRE_KEY_MESSAGE) + OlmMessage::from_parts(3, base64_decode(PRE_KEY_MESSAGE)?.as_slice()) .expect_err("Unknown message types can't be parsed"); Ok(()) diff --git a/src/pk_encryption.rs b/src/pk_encryption.rs index 24b234a5..a53f0ea9 100644 --- a/src/pk_encryption.rs +++ b/src/pk_encryption.rs @@ -15,10 +15,12 @@ //! ☣️ Compat support for libolm's PkEncryption and PkDecryption //! //! This implements the `m.megolm_backup.v1.curve25519-aes-sha2` described in -//! the Matrix [spec]. This is a asymmetric encrytpion scheme based on -//! Curve25519. +//! the Matrix [spec]. This is a hybrid encryption scheme utilizing Curve25519 +//! and AES-CBC. X25519 ECDH is performed between an ephemeral key pair and a +//! long-lived backup key pair to establish a shared secret, from which +//! symmetric encryption and message authentication (MAC) keys are derived. //! -//! **Warning**: Please note the algorithm contains a critical flaw and does not +//! **WARNING**: Please note the algorithm contains a critical flaw and does not //! provide authentication of the ciphertext. //! //! # Examples @@ -64,7 +66,8 @@ use crate::{ const PICKLE_VERSION: u32 = 1; -/// Error type describing the failure cases the Pk decryption step can have. +/// An error type describing failures which can happen during the decryption +/// step. #[derive(Debug, Error)] pub enum Error { /// The message has invalid [Pkcs7] padding. @@ -75,7 +78,7 @@ pub enum Error { Mac(#[from] MacError), } -/// Error describing failures that might happen during the decoding of a +/// An error type describing failures which can happen during the decoding of an /// encrypted [`Message`]. #[derive(Debug, Error)] pub enum MessageDecodeError { @@ -94,16 +97,16 @@ pub struct Message { pub ciphertext: Vec, /// The message authentication code of the message. /// - /// *Warning*: As stated in the module description, this does not + /// **WARNING**: As stated in the module description, this does not /// authenticate the message. pub mac: Vec, - /// The ephemeral [`Curve25519PublicKey`] of the message which was used to - /// derive the individual message key. + /// The ephemeral [`Curve25519PublicKey`] used to derive the individual + /// message key. pub ephemeral_key: Curve25519PublicKey, } impl Message { - /// Attempt to decode a PkEncryption [`Message`] from a Base64 encoded + /// Attempt to decode a PkEncryption [`Message`] from a Base64-encoded /// triplet of ciphertext, MAC, and ephemeral key. pub fn from_base64( ciphertext: &str, @@ -120,62 +123,48 @@ impl Message { /// The decryption component of the PkEncryption support. /// -/// This struct allows you to share a public key, enabling others to encrypt -/// messages that can be decrypted using the corresponding private key. +/// The public key can be shared with others, allowing them to encrypt messages +/// which can be decrypted using the corresponding private key. pub struct PkDecryption { - key: Curve25519SecretKey, + secret_key: Curve25519SecretKey, public_key: Curve25519PublicKey, } impl PkDecryption { /// Create a new random [`PkDecryption`] object. /// - /// This will create a new random [`Curve25519SecretKey`] which is used as - /// the long-term + /// This contains a fresh [`Curve25519SecretKey`] which is used as a + /// long-term key to derive individual message keys and effectively serves + /// as the decryption secret. pub fn new() -> Self { - let key = Curve25519SecretKey::new(); - let public_key = Curve25519PublicKey::from(&key); + let secret_key = Curve25519SecretKey::new(); + let public_key = Curve25519PublicKey::from(&secret_key); - Self { key, public_key } + Self { secret_key, public_key } } - /// Get the [`Curve25519PublicKey`] which is - pub const fn public_key(&self) -> Curve25519PublicKey { - self.public_key - } - - /// Decrypt a [`Message`] which was encrypted for this [`PkDecryption`] - /// object. - pub fn decrypt(&self, message: &Message) -> Result, Error> { - let shared_secret = self.key.diffie_hellman(&message.ephemeral_key); - - let expanded_keys = ExpandedKeys::new_helper(shared_secret.as_bytes(), b""); - let cipher_keys = CipherKeys::from_expanded_keys(expanded_keys); - - let hmac = HmacSha256::new_from_slice(cipher_keys.mac_key()) - .expect("We should be able to create a Hmac object from a 32 byte key"); - - // BUG: This is a know issue, we check the MAC of an empty message instead of - // updating the `hmac` object with the ciphertext bytes. - hmac.verify_truncated_left(&message.mac)?; - - let cipher = Aes256CbcDec::new(cipher_keys.aes_key(), cipher_keys.iv()); - let decrypted = cipher.decrypt_padded_vec_mut::(&message.ciphertext)?; - - Ok(decrypted) + /// Create a [`PkDecryption`] object from a [`Curve25519SecretKey`] key. + /// + /// The [`Curve25519SecretKey`] will be used as the long-term key to derive + /// individual message keys. + pub fn from_key(secret_key: Curve25519SecretKey) -> Self { + let public_key = Curve25519PublicKey::from(&secret_key); + Self { secret_key, public_key } } - /// Create a [`PkDecryption`] object from a slice of bytes. - pub fn from_bytes(bytes: &[u8; 32]) -> Self { - let key = Curve25519SecretKey::from_slice(bytes); - let public_key = Curve25519PublicKey::from(&key); - - Self { key, public_key } + /// Get the [`Curve25519SecretKey`] of this [`PkDecryption`] object. + /// + /// If persistence is required, securely serialize and store this key. It + /// can be used to reconstruct the [`PkDecryption`] object for decrypting + /// associated messages. + pub const fn secret_key(&self) -> &Curve25519SecretKey { + &self.secret_key } - /// Export this [`PkDecryption`] object to a slice of bytes. - pub fn to_bytes(&self) -> Box<[u8; 32]> { - self.key.to_bytes() + /// Get the associated ephemeral [`Curve25519PublicKey`]. This key can be + /// used to reconstruct the [`PkEncryption`] object to encrypt messages. + pub const fn public_key(&self) -> Curve25519PublicKey { + self.public_key } /// Create a [`PkDecryption`] object by unpickling a PkDecryption pickle in @@ -228,6 +217,27 @@ impl PkDecryption { use crate::utilities::pickle_libolm; pickle_libolm::(self.into(), pickle_key) } + + /// Decrypt a [`Message`] which was encrypted for this [`PkDecryption`] + /// object. + pub fn decrypt(&self, message: &Message) -> Result, Error> { + let shared_secret = self.secret_key.diffie_hellman(&message.ephemeral_key); + + let expanded_keys = ExpandedKeys::new_helper(shared_secret.as_bytes(), b""); + let cipher_keys = CipherKeys::from_expanded_keys(expanded_keys); + + let hmac = HmacSha256::new_from_slice(cipher_keys.mac_key()) + .expect("We should be able to create a Hmac object from a 32 byte key"); + + // BUG: This is a know issue, we check the MAC of an empty message instead of + // updating the `hmac` object with the ciphertext bytes. + hmac.verify_truncated_left(&message.mac)?; + + let cipher = Aes256CbcDec::new(cipher_keys.aes_key(), cipher_keys.iv()); + let decrypted = cipher.decrypt_padded_vec_mut::(&message.ciphertext)?; + + Ok(decrypted) + } } impl Default for PkDecryption { @@ -240,10 +250,10 @@ impl TryFrom for PkDecryption { type Error = crate::LibolmPickleError; fn try_from(pickle: PkDecryptionPickle) -> Result { - let key = Curve25519SecretKey::from_slice(&pickle.private_curve25519_key); - let public_key = Curve25519PublicKey::from(&key); + let secret_key = Curve25519SecretKey::from_slice(&pickle.private_curve25519_key); + let public_key = Curve25519PublicKey::from(&secret_key); - Ok(Self { key, public_key }) + Ok(Self { secret_key, public_key }) } } @@ -261,16 +271,15 @@ impl From<&PkDecryption> for PkDecryptionPickle { Self { version: PICKLE_VERSION, public_curve25519_key: decrypt.public_key.to_bytes(), - private_curve25519_key: decrypt.key.to_bytes(), + private_curve25519_key: decrypt.secret_key.to_bytes(), } } } /// The encryption component of PkEncryption support. /// -/// This struct can be created using a [`Curve25519PublicKey`] corresponding to -/// a [`PkDecryption`] object, allowing messages to be encrypted for the -/// associated decryption object. +/// This struct can be created from a [`Curve25519PublicKey`] corresponding to +/// a [`PkDecryption`] object, allowing encryption of messages for that object. pub struct PkEncryption { public_key: Curve25519PublicKey, } @@ -278,7 +287,8 @@ pub struct PkEncryption { impl PkEncryption { /// Create a new [`PkEncryption`] object from a [`Curve25519PublicKey`]. /// - /// The public key should come from an existing [`PkDecryption`] object. + /// The public key should be obtained from an existing [`PkDecryption`] + /// object. pub const fn from_key(public_key: Curve25519PublicKey) -> Self { Self { public_key } } @@ -323,7 +333,7 @@ mod tests { use olm_rs::pk::{OlmPkDecryption, OlmPkEncryption, PkMessage}; use super::{Message, MessageDecodeError, PkDecryption, PkEncryption}; - use crate::{base64_encode, Curve25519PublicKey}; + use crate::{base64_encode, Curve25519PublicKey, Curve25519SecretKey}; /// Conversion from the libolm type to the vodozemac type. To make some /// tests easier on the eyes. @@ -408,9 +418,10 @@ mod tests { #[test] fn from_bytes() { let decryption = PkDecryption::default(); - let bytes = decryption.to_bytes(); + let bytes = decryption.secret_key().to_bytes(); - let restored = PkDecryption::from_bytes(&bytes); + let secret_key = Curve25519SecretKey::from_slice(&bytes); + let restored = PkDecryption::from_key(secret_key); assert_eq!( decryption.public_key(),