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 @@
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(),