Skip to content

Commit e3b8900

Browse files
committed
feat(sdk-crypto): Add Identity based room key sharing strategy
1 parent 90f92ac commit e3b8900

File tree

2 files changed

+139
-1
lines changed

2 files changed

+139
-1
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,18 @@ impl ReadOnlyDevice {
764764
)
765765
}
766766

767+
pub(crate) fn is_cross_signed_by_owner(
768+
&self,
769+
device_owner_identity: &ReadOnlyUserIdentities,
770+
) -> bool {
771+
match device_owner_identity {
772+
ReadOnlyUserIdentities::Own(identity) => identity.is_device_signed(self).is_ok(),
773+
ReadOnlyUserIdentities::Other(device_identity) => {
774+
device_identity.is_device_signed(self).is_ok()
775+
}
776+
}
777+
}
778+
767779
/// Encrypt the given content for this device.
768780
///
769781
/// # Arguments

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

Lines changed: 127 additions & 1 deletion
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,41 @@ 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+
let mut allowed_devices: Vec<ReadOnlyDevice> = Default::default();
249+
let mut denied_devices_with_code: Vec<(ReadOnlyDevice, WithheldCode)> = Default::default();
250+
251+
match device_owner_identity {
252+
None => {
253+
// withheld all the users devices, we need to have an identity for this
254+
// distribution mode
255+
denied_devices_with_code
256+
.extend(user_devices.into_values().map(|d| (d, WithheldCode::Unauthorised)));
257+
}
258+
Some(device_owner_identity) => {
259+
// Only accept devices signed by the current identity
260+
let (recipients, withheld_recipients): (
261+
Vec<ReadOnlyDevice>,
262+
Vec<(ReadOnlyDevice, WithheldCode)>,
263+
) = user_devices.into_values().partition_map(|d| {
264+
if d.is_cross_signed_by_owner(device_owner_identity) {
265+
Either::Left(d)
266+
} else {
267+
Either::Right((d, WithheldCode::Unauthorised))
268+
}
269+
});
270+
271+
allowed_devices.extend(recipients);
272+
denied_devices_with_code.extend(withheld_recipients);
273+
}
274+
}
275+
276+
RecipientDevices { allowed_devices, denied_devices_with_code }
277+
}
278+
228279
#[cfg(test)]
229280
mod tests {
230281

@@ -402,6 +453,81 @@ mod tests {
402453
assert_eq!(code.as_str(), WithheldCode::Unverified.as_str());
403454
}
404455

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

0 commit comments

Comments
 (0)