Skip to content

Commit 7af1d3a

Browse files
authored
Merge pull request #5539 from matrix-org/kaylendog/msc3414/crypto
feat(crypto): Add support for encrypted state events to `matrix-sdk-crypto`
2 parents 7bbd02c + 13ee4c8 commit 7af1d3a

File tree

8 files changed

+471
-24
lines changed

8 files changed

+471
-24
lines changed

crates/matrix-sdk-common/src/deserialized_responses.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,6 +1003,11 @@ pub enum UnableToDecryptReason {
10031003
/// cross-signing identity did not satisfy the requested
10041004
/// `TrustRequirement`.
10051005
SenderIdentityNotTrusted(VerificationLevel),
1006+
1007+
/// The outer state key could not be verified against the inner encrypted
1008+
/// state key and type.
1009+
#[cfg(feature = "experimental-encrypted-state-events")]
1010+
StateKeyVerificationFailed,
10061011
}
10071012

10081013
impl UnableToDecryptReason {

crates/matrix-sdk-crypto/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ pub enum MegolmError {
133133
/// The nested value is the sender's current verification level.
134134
#[error("decryption failed because trust requirement not satisfied: {0}")]
135135
SenderIdentityNotTrusted(VerificationLevel),
136+
137+
/// The outer state key could not be verified against the inner encrypted
138+
/// state key and type.
139+
#[cfg(feature = "experimental-encrypted-state-events")]
140+
#[error("decryption failed because the state key failed to validate")]
141+
StateKeyVerificationFailed,
136142
}
137143

138144
/// Decryption failed because of a mismatch between the identity keys of the

crates/matrix-sdk-crypto/src/gossiping/machine.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,8 @@ mod tests {
13641364
EncryptedEvent {
13651365
sender: sender.to_owned(),
13661366
event_id: event_id!("$143273582443PhrSn:example.org").to_owned(),
1367+
#[cfg(feature = "experimental-encrypted-state-events")]
1368+
state_key: None,
13671369
content,
13681370
origin_server_ts: ruma::MilliSecondsSinceUnixEpoch::now(),
13691371
unsigned: Default::default(),

crates/matrix-sdk-crypto/src/machine/mod.rs

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
#[cfg(feature = "experimental-encrypted-state-events")]
16+
use std::borrow::Borrow;
1517
use std::{
1618
collections::{BTreeMap, HashMap, HashSet},
1719
sync::Arc,
@@ -31,6 +33,8 @@ use matrix_sdk_common::{
3133
locks::RwLock as StdRwLock,
3234
BoxFuture,
3335
};
36+
#[cfg(feature = "experimental-encrypted-state-events")]
37+
use ruma::events::{AnyStateEventContent, StateEventContent};
3438
use ruma::{
3539
api::client::{
3640
dehydrated_device::DehydratedDeviceData,
@@ -1102,6 +1106,66 @@ impl OlmMachine {
11021106
self.inner.group_session_manager.encrypt(room_id, event_type, content).await
11031107
}
11041108

1109+
/// Encrypt a state event for the given room.
1110+
///
1111+
/// # Arguments
1112+
///
1113+
/// * `room_id` - The id of the room for which the event should be
1114+
/// encrypted.
1115+
///
1116+
/// * `content` - The plaintext content of the event that should be
1117+
/// encrypted.
1118+
///
1119+
/// * `state_key` - The associated state key of the event.
1120+
#[cfg(feature = "experimental-encrypted-state-events")]
1121+
pub async fn encrypt_state_event<C, K>(
1122+
&self,
1123+
room_id: &RoomId,
1124+
content: C,
1125+
state_key: K,
1126+
) -> MegolmResult<Raw<RoomEncryptedEventContent>>
1127+
where
1128+
C: StateEventContent,
1129+
C::StateKey: Borrow<K>,
1130+
K: AsRef<str>,
1131+
{
1132+
let event_type = content.event_type().to_string();
1133+
let content = Raw::new(&content)?.cast_unchecked();
1134+
self.encrypt_state_event_raw(room_id, &event_type, state_key.as_ref(), &content).await
1135+
}
1136+
1137+
/// Encrypt a state event for the given state event using its raw JSON
1138+
/// content and state key.
1139+
///
1140+
/// This method is equivalent to [`OlmMachine::encrypt_state_event`]
1141+
/// method but operates on an arbitrary JSON value instead of strongly-typed
1142+
/// event content struct.
1143+
///
1144+
/// # Arguments
1145+
///
1146+
/// * `room_id` - The id of the room for which the message should be
1147+
/// encrypted.
1148+
///
1149+
/// * `event_type` - The type of the event.
1150+
///
1151+
/// * `state_key` - The associated state key of the event.
1152+
///
1153+
/// * `content` - The plaintext content of the event that should be
1154+
/// encrypted as a raw JSON value.
1155+
#[cfg(feature = "experimental-encrypted-state-events")]
1156+
pub async fn encrypt_state_event_raw(
1157+
&self,
1158+
room_id: &RoomId,
1159+
event_type: &str,
1160+
state_key: &str,
1161+
content: &Raw<AnyStateEventContent>,
1162+
) -> MegolmResult<Raw<RoomEncryptedEventContent>> {
1163+
self.inner
1164+
.group_session_manager
1165+
.encrypt_state(room_id, event_type, state_key, content)
1166+
.await
1167+
}
1168+
11051169
/// Forces the currently active room key, which is used to encrypt messages,
11061170
/// to be rotated.
11071171
///
@@ -2197,9 +2261,72 @@ impl OlmMachine {
21972261
.await;
21982262
}
21992263

2200-
let event = serde_json::from_value::<Raw<AnyTimelineEvent>>(decrypted_event.into())?;
2264+
let decrypted_event =
2265+
serde_json::from_value::<Raw<AnyTimelineEvent>>(decrypted_event.into())?;
2266+
2267+
#[cfg(feature = "experimental-encrypted-state-events")]
2268+
self.verify_packed_state_key(&event, &decrypted_event)?;
22012269

2202-
Ok(DecryptedRoomEvent { event, encryption_info, unsigned_encryption_info })
2270+
Ok(DecryptedRoomEvent { event: decrypted_event, encryption_info, unsigned_encryption_info })
2271+
}
2272+
2273+
/// If the passed event is a state event, verify its outer packed state key
2274+
/// matches the inner state key once unpacked.
2275+
///
2276+
/// * `original` - The original encrypted event received over the wire.
2277+
/// * `decrypted` - The decrypted event.
2278+
///
2279+
/// # Errors
2280+
///
2281+
/// Returns an error if any of the following are true:
2282+
///
2283+
/// * The original event's state key failed to unpack;
2284+
/// * The decrypted event could not be deserialised;
2285+
/// * The unpacked event type does not match the type of the decrypted
2286+
/// event;
2287+
/// * The unpacked event state key does not match the state key of the
2288+
/// decrypted event.
2289+
#[cfg(feature = "experimental-encrypted-state-events")]
2290+
fn verify_packed_state_key(
2291+
&self,
2292+
original: &EncryptedEvent,
2293+
decrypted: &Raw<AnyTimelineEvent>,
2294+
) -> MegolmResult<()> {
2295+
use serde::Deserialize;
2296+
2297+
// We only need to verify state events.
2298+
let Some(raw_state_key) = &original.state_key else { return Ok(()) };
2299+
2300+
// Unpack event type and state key from the raw state key.
2301+
let (outer_event_type, outer_state_key) =
2302+
raw_state_key.split_once(":").ok_or(MegolmError::StateKeyVerificationFailed)?;
2303+
2304+
// Helper for deserializing.
2305+
#[derive(Deserialize)]
2306+
struct PayloadDeserializationHelper {
2307+
state_key: String,
2308+
#[serde(rename = "type")]
2309+
event_type: String,
2310+
}
2311+
2312+
// Deserialize the decrypted event.
2313+
let PayloadDeserializationHelper {
2314+
state_key: inner_state_key,
2315+
event_type: inner_event_type,
2316+
} = decrypted
2317+
.deserialize_as_unchecked()
2318+
.map_err(|_| MegolmError::StateKeyVerificationFailed)?;
2319+
2320+
// Check event types match, discard if not.
2321+
if outer_event_type != inner_event_type {
2322+
return Err(MegolmError::StateKeyVerificationFailed);
2323+
}
2324+
2325+
// Check state keys match, discard if not.
2326+
if outer_state_key != inner_state_key {
2327+
return Err(MegolmError::StateKeyVerificationFailed);
2328+
}
2329+
Ok(())
22032330
}
22042331

22052332
/// Try to decrypt the events bundled in the `unsigned` object of the given
@@ -2970,6 +3097,8 @@ fn megolm_error_to_utd_info(
29703097
JsonError(_) => UnableToDecryptReason::PayloadDeserializationFailure,
29713098
MismatchedIdentityKeys(_) => UnableToDecryptReason::MismatchedIdentityKeys,
29723099
SenderIdentityNotTrusted(level) => UnableToDecryptReason::SenderIdentityNotTrusted(level),
3100+
#[cfg(feature = "experimental-encrypted-state-events")]
3101+
StateKeyVerificationFailed => UnableToDecryptReason::StateKeyVerificationFailed,
29733102

29743103
// Pass through crypto store errors, which indicate a problem with our
29753104
// application, rather than a UTD.

0 commit comments

Comments
 (0)