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}