Skip to main content

matrix_sdk_crypto/types/qr_login/
mod.rs

1// Copyright 2024 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//! Data types for the QR code login mechanism described in [MSC4108] and
16//! [MSC4388].
17//!
18//! [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
19//! [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
20
21use std::str::Utf8Error;
22
23use thiserror::Error;
24
25mod msc_4108;
26mod msc_4388;
27
28pub use msc_4108::Msc4108IntentData;
29use url::Url;
30use vodozemac::{Curve25519PublicKey, base64_decode, base64_encode};
31
32/// Error type for the decoding of the [`QrCodeData`].
33#[derive(Debug, Error)]
34#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
35pub enum LoginQrCodeDecodeError {
36    /// The QR code data is no long enough, it's missing some fields.
37    #[error("The QR code data is missing some fields.")]
38    NotEnoughData(#[from] std::io::Error),
39    /// One of the URLs in the QR code data is not a valid UTF-8 encoded string.
40    #[error("One of the URLs in the QR code data is not a valid UTF-8 string")]
41    NotUtf8(#[from] Utf8Error),
42    /// One of the URLs in the QR code data could not be parsed.
43    #[error("One of the URLs in the QR code data could not be parsed: {0:?}")]
44    UrlParse(#[from] url::ParseError),
45    /// The QR code data contains an invalid intent, we expect the login
46    /// intent or the reciprocate intent.
47    #[error(
48        "The QR code data contains an invalid QR code intent, expected {expected_login} or {expected_reciprocate}, got {got}"
49    )]
50    InvalidIntent {
51        /// The constant we expect for the login intent.
52        expected_login: u8,
53        /// The constant we expect for the reciprocate intent.
54        expected_reciprocate: u8,
55        /// The intent we received.
56        got: u8,
57    },
58    /// The QR code data contains an unsupported type.
59    #[error("The QR code data contains an unsupported type, expected {expected}, got {got}")]
60    InvalidType {
61        /// The type we expected.
62        expected: u8,
63        /// The type we received.
64        got: u8,
65    },
66    /// The base64 encoded variant of the QR code data is not a valid base64
67    /// string.
68    #[error("The QR code data could not have been decoded from a base64 string: {0:?}")]
69    Base64(#[from] vodozemac::Base64DecodeError),
70    /// The QR code data doesn't contain the expected `MATRIX` prefix.
71    #[error("The QR code data has an unexpected prefix, expected: {expected:?}, got {got:?}")]
72    InvalidPrefix {
73        /// The expected prefix.
74        expected: &'static [u8],
75        /// The prefix we received.
76        got: Vec<u8>,
77    },
78}
79
80/// Intent-specific data of the [`QrCodeData`].
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum QrCodeIntentData<'a> {
83    /// Intent-specific data in the case the QR code adheres to [MSC4108] of the
84    /// QR code data format.
85    ///
86    /// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
87    Msc4108 {
88        /// Intent specific data for the MSC4108 variant.
89        data: &'a Msc4108IntentData,
90        /// The rendezvous URL for the MSC4108 variant of the rendezvous
91        /// channel.
92        rendezvous_url: &'a Url,
93    },
94    /// Intent-specific data in the case the QR code adheres to [MSC4388] of the
95    /// QR code data format.
96    ///
97    /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
98    Msc4388 {
99        /// The ID of the rendezvous session, can be used to exchange messages
100        /// with the other device.
101        rendezvous_id: &'a str,
102        /// The base URL of the homeserver that the device generating the QR is
103        /// using.
104        base_url: &'a Url,
105    },
106}
107
108/// The intent of the device that generated/displayed the QR code.
109///
110/// The QR code login mechanism supports both, the new device, as well as the
111/// existing device to display the QR code.
112///
113/// The different intents have an explicit one-byte identifier which gets added
114/// to the QR code data.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
117pub enum QrCodeIntent {
118    /// Enum variant for the case where the new device is displaying the QR
119    /// code.
120    Login,
121    /// Enum variant for the case where the existing device is displaying the QR
122    /// code.
123    Reciprocate,
124}
125
126impl From<msc_4108::QrCodeIntent> for QrCodeIntent {
127    fn from(value: msc_4108::QrCodeIntent) -> Self {
128        match value {
129            msc_4108::QrCodeIntent::Login => Self::Login,
130            msc_4108::QrCodeIntent::Reciprocate => Self::Reciprocate,
131        }
132    }
133}
134
135impl From<msc_4388::QrCodeIntent> for QrCodeIntent {
136    fn from(value: msc_4388::QrCodeIntent) -> Self {
137        match value {
138            msc_4388::QrCodeIntent::Login => Self::Login,
139            msc_4388::QrCodeIntent::Reciprocate => Self::Reciprocate,
140        }
141    }
142}
143
144impl From<QrCodeIntent> for msc_4388::QrCodeIntent {
145    fn from(value: QrCodeIntent) -> Self {
146        match value {
147            QrCodeIntent::Login => msc_4388::QrCodeIntent::Login,
148            QrCodeIntent::Reciprocate => msc_4388::QrCodeIntent::Reciprocate,
149        }
150    }
151}
152
153/// Data for the QR code login mechanism.
154///
155/// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be
156/// decoded from a QR code.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct QrCodeData {
159    inner: QrCodeDataInner,
160}
161
162impl QrCodeData {
163    /// Create a new [`QrCodeData`] object which conforms to the data format
164    /// specified in [MSC4108].
165    ///
166    /// [MSC4108]: https://github.com/matrix-org/matrix-spec-proposals/pull/4108
167    pub fn new_msc4108(
168        public_key: Curve25519PublicKey,
169        rendezvous_url: Url,
170        intent_data: Msc4108IntentData,
171    ) -> Self {
172        Self {
173            inner: QrCodeDataInner::Msc4108(msc_4108::QrCodeData {
174                public_key,
175                rendezvous_url,
176                intent_data,
177            }),
178        }
179    }
180
181    /// Create a new [`QrCodeData`] object which conforms to the data format
182    /// specified in [MSC4388].
183    ///
184    /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388
185    pub fn new_msc4388(
186        public_key: Curve25519PublicKey,
187        rendezvous_id: String,
188        base_url: Url,
189        intent: QrCodeIntent,
190    ) -> Self {
191        Self {
192            inner: QrCodeDataInner::Msc4388(msc_4388::QrCodeData {
193                intent: intent.into(),
194                public_key,
195                rendezvous_id,
196                base_url,
197            }),
198        }
199    }
200
201    /// Attempt to decode a slice of bytes into a [`QrCodeData`] object.
202    ///
203    /// The slice of bytes would generally be returned by a QR code decoder.
204    pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoginQrCodeDecodeError> {
205        let inner = if bytes.starts_with(msc_4108::PREFIX) {
206            msc_4108::QrCodeData::from_bytes(bytes).map(QrCodeDataInner::Msc4108)?
207        } else {
208            msc_4388::QrCodeData::from_bytes(bytes).map(QrCodeDataInner::Msc4388)?
209        };
210
211        Ok(QrCodeData { inner })
212    }
213
214    /// Encode the [`QrCodeData`] into a list of bytes.
215    ///
216    /// The list of bytes can be used by a QR code generator to create an image
217    /// containing a QR code.
218    pub fn to_bytes(&self) -> Vec<u8> {
219        match &self.inner {
220            QrCodeDataInner::Msc4108(qr_code_data) => qr_code_data.to_bytes(),
221            QrCodeDataInner::Msc4388(qr_code_data) => qr_code_data.to_bytes(),
222        }
223    }
224
225    /// Attempt to decode a base64 encoded string into a [`QrCodeData`] object.
226    pub fn from_base64(data: &str) -> Result<Self, LoginQrCodeDecodeError> {
227        let bytes = base64_decode(data)?;
228        Self::from_bytes(&bytes)
229    }
230
231    /// Encode the [`QrCodeData`] into a base64 encoded string.
232    pub fn to_base64(&self) -> String {
233        let bytes = self.to_bytes();
234        base64_encode(bytes)
235    }
236
237    /// The ephemeral Curve25519 public key. Can be used to establish a shared
238    /// secret using the Diffie-Hellman key agreement.
239    pub fn public_key(&self) -> Curve25519PublicKey {
240        match &self.inner {
241            QrCodeDataInner::Msc4108(qr_code_data) => qr_code_data.public_key,
242            QrCodeDataInner::Msc4388(qr_code_data) => qr_code_data.public_key,
243        }
244    }
245
246    /// Get the [`QrCodeIntent`] of this [`QrCodeData`] object.
247    ///
248    /// This tells us if the creator of the QR code wants to log in or if they
249    /// want to log another device in.
250    pub fn intent(&self) -> QrCodeIntent {
251        match &self.inner {
252            QrCodeDataInner::Msc4108(qr_code_data) => qr_code_data.intent().into(),
253            QrCodeDataInner::Msc4388(qr_code_data) => qr_code_data.intent.clone().into(),
254        }
255    }
256
257    /// The intent-specific data for the QR code.
258    pub fn intent_data(&self) -> QrCodeIntentData<'_> {
259        match &self.inner {
260            QrCodeDataInner::Msc4108(qr_code_data) => QrCodeIntentData::Msc4108 {
261                data: &qr_code_data.intent_data,
262                rendezvous_url: &qr_code_data.rendezvous_url,
263            },
264            QrCodeDataInner::Msc4388(qr_code_data) => QrCodeIntentData::Msc4388 {
265                rendezvous_id: &qr_code_data.rendezvous_id,
266                base_url: &qr_code_data.base_url,
267            },
268        }
269    }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
273enum QrCodeDataInner {
274    Msc4108(msc_4108::QrCodeData),
275    Msc4388(msc_4388::QrCodeData),
276}