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    events::{
19        key::verification::{
20            cancel::CancelCode,
21            mac::{KeyVerificationMacEventContent, ToDeviceKeyVerificationMacEventContent},
22        },
23        relation::Reference,
24        AnyMessageLikeEventContent, AnyToDeviceEventContent,
25    },
26    serde::Base64,
27    DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceKeyId, UserId,
28};
29use sha2::{Digest, Sha256};
30use tracing::{trace, warn};
31use vodozemac::{sas::EstablishedSas, Curve25519PublicKey};
32
33use super::{sas_state::SupportedMacMethod, FlowId, OutgoingContent};
34use crate::{
35    identities::{DeviceData, UserIdentityData},
36    olm::StaticAccountData,
37    verification::event_enums::{MacContent, StartContent},
38    Emoji, OwnUserIdentityData,
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://matrix.org/docs/spec/client_server/latest#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        if own_identity.is_verified() {
305            if let Some(key) = own_identity.master_key().get_first_key() {
306                let key_id = format!("{}:{}", DeviceKeyAlgorithm::Ed25519, key.to_base64());
307
308                let calculated_mac =
309                    mac_method.calculate_mac(sas, &key.to_base64(), &format!("{info}{key_id}"));
310
311                mac.insert(key_id, calculated_mac);
312            }
313        }
314    }
315
316    let mut keys: Vec<_> = mac.keys().map(|s| s.as_str()).collect();
317    keys.sort_unstable();
318
319    let keys = mac_method.calculate_mac(sas, &keys.join(","), &format!("{info}KEY_IDS"));
320
321    match flow_id {
322        FlowId::ToDevice(s) => AnyToDeviceEventContent::KeyVerificationMac(
323            ToDeviceKeyVerificationMacEventContent::new(s.clone(), mac, keys),
324        )
325        .into(),
326        FlowId::InRoom(r, e) => {
327            (
328                r.clone(),
329                AnyMessageLikeEventContent::KeyVerificationMac(
330                    KeyVerificationMacEventContent::new(mac, keys, Reference::new(e.clone())),
331                ),
332            )
333                .into()
334        }
335    }
336}
337
338/// Get the extra info that will be used when we generate bytes for the short
339/// auth string.
340///
341/// # Arguments
342///
343/// * `ids` - The ids that are used for this SAS authentication flow.
344///
345/// * `flow_id` - The unique id that identifies this SAS verification process.
346///
347/// * `we_started` - Flag signaling if the SAS process was started on our side.
348fn extra_info_sas(
349    ids: &SasIds,
350    own_pubkey: Curve25519PublicKey,
351    their_pubkey: Curve25519PublicKey,
352    flow_id: &str,
353    we_started: bool,
354) -> String {
355    let our_info =
356        format!("{}|{}|{}", ids.account.user_id, ids.account.device_id, own_pubkey.to_base64());
357    let their_info = format!(
358        "{}|{}|{}",
359        ids.other_device.user_id(),
360        ids.other_device.device_id(),
361        their_pubkey.to_base64()
362    );
363
364    let (first_info, second_info) =
365        if we_started { (our_info, their_info) } else { (their_info, our_info) };
366
367    let info = format!("MATRIX_KEY_VERIFICATION_SAS|{first_info}|{second_info}|{flow_id}");
368
369    trace!("Generated a SAS extra info: {}", info);
370
371    info
372}
373
374/// Get the emoji version of the short authentication string.
375///
376/// Returns seven tuples where the first element is the emoji and the
377/// second element the English description of the emoji.
378///
379/// # Arguments
380///
381/// * `sas` - The Olm SAS object that can be used to generate bytes using the
382///   shared secret.
383///
384/// * `ids` - The ids that are used for this SAS authentication flow.
385///
386/// * `flow_id` - The unique id that identifies this SAS verification process.
387///
388/// * `we_started` - Flag signaling if the SAS process was started on our side.
389///
390/// # Panics
391///
392/// This will panic if the public key of the other side wasn't set.
393pub fn get_emoji(
394    sas: &EstablishedSas,
395    ids: &SasIds,
396    flow_id: &str,
397    we_started: bool,
398) -> [Emoji; 7] {
399    let bytes = sas.bytes(&extra_info_sas(
400        ids,
401        sas.our_public_key(),
402        sas.their_public_key(),
403        flow_id,
404        we_started,
405    ));
406
407    let indices = bytes.emoji_indices();
408
409    [
410        emoji_from_index(indices[0]),
411        emoji_from_index(indices[1]),
412        emoji_from_index(indices[2]),
413        emoji_from_index(indices[3]),
414        emoji_from_index(indices[4]),
415        emoji_from_index(indices[5]),
416        emoji_from_index(indices[6]),
417    ]
418}
419
420/// Get the index of the emoji of the short authentication string.
421///
422/// Returns seven u8 numbers in the range from 0 to 63 inclusive, those numbers
423/// can be converted to a unique emoji defined by the spec using the
424/// [emoji_from_index](#method.emoji_from_index) method.
425///
426/// # Arguments
427///
428/// * `sas` - The Olm SAS object that can be used to generate bytes using the
429///   shared secret.
430///
431/// * `ids` - The ids that are used for this SAS authentication flow.
432///
433/// * `flow_id` - The unique id that identifies this SAS verification process.
434///
435/// * `we_started` - Flag signaling if the SAS process was started on our side.
436///
437/// # Panics
438///
439/// This will panic if the public key of the other side wasn't set.
440pub fn get_emoji_index(
441    sas: &EstablishedSas,
442    ids: &SasIds,
443    flow_id: &str,
444    we_started: bool,
445) -> [u8; 7] {
446    let bytes = sas.bytes(&extra_info_sas(
447        ids,
448        sas.our_public_key(),
449        sas.their_public_key(),
450        flow_id,
451        we_started,
452    ));
453
454    bytes.emoji_indices()
455}
456
457/// Get the decimal version of the short authentication string.
458///
459/// Returns a tuple containing three 4 digit integer numbers that represent
460/// the short auth string.
461///
462/// # Arguments
463///
464/// * `sas` - The Olm SAS object that can be used to generate bytes using the
465///   shared secret.
466///
467/// * `ids` - The ids that are used for this SAS authentication flow.
468///
469/// * `flow_id` - The unique id that identifies this SAS verification process.
470///
471/// * `we_started` - Flag signaling if the SAS process was started on our side.
472///
473/// # Panics
474///
475/// This will panic if the public key of the other side wasn't set.
476pub fn get_decimal(
477    sas: &EstablishedSas,
478    ids: &SasIds,
479    flow_id: &str,
480    we_started: bool,
481) -> (u16, u16, u16) {
482    let bytes = sas.bytes(&extra_info_sas(
483        ids,
484        sas.our_public_key(),
485        sas.their_public_key(),
486        flow_id,
487        we_started,
488    ));
489
490    bytes.decimals()
491}
492
493#[cfg(all(test, not(target_arch = "wasm32")))]
494mod tests {
495    use ruma::{
496        events::key::verification::start::ToDeviceKeyVerificationStartEventContent, serde::Base64,
497    };
498    use serde_json::json;
499    use vodozemac::Curve25519PublicKey;
500
501    use super::calculate_commitment;
502    use crate::verification::event_enums::StartContent;
503
504    #[test]
505    fn commitment_calculation() {
506        let commitment = Base64::parse("CCQmB4JCdB0FW21FdAnHj/Hu8+W9+Nb0vgwPEnZZQ4g").unwrap();
507
508        let public_key =
509            Curve25519PublicKey::from_base64("Q/NmNFEUS1fS+YeEmiZkjjblKTitrKOAk7cPEumcMlg")
510                .unwrap();
511        let content = json!({
512            "from_device":"XOWLHHFSWM",
513            "transaction_id":"bYxBsirjUJO9osar6ST4i2M2NjrYLA7l",
514            "method":"m.sas.v1",
515            "key_agreement_protocols":["curve25519-hkdf-sha256","curve25519"],
516            "hashes":["sha256"],
517            "message_authentication_codes":["hkdf-hmac-sha256","hmac-sha256"],
518            "short_authentication_string":["decimal","emoji"]
519        });
520
521        let content: ToDeviceKeyVerificationStartEventContent =
522            serde_json::from_value(content).unwrap();
523        let content = StartContent::from(&content);
524        let calculated_commitment = calculate_commitment(public_key, &content);
525
526        assert_eq!(commitment, calculated_commitment);
527    }
528}