matrix_sdk_crypto/olm/
mod.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The crypto specific Olm objects.
16//!
17//! Note: You'll only be interested in these if you are implementing a custom
18//! `CryptoStore`.
19
20mod account;
21mod group_sessions;
22mod session;
23mod signing;
24pub(crate) mod utility;
25
26pub use account::{Account, OlmMessageHash, PickledAccount, StaticAccountData};
27pub(crate) use account::{OlmDecryptionInfo, SessionType};
28pub(crate) use group_sessions::{
29    sender_data_finder::{self, SenderDataFinder},
30    ShareState,
31};
32pub use group_sessions::{
33    BackedUpRoomKey, EncryptionSettings, ExportedRoomKey, InboundGroupSession, KnownSenderData,
34    OutboundGroupSession, PickledInboundGroupSession, PickledOutboundGroupSession, SenderData,
35    SenderDataType, SessionCreationError, SessionExportError, SessionKey, ShareInfo,
36};
37pub use session::{PickledSession, Session};
38pub use signing::{CrossSigningStatus, PickledCrossSigningIdentity, PrivateCrossSigningIdentity};
39pub(crate) use utility::{SignedJsonObject, VerifyJson};
40pub use vodozemac::{olm::IdentityKeys, Curve25519PublicKey};
41
42#[cfg(test)]
43pub(crate) mod tests {
44    use assert_matches::assert_matches;
45    use matrix_sdk_test::{async_test, message_like_event_content};
46    use ruma::{
47        device_id, event_id,
48        events::{
49            relation::Replacement,
50            room::message::{Relation, RoomMessageEventContent},
51            AnyMessageLikeEvent, AnyTimelineEvent, MessageLikeEvent,
52        },
53        room_id,
54        serde::Raw,
55        user_id, DeviceId, UserId,
56    };
57    use serde_json::{from_value, json, Value};
58    use vodozemac::olm::{OlmMessage, SessionConfig};
59
60    use crate::{
61        olm::{Account, ExportedRoomKey, InboundGroupSession, Session},
62        types::events::{
63            forwarded_room_key::ForwardedRoomKeyContent, room::encrypted::EncryptedEvent,
64        },
65        utilities::json_convert,
66    };
67
68    fn alice_id() -> &'static UserId {
69        user_id!("@alice:example.org")
70    }
71
72    fn alice_device_id() -> &'static DeviceId {
73        device_id!("ALICEDEVICE")
74    }
75
76    fn bob_id() -> &'static UserId {
77        user_id!("@bob:example.org")
78    }
79
80    fn bob_device_id() -> &'static DeviceId {
81        device_id!("BOBDEVICE")
82    }
83
84    pub(crate) fn get_account_and_session_test_helper() -> (Account, Session) {
85        let alice = Account::with_device_id(alice_id(), alice_device_id());
86        let mut bob = Account::with_device_id(bob_id(), bob_device_id());
87
88        bob.generate_one_time_keys(1);
89        let one_time_key = *bob.one_time_keys().values().next().unwrap();
90        let sender_key = bob.identity_keys().curve25519;
91        let session = alice.create_outbound_session_helper(
92            SessionConfig::default(),
93            sender_key,
94            one_time_key,
95            false,
96            alice.device_keys(),
97        );
98
99        (alice, session)
100    }
101
102    #[test]
103    fn test_account_creation() {
104        let mut account = Account::with_device_id(alice_id(), alice_device_id());
105
106        assert!(!account.shared());
107
108        account.mark_as_shared();
109        assert!(account.shared());
110    }
111
112    #[test]
113    fn test_one_time_keys_creation() {
114        let mut account = Account::with_device_id(alice_id(), alice_device_id());
115        let one_time_keys = account.one_time_keys();
116
117        assert!(!one_time_keys.is_empty());
118        assert_ne!(account.max_one_time_keys(), 0);
119
120        account.generate_one_time_keys(10);
121        let one_time_keys = account.one_time_keys();
122
123        assert_ne!(one_time_keys.values().len(), 0);
124        assert_ne!(one_time_keys.keys().len(), 0);
125        assert_ne!(one_time_keys.iter().len(), 0);
126
127        account.mark_keys_as_published();
128        let one_time_keys = account.one_time_keys();
129        assert!(one_time_keys.is_empty());
130    }
131
132    #[async_test]
133    async fn test_session_creation() {
134        let mut alice = Account::with_device_id(alice_id(), alice_device_id());
135        let bob = Account::with_device_id(bob_id(), bob_device_id());
136        let alice_keys = alice.identity_keys();
137        alice.generate_one_time_keys(1);
138        let one_time_keys = alice.one_time_keys();
139        alice.mark_keys_as_published();
140
141        let one_time_key = *one_time_keys.values().next().unwrap();
142
143        let mut bob_session = bob.create_outbound_session_helper(
144            SessionConfig::default(),
145            alice_keys.curve25519,
146            one_time_key,
147            false,
148            bob.device_keys(),
149        );
150
151        let plaintext = "Hello world";
152
153        let message = bob_session.encrypt_helper(plaintext).await;
154
155        let prekey_message = match message {
156            OlmMessage::PreKey(m) => m,
157            OlmMessage::Normal(_) => panic!("Incorrect message type"),
158        };
159
160        let bob_keys = bob.identity_keys();
161        let result = alice
162            .create_inbound_session(bob_keys.curve25519, alice.device_keys(), &prekey_message)
163            .unwrap();
164
165        assert_eq!(bob_session.session_id(), result.session.session_id());
166
167        assert_eq!(plaintext, result.plaintext);
168    }
169
170    #[async_test]
171    async fn test_group_session_creation() {
172        let alice = Account::with_device_id(alice_id(), alice_device_id());
173        let room_id = room_id!("!test:localhost");
174
175        let (outbound, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
176
177        assert_eq!(0, outbound.message_index().await);
178        assert!(!outbound.shared());
179        outbound.mark_as_shared();
180        assert!(outbound.shared());
181
182        assert_eq!(0, inbound.first_known_index());
183        assert_eq!(outbound.session_id(), inbound.session_id());
184
185        let plaintext = "This is a secret to everybody".to_owned();
186        let ciphertext = outbound.encrypt_helper(plaintext.clone()).await;
187
188        assert_eq!(
189            plaintext.as_bytes(),
190            inbound.decrypt_helper(&ciphertext).await.unwrap().plaintext
191        );
192    }
193
194    #[async_test]
195    async fn test_edit_decryption() {
196        let alice = Account::with_device_id(alice_id(), alice_device_id());
197        let room_id = room_id!("!test:localhost");
198        let event_id = event_id!("$1234adfad:asdf");
199
200        let (outbound, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
201
202        assert_eq!(0, outbound.message_index().await);
203        assert!(!outbound.shared());
204        outbound.mark_as_shared();
205        assert!(outbound.shared());
206
207        let mut content = RoomMessageEventContent::text_plain("Hello");
208        content.relates_to = Some(Relation::Replacement(Replacement::new(
209            event_id.to_owned(),
210            RoomMessageEventContent::text_plain("Hello edit").into(),
211        )));
212
213        assert_eq!(0, inbound.first_known_index());
214        assert_eq!(outbound.session_id(), inbound.session_id());
215
216        let encrypted_content =
217            outbound.encrypt("m.room.message", &Raw::new(&content).unwrap().cast()).await;
218
219        let event = json!({
220            "sender": alice.user_id(),
221            "event_id": event_id,
222            "origin_server_ts": 0u64,
223            "room_id": room_id,
224            "type": "m.room.encrypted",
225            "content": encrypted_content,
226        });
227
228        let event = json_convert(&event).unwrap();
229        let decrypted = inbound.decrypt(&event).await.unwrap().0;
230
231        if let AnyTimelineEvent::MessageLike(AnyMessageLikeEvent::RoomMessage(
232            MessageLikeEvent::Original(e),
233        )) = from_value(decrypted.into()).unwrap()
234        {
235            assert_matches!(e.content.relates_to, Some(Relation::Replacement(_)));
236        } else {
237            panic!("Invalid event type")
238        }
239    }
240
241    #[async_test]
242    async fn test_relates_to_decryption() {
243        let alice = Account::with_device_id(alice_id(), alice_device_id());
244        let room_id = room_id!("!test:localhost");
245        let event_id = event_id!("$1234adfad:asdf");
246
247        let relation_json = json!({
248            "rel_type": "m.reference",
249            "event_id": "$WUreEJERkFzO8i2dk6CmTex01cP1dZ4GWKhKCwkWHrQ"
250        });
251
252        let (outbound, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
253
254        // We first test that we're copying the relation from the content that
255        // will be encrypted to the content that will stay plaintext.
256        let content = message_like_event_content!({
257            "m.relates_to": relation_json,
258        });
259        let encrypted = outbound.encrypt("m.dummy", &content).await;
260
261        let event = json!({
262            "sender": alice.user_id(),
263            "event_id": event_id,
264            "origin_server_ts": 0u64,
265            "room_id": room_id,
266            "type": "m.room.encrypted",
267            "content": encrypted,
268        });
269        let event: EncryptedEvent = json_convert(&event).unwrap();
270
271        assert_eq!(
272            event.content.relates_to.as_ref(),
273            Some(&relation_json),
274            "The encrypted event should contain an unencrypted relation"
275        );
276
277        let (decrypted, _) = inbound.decrypt(&event).await.unwrap();
278
279        let decrypted: Value = json_convert(&decrypted).unwrap();
280        let relation = decrypted.get("content").and_then(|c| c.get("m.relates_to"));
281        assert_eq!(relation, Some(&relation_json), "The decrypted event should contain a relation");
282
283        let content = message_like_event_content!({});
284        let encrypted = outbound.encrypt("m.dummy", &content).await;
285        let mut encrypted: Value = json_convert(&encrypted).unwrap();
286        encrypted.as_object_mut().unwrap().insert("m.relates_to".to_owned(), relation_json.clone());
287
288        // Let's now test if we copy the correct relation if there is no
289        // relation in the encrypted content.
290        let event = json!({
291            "sender": alice.user_id(),
292            "event_id": event_id,
293            "origin_server_ts": 0u64,
294            "room_id": room_id,
295            "type": "m.room.encrypted",
296            "content": encrypted,
297        });
298        let event: EncryptedEvent = json_convert(&event).unwrap();
299
300        assert_eq!(
301            event.content.relates_to,
302            Some(relation_json),
303            "The encrypted event should contain an unencrypted relation"
304        );
305
306        let (decrypted, _) = inbound.decrypt(&event).await.unwrap();
307
308        let decrypted: Value = json_convert(&decrypted).unwrap();
309        let relation = decrypted.get("content").and_then(|c| c.get("m.relates_to"));
310        assert!(relation.is_some(), "The decrypted event should contain a relation");
311    }
312
313    #[async_test]
314    async fn test_group_session_export() {
315        let alice = Account::with_device_id(alice_id(), alice_device_id());
316        let room_id = room_id!("!test:localhost");
317
318        let (_, inbound) = alice.create_group_session_pair_with_defaults(room_id).await;
319
320        let export = inbound.export().await;
321        let export: ForwardedRoomKeyContent = export.try_into().unwrap();
322        let export = ExportedRoomKey::try_from(export).unwrap();
323
324        let imported = InboundGroupSession::from_export(&export)
325            .expect("We can always import an inbound group session from a fresh export");
326
327        assert_eq!(inbound.session_id(), imported.session_id());
328    }
329}