Skip to main content

matrix_sdk_crypto/verification/sas/
helpers.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
15use std::collections::BTreeMap;
16
17use ruma::{
18    DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, UserId,
19    events::{
20        AnyMessageLikeEventContent, AnyToDeviceEventContent,
21        key::verification::{
22            cancel::CancelCode,
23            mac::{KeyVerificationMacEventContent, ToDeviceKeyVerificationMacEventContent},
24        },
25        relation::Reference,
26    },
27    serde::Base64,
28};
29use sha2::{Digest, Sha256};
30use tracing::{trace, warn};
31use vodozemac::{Curve25519PublicKey, sas::EstablishedSas};
32
33use super::{FlowId, OutgoingContent, sas_state::SupportedMacMethod};
34use crate::{
35    Emoji, OwnUserIdentityData,
36    identities::{DeviceData, UserIdentityData},
37    olm::StaticAccountData,
38    verification::event_enums::{MacContent, StartContent},
39};
40
41#[derive(Clone, Debug)]
42pub struct SasIds {
43    pub account: StaticAccountData,
44    pub own_identity: Option<OwnUserIdentityData>,
45    pub other_device: DeviceData,
46    pub other_identity: Option<UserIdentityData>,
47}
48
49/// Calculate the commitment for a accept event from the public key and the
50/// start event.
51///
52/// # Arguments
53///
54/// * `public_key` - Our own ephemeral public key that is used for the
55///   interactive verification.
56///
57/// * `content` - The `m.key.verification.start` event content that started the
58///   interactive verification process.
59pub fn calculate_commitment(public_key: Curve25519PublicKey, content: &StartContent<'_>) -> Base64 {
60    let content = content.canonical_json();
61    let content_string = content.to_string();
62
63    Base64::new(
64        Sha256::new()
65            .chain_update(public_key.to_base64())
66            .chain_update(content_string)
67            .finalize()
68            .as_slice()
69            .to_owned(),
70    )
71}
72
73/// Get a tuple of an emoji and a description of the emoji using a number.
74///
75/// This is taken directly from the [spec]
76///
77/// # Panics
78///
79/// The spec defines 64 unique emojis, this function panics if the index is
80/// bigger than 63.
81///
82/// [spec]: https://spec.matrix.org/latest/client-server-api/#sas-method-emoji
83fn emoji_from_index(index: u8) -> Emoji {
84    /*
85    This list was generated from the data in the spec [1] with the following command:
86
87    jq  -r '.[] |  "        " + (.number|tostring) + " => Emoji { symbol: \"" + .emoji + "\", description: \"" + .description + "\" },"' sas-emoji.json
88
89    [1]: https://github.com/matrix-org/matrix-spec/blob/main/data-definitions/sas-emoji.json
90    */
91    match index {
92        0 => Emoji { symbol: "🐶", description: "Dog" },
93        1 => Emoji { symbol: "🐱", description: "Cat" },
94        2 => Emoji { symbol: "🦁", description: "Lion" },
95        3 => Emoji { symbol: "🐎", description: "Horse" },
96        4 => Emoji { symbol: "🦄", description: "Unicorn" },
97        5 => Emoji { symbol: "🐷", description: "Pig" },
98        6 => Emoji { symbol: "🐘", description: "Elephant" },
99        7 => Emoji { symbol: "🐰", description: "Rabbit" },
100        8 => Emoji { symbol: "🐼", description: "Panda" },
101        9 => Emoji { symbol: "🐓", description: "Rooster" },
102        10 => Emoji { symbol: "🐧", description: "Penguin" },
103        11 => Emoji { symbol: "🐢", description: "Turtle" },
104        12 => Emoji { symbol: "🐟", description: "Fish" },
105        13 => Emoji { symbol: "🐙", description: "Octopus" },
106        14 => Emoji { symbol: "🦋", description: "Butterfly" },
107        15 => Emoji { symbol: "🌷", description: "Flower" },
108        16 => Emoji { symbol: "🌳", description: "Tree" },
109        17 => Emoji { symbol: "🌵", description: "Cactus" },
110        18 => Emoji { symbol: "🍄", description: "Mushroom" },
111        19 => Emoji { symbol: "🌏", description: "Globe" },
112        20 => Emoji { symbol: "🌙", description: "Moon" },
113        21 => Emoji { symbol: "☁️", description: "Cloud" },
114        22 => Emoji { symbol: "🔥", description: "Fire" },
115        23 => Emoji { symbol: "🍌", description: "Banana" },
116        24 => Emoji { symbol: "🍎", description: "Apple" },
117        25 => Emoji { symbol: "🍓", description: "Strawberry" },
118        26 => Emoji { symbol: "🌽", description: "Corn" },
119        27 => Emoji { symbol: "🍕", description: "Pizza" },
120        28 => Emoji { symbol: "🎂", description: "Cake" },
121        29 => Emoji { symbol: "❤️", description: "Heart" },
122        30 => Emoji { symbol: "😀", description: "Smiley" },
123        31 => Emoji { symbol: "🤖", description: "Robot" },
124        32 => Emoji { symbol: "🎩", description: "Hat" },
125        33 => Emoji { symbol: "👓", description: "Glasses" },
126        34 => Emoji { symbol: "🔧", description: "Spanner" },
127        35 => Emoji { symbol: "🎅", description: "Santa" },
128        36 => Emoji { symbol: "👍", description: "Thumbs Up" },
129        37 => Emoji { symbol: "☂️", description: "Umbrella" },
130        38 => Emoji { symbol: "⌛", description: "Hourglass" },
131        39 => Emoji { symbol: "⏰", description: "Clock" },
132        40 => Emoji { symbol: "🎁", description: "Gift" },
133        41 => Emoji { symbol: "💡", description: "Light Bulb" },
134        42 => Emoji { symbol: "📕", description: "Book" },
135        43 => Emoji { symbol: "✏️", description: "Pencil" },
136        44 => Emoji { symbol: "📎", description: "Paperclip" },
137        45 => Emoji { symbol: "✂️", description: "Scissors" },
138        46 => Emoji { symbol: "🔒", description: "Lock" },
139        47 => Emoji { symbol: "🔑", description: "Key" },
140        48 => Emoji { symbol: "🔨", description: "Hammer" },
141        49 => Emoji { symbol: "☎️", description: "Telephone" },
142        50 => Emoji { symbol: "🏁", description: "Flag" },
143        51 => Emoji { symbol: "🚂", description: "Train" },
144        52 => Emoji { symbol: "🚲", description: "Bicycle" },
145        53 => Emoji { symbol: "✈️", description: "Aeroplane" },
146        54 => Emoji { symbol: "🚀", description: "Rocket" },
147        55 => Emoji { symbol: "🏆", description: "Trophy" },
148        56 => Emoji { symbol: "⚽", description: "Ball" },
149        57 => Emoji { symbol: "🎸", description: "Guitar" },
150        58 => Emoji { symbol: "🎺", description: "Trumpet" },
151        59 => Emoji { symbol: "🔔", description: "Bell" },
152        60 => Emoji { symbol: "⚓", description: "Anchor" },
153        61 => Emoji { symbol: "🎧", description: "Headphones" },
154        62 => Emoji { symbol: "📁", description: "Folder" },
155        63 => Emoji { symbol: "📌", description: "Pin" },
156        _ => panic!("Trying to fetch an emoji outside the allowed range"),
157    }
158}
159
160/// Get the extra info that will be used when we check the MAC of a
161/// m.key.verification.key event.
162///
163/// # Arguments
164///
165/// * `ids` - The ids that are used for this SAS authentication flow.
166///
167/// * `flow_id` - The unique id that identifies this SAS verification process.
168fn extra_mac_info_receive(ids: &SasIds, flow_id: &str) -> String {
169    format!(
170        "MATRIX_KEY_VERIFICATION_MAC{first_user}{first_device}\
171        {second_user}{second_device}{transaction_id}",
172        first_user = ids.other_device.user_id(),
173        first_device = ids.other_device.device_id(),
174        second_user = ids.account.user_id,
175        second_device = ids.account.device_id,
176        transaction_id = flow_id,
177    )
178}
179
180/// Get the content for a m.key.verification.mac event.
181///
182/// Returns a tuple that contains the list of verified devices and the list of
183/// verified master keys.
184///
185/// # Arguments
186///
187/// * `sas` - The Olm SAS object that can be used to MACs
188///
189/// * `ids` - The ids that are used for this SAS authentication flow.
190///
191/// * `flow_id` - The unique id that identifies this SAS verification process.
192///
193/// * `event` - The m.key.verification.mac event that was sent to us by the
194///   other side.
195pub fn receive_mac_event(
196    sas: &EstablishedSas,
197    ids: &SasIds,
198    flow_id: &str,
199    sender: &UserId,
200    mac_method: SupportedMacMethod,
201    content: &MacContent<'_>,
202) -> Result<(Vec<DeviceData>, Vec<UserIdentityData>), CancelCode> {
203    let mut verified_devices = Vec::new();
204    let mut verified_identities = Vec::new();
205
206    let info = extra_mac_info_receive(ids, flow_id);
207
208    trace!(
209        ?sender,
210        device_id = ?ids.other_device.device_id(),
211        "Received a key.verification.mac event"
212    );
213
214    let mut keys = content.mac().keys().map(|k| k.as_str()).collect::<Vec<_>>();
215    keys.sort_unstable();
216    mac_method.verify_mac(sas, &keys.join(","), &format!("{info}KEY_IDS"), content.keys())?;
217
218    for (key_id, key_mac) in content.mac() {
219        trace!(
220            ?sender,
221            device_id = ?ids.other_device.device_id(),
222            key_id,
223            "Checking a SAS MAC",
224        );
225
226        let key_id: OwnedDeviceKeyId = match key_id.as_str().try_into() {
227            Ok(id) => id,
228            Err(_) => continue,
229        };
230
231        if let Some(key) = ids.other_device.keys().get(&key_id) {
232            mac_method.verify_mac(sas, &key.to_base64(), &format!("{info}{key_id}"), key_mac)?;
233            trace!(?sender, ?key_id, "Successfully verified a device key");
234            verified_devices.push(ids.other_device.clone());
235        } else if let Some(identity) = &ids.other_identity {
236            if let Some(key) = identity.master_key().get_key(&key_id) {
237                // TODO: we should check that the master key signs the device,
238                // this way we know the master key also trusts the device
239                mac_method.verify_mac(
240                    sas,
241                    &key.to_base64(),
242                    &format!("{info}{key_id}"),
243                    key_mac,
244                )?;
245                trace!(?sender, ?key_id, "Successfully verified a master key");
246                verified_identities.push(identity.clone())
247            }
248        } else {
249            warn!(
250                "Key ID {key_id} in MAC event from {sender} {} doesn't belong to any device \
251                or user identity",
252                ids.other_device.device_id()
253            );
254        }
255    }
256
257    Ok((verified_devices, verified_identities))
258}
259
260/// Get the extra info that will be used when we generate a MAC and need to send
261/// it out
262///
263/// # Arguments
264///
265/// * `ids` - The ids that are used for this SAS authentication flow.
266///
267/// * `flow_id` - The unique id that identifies this SAS verification process.
268fn extra_mac_info_send(ids: &SasIds, flow_id: &str) -> String {
269    format!(
270        "MATRIX_KEY_VERIFICATION_MAC{first_user}{first_device}\
271        {second_user}{second_device}{transaction_id}",
272        first_user = ids.account.user_id,
273        first_device = ids.account.device_id,
274        second_user = ids.other_device.user_id(),
275        second_device = ids.other_device.device_id(),
276        transaction_id = flow_id,
277    )
278}
279
280/// Get the content for a m.key.verification.mac event.
281///
282/// # Arguments
283///
284/// * `sas` - The Olm SAS object that can be used to generate the MAC
285///
286/// * `ids` - The ids that are used for this SAS authentication flow.
287///
288/// * `flow_id` - The unique id that identifies this SAS verification process.
289pub fn get_mac_content(
290    sas: &EstablishedSas,
291    ids: &SasIds,
292    flow_id: &FlowId,
293    mac_method: SupportedMacMethod,
294) -> OutgoingContent {
295    let mut mac: BTreeMap<String, Base64> = BTreeMap::new();
296
297    let key_id = DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, &ids.account.device_id);
298    let key = ids.account.identity_keys.ed25519.to_base64();
299    let info = extra_mac_info_send(ids, flow_id.as_str());
300
301    mac.insert(key_id.to_string(), mac_method.calculate_mac(sas, &key, &format!("{info}{key_id}")));
302
303    if let Some(own_identity) = &ids.own_identity
304        && own_identity.is_verified()
305        && let Some(key) = own_identity.master_key().get_first_key()
306    {
307        let key_id = format!("{}:{}", DeviceKeyAlgorithm::Ed25519, key.to_base64());
308
309        let calculated_mac =
310            mac_method.calculate_mac(sas, &key.to_base64(), &format!("{info}{key_id}"));
311
312        mac.insert(key_id, calculated_mac);
313    }
314
315    let mut keys: Vec<_> = mac.keys().map(|s| s.as_str()).collect();
316    keys.sort_unstable();
317
318    let keys = mac_method.calculate_mac(sas, &keys.join(","), &format!("{info}KEY_IDS"));
319
320    match flow_id {
321        FlowId::ToDevice(s) => AnyToDeviceEventContent::KeyVerificationMac(
322            ToDeviceKeyVerificationMacEventContent::new(s.clone(), mac, keys),
323        )
324        .into(),
325        FlowId::InRoom(r, e) => {
326            (
327                r.clone(),
328                AnyMessageLikeEventContent::KeyVerificationMac(
329                    KeyVerificationMacEventContent::new(mac, keys, Reference::new(e.clone())),
330                ),
331            )
332                .into()
333        }
334    }
335}
336
337/// Get the extra info that will be used when we generate bytes for the short
338/// auth string.
339///
340/// # Arguments
341///
342/// * `ids` - The ids that are used for this SAS authentication flow.
343///
344/// * `flow_id` - The unique id that identifies this SAS verification process.
345///
346/// * `we_started` - Flag signaling if the SAS process was started on our side.
347fn extra_info_sas(
348    ids: &SasIds,
349    own_pubkey: Curve25519PublicKey,
350    their_pubkey: Curve25519PublicKey,
351    flow_id: &str,
352    we_started: bool,
353) -> String {
354    let our_info =
355        format!("{}|{}|{}", ids.account.user_id, ids.account.device_id, own_pubkey.to_base64());
356    let their_info = format!(
357        "{}|{}|{}",
358        ids.other_device.user_id(),
359        ids.other_device.device_id(),
360        their_pubkey.to_base64()
361    );
362
363    let (first_info, second_info) =
364        if we_started { (our_info, their_info) } else { (their_info, our_info) };
365
366    let info = format!("MATRIX_KEY_VERIFICATION_SAS|{first_info}|{second_info}|{flow_id}");
367
368    trace!("Generated a SAS extra info: {}", info);
369
370    info
371}
372
373/// Get the emoji version of the short authentication string.
374///
375/// Returns seven tuples where the first element is the emoji and the
376/// second element the English description of the emoji.
377///
378/// # Arguments
379///
380/// * `sas` - The Olm SAS object that can be used to generate bytes using the
381///   shared secret.
382///
383/// * `ids` - The ids that are used for this SAS authentication flow.
384///
385/// * `flow_id` - The unique id that identifies this SAS verification process.
386///
387/// * `we_started` - Flag signaling if the SAS process was started on our side.
388///
389/// # Panics
390///
391/// This will panic if the public key of the other side wasn't set.
392pub fn get_emoji(
393    sas: &EstablishedSas,
394    ids: &SasIds,
395    flow_id: &str,
396    we_started: bool,
397) -> [Emoji; 7] {
398    let bytes = sas.bytes(&extra_info_sas(
399        ids,
400        sas.our_public_key(),
401        sas.their_public_key(),
402        flow_id,
403        we_started,
404    ));
405
406    let indices = bytes.emoji_indices();
407
408    [
409        emoji_from_index(indices[0]),
410        emoji_from_index(indices[1]),
411        emoji_from_index(indices[2]),
412        emoji_from_index(indices[3]),
413        emoji_from_index(indices[4]),
414        emoji_from_index(indices[5]),
415        emoji_from_index(indices[6]),
416    ]
417}
418
419/// Get the index of the emoji of the short authentication string.
420///
421/// Returns seven u8 numbers in the range from 0 to 63 inclusive, those numbers
422/// can be converted to a unique emoji defined by the spec using the
423/// [emoji_from_index](#method.emoji_from_index) method.
424///
425/// # Arguments
426///
427/// * `sas` - The Olm SAS object that can be used to generate bytes using the
428///   shared secret.
429///
430/// * `ids` - The ids that are used for this SAS authentication flow.
431///
432/// * `flow_id` - The unique id that identifies this SAS verification process.
433///
434/// * `we_started` - Flag signaling if the SAS process was started on our side.
435///
436/// # Panics
437///
438/// This will panic if the public key of the other side wasn't set.
439pub fn get_emoji_index(
440    sas: &EstablishedSas,
441    ids: &SasIds,
442    flow_id: &str,
443    we_started: bool,
444) -> [u8; 7] {
445    let bytes = sas.bytes(&extra_info_sas(
446        ids,
447        sas.our_public_key(),
448        sas.their_public_key(),
449        flow_id,
450        we_started,
451    ));
452
453    bytes.emoji_indices()
454}
455
456/// Get the decimal version of the short authentication string.
457///
458/// Returns a tuple containing three 4 digit integer numbers that represent
459/// the short auth string.
460///
461/// # Arguments
462///
463/// * `sas` - The Olm SAS object that can be used to generate bytes using the
464///   shared secret.
465///
466/// * `ids` - The ids that are used for this SAS authentication flow.
467///
468/// * `flow_id` - The unique id that identifies this SAS verification process.
469///
470/// * `we_started` - Flag signaling if the SAS process was started on our side.
471///
472/// # Panics
473///
474/// This will panic if the public key of the other side wasn't set.
475pub fn get_decimal(
476    sas: &EstablishedSas,
477    ids: &SasIds,
478    flow_id: &str,
479    we_started: bool,
480) -> (u16, u16, u16) {
481    let bytes = sas.bytes(&extra_info_sas(
482        ids,
483        sas.our_public_key(),
484        sas.their_public_key(),
485        flow_id,
486        we_started,
487    ));
488
489    bytes.decimals()
490}
491
492#[cfg(all(test, not(target_family = "wasm")))]
493mod tests {
494    use ruma::{
495        events::key::verification::start::ToDeviceKeyVerificationStartEventContent, serde::Base64,
496    };
497    use serde_json::json;
498    use vodozemac::Curve25519PublicKey;
499
500    use super::calculate_commitment;
501    use crate::verification::event_enums::StartContent;
502
503    #[test]
504    fn commitment_calculation() {
505        let commitment = Base64::parse("CCQmB4JCdB0FW21FdAnHj/Hu8+W9+Nb0vgwPEnZZQ4g").unwrap();
506
507        let public_key =
508            Curve25519PublicKey::from_base64("Q/NmNFEUS1fS+YeEmiZkjjblKTitrKOAk7cPEumcMlg")
509                .unwrap();
510        let content = json!({
511            "from_device":"XOWLHHFSWM",
512            "transaction_id":"bYxBsirjUJO9osar6ST4i2M2NjrYLA7l",
513            "method":"m.sas.v1",
514            "key_agreement_protocols":["curve25519-hkdf-sha256","curve25519"],
515            "hashes":["sha256"],
516            "message_authentication_codes":["hkdf-hmac-sha256","hmac-sha256"],
517            "short_authentication_string":["decimal","emoji"]
518        });
519
520        let content: ToDeviceKeyVerificationStartEventContent =
521            serde_json::from_value(content).unwrap();
522        let content = StartContent::from(&content);
523        let calculated_commitment = calculate_commitment(public_key, &content);
524
525        assert_eq!(commitment, calculated_commitment);
526    }
527}