From 2a78b925e4cdafcbee25e3b47b2326ff1838d33e Mon Sep 17 00:00:00 2001 From: Benjamin Bouvier Date: Fri, 6 Oct 2023 18:32:31 +0200 Subject: [PATCH] crypto: move all the `Account` methods in a single impl block --- crates/matrix-sdk-crypto/src/olm/account.rs | 688 ++++++++++---------- 1 file changed, 343 insertions(+), 345 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/olm/account.rs b/crates/matrix-sdk-crypto/src/olm/account.rs index 6f5f604ac..558da7cf1 100644 --- a/crates/matrix-sdk-crypto/src/olm/account.rs +++ b/crates/matrix-sdk-crypto/src/olm/account.rs @@ -146,351 +146,6 @@ impl OlmMessageHash { } } -impl Account { - async fn decrypt_olm_helper( - &self, - store: &Store, - sender: &UserId, - sender_key: Curve25519PublicKey, - ciphertext: &OlmMessage, - ) -> OlmResult { - let message_hash = OlmMessageHash::new(sender_key, ciphertext); - - match self.decrypt_olm_message(store, sender, sender_key, ciphertext).await { - Ok((session, result)) => { - Ok(OlmDecryptionInfo { session, message_hash, result, inbound_group_session: None }) - } - Err(OlmError::SessionWedged(user_id, sender_key)) => { - if store.is_message_known(&message_hash).await? { - info!(?sender_key, "An Olm message got replayed, decryption failed"); - Err(OlmError::ReplayedMessage(user_id, sender_key)) - } else { - Err(OlmError::SessionWedged(user_id, sender_key)) - } - } - Err(e) => Err(e), - } - } - - #[cfg(feature = "experimental-algorithms")] - async fn decrypt_olm_v2( - &self, - store: &Store, - sender: &UserId, - content: &OlmV2Curve25519AesSha2Content, - ) -> OlmResult { - self.decrypt_olm_helper(store, sender, content.sender_key, &content.ciphertext).await - } - - #[instrument(skip_all, fields(sender, sender_key = %content.sender_key))] - async fn decrypt_olm_v1( - &self, - store: &Store, - sender: &UserId, - content: &OlmV1Curve25519AesSha2Content, - ) -> OlmResult { - if content.recipient_key != self.static_data.identity_keys.curve25519 { - warn!("Olm event doesn't contain a ciphertext for our key"); - - Err(EventError::MissingCiphertext.into()) - } else { - Box::pin(self.decrypt_olm_helper( - store, - sender, - content.sender_key, - &content.ciphertext, - )) - .await - } - } - - #[instrument(skip_all, fields(algorithm = ?event.content.algorithm()))] - pub(crate) async fn decrypt_to_device_event( - &self, - store: &Store, - event: &EncryptedToDeviceEvent, - ) -> OlmResult { - trace!("Decrypting a to-device event"); - - match &event.content { - ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(c) => { - self.decrypt_olm_v1(store, &event.sender, c).await - } - #[cfg(feature = "experimental-algorithms")] - ToDeviceEncryptedEventContent::OlmV2Curve25519AesSha2(c) => { - self.decrypt_olm_v2(store, &event.sender, c).await - } - ToDeviceEncryptedEventContent::Unknown(_) => { - warn!( - "Error decrypting an to-device event, unsupported \ - encryption algorithm" - ); - - Err(EventError::UnsupportedAlgorithm.into()) - } - } - } - - /// Handles a response to a /keys/upload request. - pub async fn receive_keys_upload_response( - &self, - store: &Store, - response: &upload_keys::v3::Response, - ) -> OlmResult<()> { - if !self.shared() { - debug!("Marking account as shared"); - } - self.mark_as_shared(); - - debug!("Marking one-time keys as published"); - // First mark the current keys as published, as updating the key counts might - // generate some new keys if we're still below the limit. - self.mark_keys_as_published().await; - self.update_key_counts(&response.one_time_key_counts, None).await; - - store.save_account(self.clone()).await?; - - Ok(()) - } - - /// Try to decrypt an Olm message. - /// - /// This try to decrypt an Olm message using all the sessions we share - /// with the given sender. - async fn decrypt_with_existing_sessions( - store: &Store, - sender_key: Curve25519PublicKey, - message: &OlmMessage, - ) -> OlmResult> { - let s = store.get_sessions(&sender_key.to_base64()).await?; - - let Some(sessions) = s else { - // We don't have any existing sessions, return early. - return Ok(None); - }; - - let mut decrypted: Option<(Session, String)> = None; - - // Try to decrypt the message using each Session we share with the - // given curve25519 sender key. - for session in &mut *sessions.lock().await { - if let Ok(p) = session.decrypt(message).await { - decrypted = Some((session.clone(), p)); - break; - } else if let OlmMessage::PreKey(message) = message { - if message.session_id() == session.session_id() { - // The message was intended for this session, but we weren't able to decrypt it. - // - // We're going to return early here since no other session will be able to - // decrypt this message, nor should we try to create a new one since we had - // already previously created a `Session` with such a pre-key message. - // - // Creating this session would have likely failed anyway since the corresponding - // one-time key would've been already used up in the previous session creation - // operation. The one exception where this would not be so is if the fallback - // key was used for creating the session in lieu of an OTK. - return Err(OlmError::SessionWedged( - session.user_id.to_owned(), - session.sender_key(), - )); - } - } else { - // An error here is completely normal, after all we don't know - // which session was used to encrypt a message. We will log a - // warning if no session was able to decrypt the message. - continue; - } - } - - Ok(decrypted) - } - - /// Decrypt an Olm message, creating a new Olm session if possible. - #[instrument(skip(self, message))] - async fn decrypt_olm_message( - &self, - store: &Store, - sender: &UserId, - sender_key: Curve25519PublicKey, - message: &OlmMessage, - ) -> OlmResult<(SessionType, DecryptionResult)> { - // First try to decrypt using an existing session. - let (session, plaintext) = if let Some(d) = - Self::decrypt_with_existing_sessions(store, sender_key, message).await? - { - // Decryption succeeded, de-structure the session/plaintext out of - // the Option. - (SessionType::Existing(d.0), d.1) - } else { - // Decryption failed with every known session, let's try to create a - // new session. - match message { - // A new session can only be created using a pre-key message, - // return with an error if it isn't one. - OlmMessage::Normal(_) => { - let session_ids = if let Some(sessions) = - store.get_sessions(&sender_key.to_base64()).await? - { - sessions.lock().await.iter().map(|s| s.session_id().to_owned()).collect() - } else { - vec![] - }; - - warn!( - ?session_ids, - "Failed to decrypt a non-pre-key message with all available sessions", - ); - - return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); - } - - OlmMessage::PreKey(m) => { - // Create the new session. - let result = match self.create_inbound_session(sender_key, m).await { - Ok(r) => r, - Err(_) => { - return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); - } - }; - - // We need to add the new session to the session cache, otherwise - // we might try to create the same session again. - // TODO: separate the session cache from the storage so we only add - // it to the cache but don't store it. - let changes = Changes { - account: Some(self.clone()), - sessions: vec![result.session.clone()], - ..Default::default() - }; - store.save_changes(changes).await?; - - (SessionType::New(result.session), result.plaintext) - } - } - }; - - { - let session_id = match &session { - SessionType::New(s) => s.session_id(), - SessionType::Existing(s) => s.session_id(), - }; - - Span::current().record("session_id", session_id); - trace!("Successfully decrypted an Olm message"); - } - - match self.parse_decrypted_to_device_event(store, sender, sender_key, plaintext).await { - Ok(result) => Ok((session, result)), - Err(e) => { - // We might created a new session but decryption might still - // have failed, store it for the error case here, this is fine - // since we don't expect this to happen often or at all. - match session { - SessionType::New(s) => { - let changes = Changes { - account: Some(self.clone()), - sessions: vec![s], - ..Default::default() - }; - store.save_changes(changes).await?; - } - SessionType::Existing(s) => { - store.save_sessions(&[s]).await?; - } - } - - warn!( - error = ?e, - "A to-device message was successfully decrypted but \ - parsing and checking the event fields failed" - ); - - Err(e) - } - } - } - - /// Parse the decrypted plaintext as JSON and verify that it wasn't - /// forwarded by a third party. - /// - /// These checks are mandated by the spec[1]: - /// - /// > Other properties are included in order to prevent an attacker from - /// > publishing someone else's Curve25519 keys as their own and - /// > subsequently claiming to have sent messages which they didn't. - /// > sender must correspond to the user who sent the event, recipient to - /// > the local user, and recipient_keys to the local Ed25519 key. - /// - /// # Arguments - /// - /// * `sender` - The `sender` field from the top level of the received - /// event. - /// * `sender_key` - The `sender_key` from the cleartext `content` of the - /// received event (which should also have been used to find or establish - /// the Olm session that was used to decrypt the event -- so it is - /// guaranteed to be correct). - /// * `plaintext` - The decrypted content of the event. - async fn parse_decrypted_to_device_event( - &self, - store: &Store, - sender: &UserId, - sender_key: Curve25519PublicKey, - plaintext: String, - ) -> OlmResult { - let event: Box = serde_json::from_str(&plaintext)?; - let identity_keys = &self.static_data.identity_keys; - - if event.recipient() != self.static_data.user_id { - Err(EventError::MismatchedSender( - event.recipient().to_owned(), - self.static_data.user_id.clone(), - ) - .into()) - } - // Check that the `sender` in the decrypted to-device event matches that at the - // top level of the encrypted event. - else if event.sender() != sender { - Err(EventError::MismatchedSender(event.sender().to_owned(), sender.to_owned()).into()) - } else if identity_keys.ed25519 != event.recipient_keys().ed25519 { - Err(EventError::MismatchedKeys( - identity_keys.ed25519.into(), - event.recipient_keys().ed25519.into(), - ) - .into()) - } else { - // If this event is an `m.room_key` event, defer the check for the - // Ed25519 key of the sender until we decrypt room events. This - // ensures that we receive the room key even if we don't have access - // to the device. - if !matches!(*event, AnyDecryptedOlmEvent::RoomKey(_)) { - let Some(device) = - store.get_device_from_curve_key(event.sender(), sender_key).await? - else { - return Err(EventError::MissingSigningKey.into()); - }; - - let Some(key) = device.ed25519_key() else { - return Err(EventError::MissingSigningKey.into()); - }; - - if key != event.keys().ed25519 { - return Err(EventError::MismatchedKeys( - key.into(), - event.keys().ed25519.into(), - ) - .into()); - } - } - - Ok(DecryptionResult { - event, - raw_event: Raw::from_json(RawJsonValue::from_string(plaintext)?), - sender_key, - }) - } - } -} - /// Account data that's static for the lifetime of a Client. /// /// This data never changes once it's set, so it can be freely passed and cloned @@ -1369,6 +1024,349 @@ impl Account { (our_session, other_session.session) } + + async fn decrypt_olm_helper( + &self, + store: &Store, + sender: &UserId, + sender_key: Curve25519PublicKey, + ciphertext: &OlmMessage, + ) -> OlmResult { + let message_hash = OlmMessageHash::new(sender_key, ciphertext); + + match self.decrypt_olm_message(store, sender, sender_key, ciphertext).await { + Ok((session, result)) => { + Ok(OlmDecryptionInfo { session, message_hash, result, inbound_group_session: None }) + } + Err(OlmError::SessionWedged(user_id, sender_key)) => { + if store.is_message_known(&message_hash).await? { + info!(?sender_key, "An Olm message got replayed, decryption failed"); + Err(OlmError::ReplayedMessage(user_id, sender_key)) + } else { + Err(OlmError::SessionWedged(user_id, sender_key)) + } + } + Err(e) => Err(e), + } + } + + #[cfg(feature = "experimental-algorithms")] + async fn decrypt_olm_v2( + &self, + store: &Store, + sender: &UserId, + content: &OlmV2Curve25519AesSha2Content, + ) -> OlmResult { + self.decrypt_olm_helper(store, sender, content.sender_key, &content.ciphertext).await + } + + #[instrument(skip_all, fields(sender, sender_key = %content.sender_key))] + async fn decrypt_olm_v1( + &self, + store: &Store, + sender: &UserId, + content: &OlmV1Curve25519AesSha2Content, + ) -> OlmResult { + if content.recipient_key != self.static_data.identity_keys.curve25519 { + warn!("Olm event doesn't contain a ciphertext for our key"); + + Err(EventError::MissingCiphertext.into()) + } else { + Box::pin(self.decrypt_olm_helper( + store, + sender, + content.sender_key, + &content.ciphertext, + )) + .await + } + } + + #[instrument(skip_all, fields(algorithm = ?event.content.algorithm()))] + pub(crate) async fn decrypt_to_device_event( + &self, + store: &Store, + event: &EncryptedToDeviceEvent, + ) -> OlmResult { + trace!("Decrypting a to-device event"); + + match &event.content { + ToDeviceEncryptedEventContent::OlmV1Curve25519AesSha2(c) => { + self.decrypt_olm_v1(store, &event.sender, c).await + } + #[cfg(feature = "experimental-algorithms")] + ToDeviceEncryptedEventContent::OlmV2Curve25519AesSha2(c) => { + self.decrypt_olm_v2(store, &event.sender, c).await + } + ToDeviceEncryptedEventContent::Unknown(_) => { + warn!( + "Error decrypting an to-device event, unsupported \ + encryption algorithm" + ); + + Err(EventError::UnsupportedAlgorithm.into()) + } + } + } + + /// Handles a response to a /keys/upload request. + pub async fn receive_keys_upload_response( + &self, + store: &Store, + response: &upload_keys::v3::Response, + ) -> OlmResult<()> { + if !self.shared() { + debug!("Marking account as shared"); + } + self.mark_as_shared(); + + debug!("Marking one-time keys as published"); + // First mark the current keys as published, as updating the key counts might + // generate some new keys if we're still below the limit. + self.mark_keys_as_published().await; + self.update_key_counts(&response.one_time_key_counts, None).await; + + store.save_account(self.clone()).await?; + + Ok(()) + } + + /// Try to decrypt an Olm message. + /// + /// This try to decrypt an Olm message using all the sessions we share + /// with the given sender. + async fn decrypt_with_existing_sessions( + store: &Store, + sender_key: Curve25519PublicKey, + message: &OlmMessage, + ) -> OlmResult> { + let s = store.get_sessions(&sender_key.to_base64()).await?; + + let Some(sessions) = s else { + // We don't have any existing sessions, return early. + return Ok(None); + }; + + let mut decrypted: Option<(Session, String)> = None; + + // Try to decrypt the message using each Session we share with the + // given curve25519 sender key. + for session in &mut *sessions.lock().await { + if let Ok(p) = session.decrypt(message).await { + decrypted = Some((session.clone(), p)); + break; + } else if let OlmMessage::PreKey(message) = message { + if message.session_id() == session.session_id() { + // The message was intended for this session, but we weren't able to decrypt it. + // + // We're going to return early here since no other session will be able to + // decrypt this message, nor should we try to create a new one since we had + // already previously created a `Session` with such a pre-key message. + // + // Creating this session would have likely failed anyway since the corresponding + // one-time key would've been already used up in the previous session creation + // operation. The one exception where this would not be so is if the fallback + // key was used for creating the session in lieu of an OTK. + return Err(OlmError::SessionWedged( + session.user_id.to_owned(), + session.sender_key(), + )); + } + } else { + // An error here is completely normal, after all we don't know + // which session was used to encrypt a message. We will log a + // warning if no session was able to decrypt the message. + continue; + } + } + + Ok(decrypted) + } + + /// Decrypt an Olm message, creating a new Olm session if possible. + #[instrument(skip(self, message))] + async fn decrypt_olm_message( + &self, + store: &Store, + sender: &UserId, + sender_key: Curve25519PublicKey, + message: &OlmMessage, + ) -> OlmResult<(SessionType, DecryptionResult)> { + // First try to decrypt using an existing session. + let (session, plaintext) = if let Some(d) = + Self::decrypt_with_existing_sessions(store, sender_key, message).await? + { + // Decryption succeeded, de-structure the session/plaintext out of + // the Option. + (SessionType::Existing(d.0), d.1) + } else { + // Decryption failed with every known session, let's try to create a + // new session. + match message { + // A new session can only be created using a pre-key message, + // return with an error if it isn't one. + OlmMessage::Normal(_) => { + let session_ids = if let Some(sessions) = + store.get_sessions(&sender_key.to_base64()).await? + { + sessions.lock().await.iter().map(|s| s.session_id().to_owned()).collect() + } else { + vec![] + }; + + warn!( + ?session_ids, + "Failed to decrypt a non-pre-key message with all available sessions", + ); + + return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); + } + + OlmMessage::PreKey(m) => { + // Create the new session. + let result = match self.create_inbound_session(sender_key, m).await { + Ok(r) => r, + Err(_) => { + return Err(OlmError::SessionWedged(sender.to_owned(), sender_key)); + } + }; + + // We need to add the new session to the session cache, otherwise + // we might try to create the same session again. + // TODO: separate the session cache from the storage so we only add + // it to the cache but don't store it. + let changes = Changes { + account: Some(self.clone()), + sessions: vec![result.session.clone()], + ..Default::default() + }; + store.save_changes(changes).await?; + + (SessionType::New(result.session), result.plaintext) + } + } + }; + + { + let session_id = match &session { + SessionType::New(s) => s.session_id(), + SessionType::Existing(s) => s.session_id(), + }; + + Span::current().record("session_id", session_id); + trace!("Successfully decrypted an Olm message"); + } + + match self.parse_decrypted_to_device_event(store, sender, sender_key, plaintext).await { + Ok(result) => Ok((session, result)), + Err(e) => { + // We might created a new session but decryption might still + // have failed, store it for the error case here, this is fine + // since we don't expect this to happen often or at all. + match session { + SessionType::New(s) => { + let changes = Changes { + account: Some(self.clone()), + sessions: vec![s], + ..Default::default() + }; + store.save_changes(changes).await?; + } + SessionType::Existing(s) => { + store.save_sessions(&[s]).await?; + } + } + + warn!( + error = ?e, + "A to-device message was successfully decrypted but \ + parsing and checking the event fields failed" + ); + + Err(e) + } + } + } + + /// Parse the decrypted plaintext as JSON and verify that it wasn't + /// forwarded by a third party. + /// + /// These checks are mandated by the spec[1]: + /// + /// > Other properties are included in order to prevent an attacker from + /// > publishing someone else's Curve25519 keys as their own and + /// > subsequently claiming to have sent messages which they didn't. + /// > sender must correspond to the user who sent the event, recipient to + /// > the local user, and recipient_keys to the local Ed25519 key. + /// + /// # Arguments + /// + /// * `sender` - The `sender` field from the top level of the received + /// event. + /// * `sender_key` - The `sender_key` from the cleartext `content` of the + /// received event (which should also have been used to find or establish + /// the Olm session that was used to decrypt the event -- so it is + /// guaranteed to be correct). + /// * `plaintext` - The decrypted content of the event. + async fn parse_decrypted_to_device_event( + &self, + store: &Store, + sender: &UserId, + sender_key: Curve25519PublicKey, + plaintext: String, + ) -> OlmResult { + let event: Box = serde_json::from_str(&plaintext)?; + let identity_keys = &self.static_data.identity_keys; + + if event.recipient() != self.static_data.user_id { + Err(EventError::MismatchedSender( + event.recipient().to_owned(), + self.static_data.user_id.clone(), + ) + .into()) + } + // Check that the `sender` in the decrypted to-device event matches that at the + // top level of the encrypted event. + else if event.sender() != sender { + Err(EventError::MismatchedSender(event.sender().to_owned(), sender.to_owned()).into()) + } else if identity_keys.ed25519 != event.recipient_keys().ed25519 { + Err(EventError::MismatchedKeys( + identity_keys.ed25519.into(), + event.recipient_keys().ed25519.into(), + ) + .into()) + } else { + // If this event is an `m.room_key` event, defer the check for the + // Ed25519 key of the sender until we decrypt room events. This + // ensures that we receive the room key even if we don't have access + // to the device. + if !matches!(*event, AnyDecryptedOlmEvent::RoomKey(_)) { + let Some(device) = + store.get_device_from_curve_key(event.sender(), sender_key).await? + else { + return Err(EventError::MissingSigningKey.into()); + }; + + let Some(key) = device.ed25519_key() else { + return Err(EventError::MissingSigningKey.into()); + }; + + if key != event.keys().ed25519 { + return Err(EventError::MismatchedKeys( + key.into(), + event.keys().ed25519.into(), + ) + .into()); + } + } + + Ok(DecryptionResult { + event, + raw_event: Raw::from_json(RawJsonValue::from_string(plaintext)?), + sender_key, + }) + } + } } impl PartialEq for Account {