Skip to content

Commit d8b2b74

Browse files
authored
feat(sdk-crypto): Add Identity based room key sharing strategy (#3607)
This sharing strategy is defined as part of MSC4153[1]. [1]: matrix-org/matrix-spec-proposals#4153
1 parent cdc3743 commit d8b2b74

File tree

2 files changed

+149
-13
lines changed

2 files changed

+149
-13
lines changed

crates/matrix-sdk-crypto/src/identities/device.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -279,16 +279,9 @@ impl Device {
279279

280280
/// Is this device cross signed by its owner?
281281
pub fn is_cross_signed_by_owner(&self) -> bool {
282-
self.device_owner_identity.as_ref().is_some_and(|device_identity| match device_identity {
283-
// If it's one of our own devices, just check that
284-
// we signed the device.
285-
ReadOnlyUserIdentities::Own(identity) => identity.is_device_signed(&self.inner).is_ok(),
286-
// If it's a device from someone else, check
287-
// if the other user has signed this device.
288-
ReadOnlyUserIdentities::Other(device_identity) => {
289-
device_identity.is_device_signed(&self.inner).is_ok()
290-
}
291-
})
282+
self.device_owner_identity
283+
.as_ref()
284+
.is_some_and(|owner_identity| self.inner.is_cross_signed_by_owner(owner_identity))
292285
}
293286

294287
/// Is the device owner verified by us?
@@ -771,6 +764,22 @@ impl ReadOnlyDevice {
771764
)
772765
}
773766

767+
pub(crate) fn is_cross_signed_by_owner(
768+
&self,
769+
device_owner_identity: &ReadOnlyUserIdentities,
770+
) -> bool {
771+
match device_owner_identity {
772+
// If it's one of our own devices, just check that
773+
// we signed the device.
774+
ReadOnlyUserIdentities::Own(identity) => identity.is_device_signed(self).is_ok(),
775+
// If it's a device from someone else, check
776+
// if the other user has signed this device.
777+
ReadOnlyUserIdentities::Other(device_identity) => {
778+
device_identity.is_device_signed(self).is_ok()
779+
}
780+
}
781+
}
782+
774783
/// Encrypt the given content for this device.
775784
///
776785
/// # Arguments

crates/matrix-sdk-crypto/src/session_manager/group_sessions/share_strategy.rs

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,23 @@ pub enum CollectStrategy {
4242
/// trusted via interactive verification.
4343
/// - It is the current own device of the user.
4444
only_allow_trusted_devices: bool,
45-
}, // XXX some new strategy to be defined later
45+
},
46+
/// Share based on identity. Only distribute to devices signed by their
47+
/// owner. If a user has no published identity he will not receive
48+
/// any room keys.
49+
IdentityBasedStrategy,
4650
}
4751

4852
impl CollectStrategy {
4953
/// Creates a new legacy strategy, based on per device trust.
5054
pub const fn new_device_based(only_allow_trusted_devices: bool) -> Self {
5155
CollectStrategy::DeviceBasedStrategy { only_allow_trusted_devices }
5256
}
57+
58+
/// Creates an identity based strategy
59+
pub const fn new_identity_based() -> Self {
60+
CollectStrategy::IdentityBasedStrategy
61+
}
5362
}
5463

5564
impl Default for CollectStrategy {
@@ -136,6 +145,13 @@ pub(crate) async fn collect_session_recipients(
136145
only_allow_trusted_devices,
137146
)
138147
}
148+
CollectStrategy::IdentityBasedStrategy => {
149+
let device_owner_identity = store.get_user_identity(user_id).await?;
150+
split_recipients_withhelds_for_user_based_on_identity(
151+
user_devices,
152+
&device_owner_identity,
153+
)
154+
}
139155
};
140156

141157
let recipients = recipient_devices.allowed_devices;
@@ -225,6 +241,42 @@ fn split_recipients_withhelds_for_user(
225241
RecipientDevices { allowed_devices: recipients, denied_devices_with_code: withheld_recipients }
226242
}
227243

244+
fn split_recipients_withhelds_for_user_based_on_identity(
245+
user_devices: HashMap<OwnedDeviceId, ReadOnlyDevice>,
246+
device_owner_identity: &Option<ReadOnlyUserIdentities>,
247+
) -> RecipientDevices {
248+
match device_owner_identity {
249+
None => {
250+
// withheld all the users devices, we need to have an identity for this
251+
// distribution mode
252+
RecipientDevices {
253+
allowed_devices: Vec::default(),
254+
denied_devices_with_code: user_devices
255+
.into_values()
256+
.map(|d| (d, WithheldCode::Unauthorised))
257+
.collect(),
258+
}
259+
}
260+
Some(device_owner_identity) => {
261+
// Only accept devices signed by the current identity
262+
let (recipients, withheld_recipients): (
263+
Vec<ReadOnlyDevice>,
264+
Vec<(ReadOnlyDevice, WithheldCode)>,
265+
) = user_devices.into_values().partition_map(|d| {
266+
if d.is_cross_signed_by_owner(device_owner_identity) {
267+
Either::Left(d)
268+
} else {
269+
Either::Right((d, WithheldCode::Unauthorised))
270+
}
271+
});
272+
RecipientDevices {
273+
allowed_devices: recipients,
274+
denied_devices_with_code: withheld_recipients,
275+
}
276+
}
277+
}
278+
}
279+
228280
#[cfg(test)]
229281
mod tests {
230282

@@ -391,15 +443,90 @@ mod tests {
391443
.find(|(d, _)| d.device_id() == KeyDistributionTestData::dan_unsigned_device_id())
392444
.expect("This dan's device should receive a withheld code");
393445

394-
assert_eq!(code.as_str(), WithheldCode::Unverified.as_str());
446+
assert_eq!(code, &WithheldCode::Unverified);
395447

396448
let (_, code) = share_result
397449
.withheld_devices
398450
.iter()
399451
.find(|(d, _)| d.device_id() == KeyDistributionTestData::dave_device_id())
400452
.expect("This daves's device should receive a withheld code");
401453

402-
assert_eq!(code.as_str(), WithheldCode::Unverified.as_str());
454+
assert_eq!(code, &WithheldCode::Unverified);
455+
}
456+
457+
#[async_test]
458+
async fn test_share_with_identity_strategy() {
459+
let machine = set_up_test_machine().await;
460+
461+
let fake_room_id = room_id!("!roomid:localhost");
462+
463+
let strategy = CollectStrategy::new_identity_based();
464+
465+
let encryption_settings =
466+
EncryptionSettings { sharing_strategy: strategy.clone(), ..Default::default() };
467+
468+
let id_keys = machine.identity_keys();
469+
let group_session = OutboundGroupSession::new(
470+
machine.device_id().into(),
471+
Arc::new(id_keys),
472+
fake_room_id,
473+
encryption_settings.clone(),
474+
)
475+
.unwrap();
476+
477+
let share_result = collect_session_recipients(
478+
machine.store(),
479+
vec![
480+
KeyDistributionTestData::dan_id(),
481+
KeyDistributionTestData::dave_id(),
482+
KeyDistributionTestData::good_id(),
483+
]
484+
.into_iter(),
485+
&encryption_settings,
486+
&group_session,
487+
)
488+
.await
489+
.unwrap();
490+
491+
assert!(!share_result.should_rotate);
492+
493+
let dave_devices_shared = share_result.devices.get(KeyDistributionTestData::dave_id());
494+
let good_devices_shared = share_result.devices.get(KeyDistributionTestData::good_id());
495+
// dave has no published identity so will not receive the key
496+
assert!(dave_devices_shared.unwrap().is_empty());
497+
498+
// @good has properly signed his devices, he should get the keys
499+
assert_eq!(good_devices_shared.unwrap().len(), 2);
500+
501+
// dan has one of his devices self signed, so should get
502+
// the key
503+
let dan_devices_shared =
504+
share_result.devices.get(KeyDistributionTestData::dan_id()).unwrap();
505+
506+
assert_eq!(dan_devices_shared.len(), 1);
507+
let dan_device_that_will_get_the_key = &dan_devices_shared[0];
508+
assert_eq!(
509+
dan_device_that_will_get_the_key.device_id().as_str(),
510+
KeyDistributionTestData::dan_signed_device_id()
511+
);
512+
513+
// Check withhelds for others
514+
let (_, code) = share_result
515+
.withheld_devices
516+
.iter()
517+
.find(|(d, _)| d.device_id() == KeyDistributionTestData::dan_unsigned_device_id())
518+
.expect("This dan's device should receive a withheld code");
519+
520+
assert_eq!(code, &WithheldCode::Unauthorised);
521+
522+
// Check withhelds for others
523+
let (_, code) = share_result
524+
.withheld_devices
525+
.iter()
526+
.find(|(d, _)| d.device_id() == KeyDistributionTestData::dave_device_id())
527+
.expect("This dave device should receive a withheld code");
528+
529+
assert_eq!(code, &WithheldCode::Unauthorised);
403530
}
404531

405532
#[async_test]

0 commit comments

Comments
 (0)