Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 76d1f8bd18 | |||
| 550f4c5fde | |||
| b3f07f4587 | |||
| 56980745b4 | |||
| 13c1d20482 | |||
| 7f3e144cb3 | |||
| fe8bd2fdf3 | |||
| aa67148247 | |||
| 769fcdb1fb | |||
| 6e628781c0 | |||
| a75a2b4113 |
Generated
+1
-1
@@ -2993,7 +2993,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "matrix-sdk-crypto"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ zeroize = "1.8.1"
|
||||
matrix-sdk = { path = "crates/matrix-sdk", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-base = { path = "crates/matrix-sdk-base", version = "0.11.0" }
|
||||
matrix-sdk-common = { path = "crates/matrix-sdk-common", version = "0.11.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.0" }
|
||||
matrix-sdk-crypto = { path = "crates/matrix-sdk-crypto", version = "0.11.1" }
|
||||
matrix-sdk-ffi-macros = { path = "bindings/matrix-sdk-ffi-macros", version = "0.7.0" }
|
||||
matrix-sdk-indexeddb = { path = "crates/matrix-sdk-indexeddb", version = "0.11.0", default-features = false }
|
||||
matrix-sdk-qrcode = { path = "crates/matrix-sdk-qrcode", version = "0.11.0" }
|
||||
|
||||
@@ -6,6 +6,19 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [Unreleased] - ReleaseDate
|
||||
|
||||
## [0.11.1] - 2025-06-10
|
||||
|
||||
### Security Fixes
|
||||
- Check the sender of an event matches owner of session, preventing sender
|
||||
spoofing by homeserver owners.
|
||||
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
|
||||
|
||||
### Bug Fixes
|
||||
- Remove a wildcard enum variant import which breaks compilation if used with
|
||||
`tracing-attributes` version `0.1.29`. This is a workaround for a bug in
|
||||
`tracing-attributes`.
|
||||
([#5190](https://github.com/matrix-org/matrix-rust-sdk/issues/5190)) ([#5191](https://github.com/matrix-org/matrix-rust-sdk/issues/5191)) ([#5193](https://github.com/matrix-org/matrix-rust-sdk/issues/5193))
|
||||
|
||||
## [0.11.0] - 2025-04-11
|
||||
|
||||
### Features
|
||||
|
||||
@@ -9,7 +9,7 @@ name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
rust-version = { workspace = true }
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
@@ -32,7 +32,10 @@ use tracing::{debug, enabled, info, instrument, trace, warn, Level};
|
||||
use crate::{
|
||||
error::OlmResult,
|
||||
identities::{DeviceData, OtherUserIdentityData, OwnUserIdentityData, UserIdentityData},
|
||||
olm::{InboundGroupSession, PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType},
|
||||
olm::{
|
||||
sender_data_finder::SessionDeviceCheckError, InboundGroupSession,
|
||||
PrivateCrossSigningIdentity, SenderDataFinder, SenderDataType,
|
||||
},
|
||||
store::{
|
||||
caches::SequenceNumber, Changes, DeviceChanges, IdentityChanges, KeyQueryManager,
|
||||
Result as StoreResult, Store, StoreCache, StoreCacheGuard, UserKeyQueryResult,
|
||||
@@ -1124,8 +1127,6 @@ impl IdentityManager {
|
||||
session: &mut InboundGroupSession,
|
||||
device: &DeviceData,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
use crate::olm::sender_data_finder::SessionDeviceCheckError::*;
|
||||
|
||||
match SenderDataFinder::find_using_device_data(&self.store, device.clone(), session).await {
|
||||
Ok(sender_data) => {
|
||||
debug!(
|
||||
@@ -1134,10 +1135,10 @@ impl IdentityManager {
|
||||
);
|
||||
session.sender_data = sender_data;
|
||||
}
|
||||
Err(CryptoStoreError(e)) => {
|
||||
Err(SessionDeviceCheckError::CryptoStoreError(e)) => {
|
||||
return Err(e);
|
||||
}
|
||||
Err(MismatchedIdentityKeys(e)) => {
|
||||
Err(SessionDeviceCheckError::MismatchedIdentityKeys(e)) => {
|
||||
warn!(
|
||||
?session,
|
||||
?device,
|
||||
|
||||
@@ -1521,20 +1521,72 @@ impl OlmMachine {
|
||||
self.inner.key_request_machine.request_key(room_id, &event).await
|
||||
}
|
||||
|
||||
/// Find whether the supplied session is verified, and provide
|
||||
/// explanation of what is missing/wrong if not.
|
||||
/// Find whether an event decrypted via the supplied session is verified,
|
||||
/// and provide explanation of what is missing/wrong if not.
|
||||
///
|
||||
/// Stores the updated [`SenderData`] for the session in the store
|
||||
/// if we find an updated value for it.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session` - The inbound Megolm session that was used to decrypt the
|
||||
/// event.
|
||||
/// * `sender` - The `sender` of that event (as claimed by the envelope of
|
||||
/// the event).
|
||||
async fn get_room_event_verification_state(
|
||||
&self,
|
||||
session: &InboundGroupSession,
|
||||
sender: &UserId,
|
||||
) -> MegolmResult<(VerificationState, Option<OwnedDeviceId>)> {
|
||||
let sender_data = self.get_or_update_sender_data(session, sender).await?;
|
||||
|
||||
// If the user ID in the sender data doesn't match that in the event envelope,
|
||||
// this event is not from who it appears to be from.
|
||||
//
|
||||
// If `sender_data.user_id()` returns `None`, that means we don't have any
|
||||
// information about the owner of the session (i.e. we have
|
||||
// `SenderData::UnknownDevice`); in that case we fall through to the
|
||||
// logic in `sender_data_to_verification_state` which will pick an appropriate
|
||||
// `DeviceLinkProblem` for `VerificationLevel::None`.
|
||||
let (verification_state, device_id) = match sender_data.user_id() {
|
||||
Some(i) if i != sender => {
|
||||
// For backwards compatibility, we treat this the same as "Unknown device".
|
||||
// TODO: use a dedicated VerificationLevel here.
|
||||
(
|
||||
VerificationState::Unverified(VerificationLevel::None(
|
||||
DeviceLinkProblem::MissingDevice,
|
||||
)),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
Some(_) | None => {
|
||||
sender_data_to_verification_state(sender_data, session.has_been_imported())
|
||||
}
|
||||
};
|
||||
|
||||
Ok((verification_state, device_id))
|
||||
}
|
||||
|
||||
/// Get an up-to-date [`SenderData`] for the given session, suitable for
|
||||
/// determining if messages decrypted using that session are verified.
|
||||
///
|
||||
/// Checks both the stored verification state of the session and a
|
||||
/// recalculated verification state based on our current knowledge, and
|
||||
/// returns the more trusted of the two.
|
||||
///
|
||||
/// Store the updated [`SenderData`] for this session in the store
|
||||
/// Stores the updated [`SenderData`] for the session in the store
|
||||
/// if we find an updated value for it.
|
||||
async fn get_or_update_verification_state(
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session` - The Megolm session that was used to decrypt the event.
|
||||
/// * `sender` - The claimed sender of that event.
|
||||
async fn get_or_update_sender_data(
|
||||
&self,
|
||||
session: &InboundGroupSession,
|
||||
sender: &UserId,
|
||||
) -> MegolmResult<(VerificationState, Option<OwnedDeviceId>)> {
|
||||
) -> MegolmResult<SenderData> {
|
||||
/// Whether we should recalculate the Megolm sender's data, given the
|
||||
/// current sender data. We only want to recalculate if it might
|
||||
/// increase trust and allow us to decrypt messages that we
|
||||
@@ -1555,7 +1607,24 @@ impl OlmMachine {
|
||||
}
|
||||
|
||||
let sender_data = if should_recalculate_sender_data(&session.sender_data) {
|
||||
// The session is not sure of the sender yet. Calculate it.
|
||||
// The session is not sure of the sender yet. Try to find a matching device
|
||||
// belonging to the claimed sender of the recently-received event.
|
||||
//
|
||||
// It's worth noting that this could in theory result in unintuitive changes,
|
||||
// like a session which initially appears to belong to Alice turning into a
|
||||
// session which belongs to Bob [1]. This could mean that a session initially
|
||||
// successfully decrypts events from Alice, but then stops decrypting those same
|
||||
// events once we get an update.
|
||||
//
|
||||
// That's ok though: if we get good evidence that the session belongs to Bob,
|
||||
// it's correct to update the session even if we previously had weak
|
||||
// evidence it belonged to Alice.
|
||||
//
|
||||
// [1] For example: maybe Alice and Bob both publish devices with the *same*
|
||||
// keys (presumably because they are colluding). Initially we think
|
||||
// the session belongs to Alice, but then we do a device lookup for
|
||||
// Bob, we find a matching device with a cross-signature, so prefer
|
||||
// that.
|
||||
let calculated_sender_data = SenderDataFinder::find_using_curve_key(
|
||||
self.store(),
|
||||
session.sender_key(),
|
||||
@@ -1581,7 +1650,7 @@ impl OlmMachine {
|
||||
session.sender_data.clone()
|
||||
};
|
||||
|
||||
Ok(sender_data_to_verification_state(sender_data, session.has_been_imported()))
|
||||
Ok(sender_data)
|
||||
}
|
||||
|
||||
/// Request missing local secrets from our devices (cross signing private
|
||||
@@ -1654,7 +1723,7 @@ impl OlmMachine {
|
||||
sender: &UserId,
|
||||
) -> MegolmResult<EncryptionInfo> {
|
||||
let (verification_state, device_id) =
|
||||
self.get_or_update_verification_state(session, sender).await?;
|
||||
self.get_room_event_verification_state(session, sender).await?;
|
||||
|
||||
let sender = sender.to_owned();
|
||||
|
||||
@@ -2074,7 +2143,7 @@ impl OlmMachine {
|
||||
self.get_session_encryption_info(room_id, content.session_id(), &event.sender).await
|
||||
}
|
||||
|
||||
/// Get encryption info for a megolm session.
|
||||
/// Get encryption info for an event decrypted with a megolm session.
|
||||
///
|
||||
/// This recalculates the [`EncryptionInfo`] data that is returned by
|
||||
/// [`OlmMachine::decrypt_room_event`], based on the current
|
||||
@@ -2086,7 +2155,8 @@ impl OlmMachine {
|
||||
///
|
||||
/// * `room_id` - The ID of the room where the session is being used.
|
||||
/// * `session_id` - The ID of the session to get information for.
|
||||
/// * `sender` - The user ID of the sender who created this session.
|
||||
/// * `sender` - The (claimed) sender of the event where the session was
|
||||
/// used.
|
||||
pub async fn get_session_encryption_info(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
|
||||
@@ -311,6 +311,69 @@ pub async fn mark_alice_identity_as_verified_test_helper(alice: &OlmMachine, bob
|
||||
.is_verified());
|
||||
}
|
||||
|
||||
/// Test that the verification state is set correctly when the sender of an
|
||||
/// event does not match the owner of the device that sent us the session.
|
||||
///
|
||||
/// In this test, Bob receives an event from Alice, but the HS admin has
|
||||
/// rewritten the `sender` of the event to look like another user.
|
||||
#[async_test]
|
||||
async fn test_verification_states_spoofed_sender() {
|
||||
let (alice, bob) = get_machine_pair_with_setup_sessions_test_helper(
|
||||
tests::alice_id(),
|
||||
tests::user_id(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let room_id = room_id!("!test:example.org");
|
||||
let decryption_settings =
|
||||
DecryptionSettings { sender_device_trust_requirement: TrustRequirement::Untrusted };
|
||||
|
||||
// Alice sends a message to Bob.
|
||||
let (event, _) = encrypt_message(&alice, room_id, &bob, "Secret message").await;
|
||||
bob.decrypt_room_event(&event, room_id, &decryption_settings)
|
||||
.await
|
||||
.expect("Bob could not decrypt event");
|
||||
let event_encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
|
||||
assert_matches!(
|
||||
event_encryption_info.verification_state,
|
||||
VerificationState::Unverified(VerificationLevel::UnsignedDevice)
|
||||
);
|
||||
|
||||
// Alice now sends a second message to Bob, using the same room key, but the HS
|
||||
// admin rewrites the 'sender' to Charlie.
|
||||
let encrypted_content = alice
|
||||
.encrypt_room_event(
|
||||
room_id,
|
||||
AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::text_plain(
|
||||
"spoofed message",
|
||||
)),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let event = json!({
|
||||
"event_id": "$xxxxy:example.org",
|
||||
"origin_server_ts": MilliSecondsSinceUnixEpoch::now(),
|
||||
"sender": "@charlie:example.org", // Note! spoofed sender
|
||||
"type": "m.room.encrypted",
|
||||
"content": encrypted_content,
|
||||
});
|
||||
let event = json_convert(&event).unwrap();
|
||||
|
||||
bob.decrypt_room_event(&event, room_id, &decryption_settings)
|
||||
.await
|
||||
.expect("Bob could not decrypt spoofed event");
|
||||
|
||||
// The verification_state of the event should be `MissingDevice` (since it
|
||||
// manifests as a message from Charlie which does not correspond to one of
|
||||
// Charlie's devices).
|
||||
let event_encryption_info = bob.get_room_event_encryption_info(&event, room_id).await.unwrap();
|
||||
assert_matches!(
|
||||
event_encryption_info.verification_state,
|
||||
VerificationState::Unverified(VerificationLevel::None(DeviceLinkProblem::MissingDevice))
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_verification_states_multiple_device() {
|
||||
let (bob, _) = get_prepared_machine_test_helper(tests::user_id(), false).await;
|
||||
@@ -358,7 +421,7 @@ async fn test_verification_states_multiple_device() {
|
||||
.unwrap();
|
||||
|
||||
let (state, _) = bob
|
||||
.get_or_update_verification_state(&web_unverified_inbound_session, other_user_id)
|
||||
.get_room_event_verification_state(&web_unverified_inbound_session, other_user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(VerificationState::Unverified(VerificationLevel::UnsignedDevice), state);
|
||||
@@ -376,7 +439,7 @@ async fn test_verification_states_multiple_device() {
|
||||
.unwrap();
|
||||
|
||||
let (state, _) = bob
|
||||
.get_or_update_verification_state(&web_signed_inbound_session, other_user_id)
|
||||
.get_room_event_verification_state(&web_signed_inbound_session, other_user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -252,6 +252,26 @@ impl SenderData {
|
||||
Self::SenderVerified { .. } => SenderDataType::SenderVerified,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return our best guess of the owner of the associated megolm session.
|
||||
///
|
||||
/// For `SenderData::UnknownDevice`, we don't record any information about
|
||||
/// the owner of the sender, so returns `None`.
|
||||
pub(crate) fn user_id(&self) -> Option<OwnedUserId> {
|
||||
match &self {
|
||||
SenderData::UnknownDevice { .. } => None,
|
||||
SenderData::DeviceInfo { device_keys, .. } => Some(device_keys.user_id.clone()),
|
||||
SenderData::VerificationViolation(known_sender_data) => {
|
||||
Some(known_sender_data.user_id.clone())
|
||||
}
|
||||
SenderData::SenderUnverified(known_sender_data) => {
|
||||
Some(known_sender_data.user_id.clone())
|
||||
}
|
||||
SenderData::SenderVerified(known_sender_data) => {
|
||||
Some(known_sender_data.user_id.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used when deserialising and the sender_data property is missing.
|
||||
|
||||
@@ -1375,12 +1375,18 @@ impl Room {
|
||||
Ok(event)
|
||||
}
|
||||
|
||||
/// Fetches the [`EncryptionInfo`] for the supplied session_id.
|
||||
/// Fetches the [`EncryptionInfo`] for an event decrypted with the supplied
|
||||
/// session_id.
|
||||
///
|
||||
/// This may be used when we receive an update for a session, and we want to
|
||||
/// reflect the changes in messages we have received that were encrypted
|
||||
/// with that session, e.g. to remove a warning shield because a device is
|
||||
/// now verified.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `session_id` - The ID of the Megolm session to get information for.
|
||||
/// * `sender` - The (claimed) sender of the event where the session was
|
||||
/// used.
|
||||
#[cfg(feature = "e2e-encryption")]
|
||||
pub async fn get_encryption_info(
|
||||
&self,
|
||||
|
||||
+38
-17
@@ -1,5 +1,5 @@
|
||||
use clap::{Args, Subcommand, ValueEnum};
|
||||
use xshell::cmd;
|
||||
use xshell::{cmd, Cmd};
|
||||
|
||||
use crate::{sh, Result};
|
||||
|
||||
@@ -13,15 +13,19 @@ pub struct ReleaseArgs {
|
||||
enum ReleaseCommand {
|
||||
/// Prepare the release of the matrix-sdk workspace.
|
||||
///
|
||||
/// This command will update the `README.md`, prepend the `CHANGELOG.md`
|
||||
/// file using `git cliff`, and bump the versions in the `Cargo.toml`
|
||||
/// files.
|
||||
/// This command will update the `README.md`, update the `CHANGELOG.md` file
|
||||
/// using, and bump the versions in the `Cargo.toml` files.
|
||||
Prepare {
|
||||
/// What type of version bump we should perform.
|
||||
version: ReleaseVersion,
|
||||
/// Actually prepare a release. Dry-run mode is the default.
|
||||
#[clap(long)]
|
||||
execute: bool,
|
||||
/// The crate or package that should be released. Use this if you'd like
|
||||
/// to release only one specific crate. The default is to
|
||||
/// release all crates.
|
||||
#[clap(long)]
|
||||
package: Option<String>,
|
||||
},
|
||||
/// Publish the release.
|
||||
///
|
||||
@@ -31,6 +35,11 @@ enum ReleaseCommand {
|
||||
/// Actually publish a release. Dry-run mode is the default
|
||||
#[clap(long)]
|
||||
execute: bool,
|
||||
/// The crate or package that should be released. Use this if you'd like
|
||||
/// to release only one specific crate. The default is to
|
||||
/// release all crates.
|
||||
#[clap(long)]
|
||||
package: Option<String>,
|
||||
},
|
||||
/// Get a list of interesting changes that happened in the last week.
|
||||
WeeklyReport,
|
||||
@@ -63,8 +72,10 @@ impl ReleaseArgs {
|
||||
check_prerequisites();
|
||||
|
||||
match self.cmd {
|
||||
ReleaseCommand::Prepare { version, execute } => prepare(version, execute),
|
||||
ReleaseCommand::Publish { execute } => publish(execute),
|
||||
ReleaseCommand::Prepare { version, execute, package } => {
|
||||
prepare(version, execute, package)
|
||||
}
|
||||
ReleaseCommand::Publish { execute, package } => publish(execute, package),
|
||||
ReleaseCommand::WeeklyReport => weekly_report(),
|
||||
}
|
||||
}
|
||||
@@ -88,11 +99,21 @@ fn check_prerequisites() {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> {
|
||||
let sh = sh();
|
||||
let cmd = cmd!(sh, "cargo release --workspace --no-publish --no-tag --no-push");
|
||||
fn append_options<'a>(command: Cmd<'a>, execute: &bool, package: &Option<String>) -> Cmd<'a> {
|
||||
let command = if *execute { command.arg("--execute") } else { command };
|
||||
if let Some(package) = package.as_deref() {
|
||||
command.args(["--package", package])
|
||||
} else {
|
||||
command.arg("--workspace")
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare(version: ReleaseVersion, execute: bool, package: Option<String>) -> Result<()> {
|
||||
let sh = sh();
|
||||
|
||||
let cmd = cmd!(sh, "cargo release --no-publish --no-tag --no-push");
|
||||
let cmd = append_options(cmd, &execute, &package);
|
||||
|
||||
let cmd = if execute { cmd.arg("--execute") } else { cmd };
|
||||
let cmd = cmd.arg(version.as_str());
|
||||
|
||||
cmd.run()?;
|
||||
@@ -108,19 +129,19 @@ fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn publish(execute: bool) -> Result<()> {
|
||||
fn publish(execute: bool, package: Option<String>) -> Result<()> {
|
||||
let sh = sh();
|
||||
|
||||
let cmd = cmd!(sh, "cargo release tag --workspace");
|
||||
let cmd = if execute { cmd.arg("--execute") } else { cmd };
|
||||
let cmd = cmd!(sh, "cargo release tag");
|
||||
let cmd = append_options(cmd, &execute, &package);
|
||||
cmd.run()?;
|
||||
|
||||
let cmd = cmd!(sh, "cargo release publish --workspace");
|
||||
let cmd = if execute { cmd.arg("--execute") } else { cmd };
|
||||
let cmd = cmd!(sh, "cargo release publish");
|
||||
let cmd = append_options(cmd, &execute, &package);
|
||||
cmd.run()?;
|
||||
|
||||
let cmd = cmd!(sh, "cargo release push --workspace");
|
||||
let cmd = if execute { cmd.arg("--execute") } else { cmd };
|
||||
let cmd = cmd!(sh, "cargo release push");
|
||||
let cmd = append_options(cmd, &execute, &package);
|
||||
cmd.run()?;
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user