1use std::{
20 io::{Cursor, Read},
21 str::{self, Utf8Error},
22};
23
24use byteorder::{BigEndian, ReadBytesExt};
25use thiserror::Error;
26use url::Url;
27use vodozemac::{base64_decode, base64_encode, Curve25519PublicKey};
28
29const VERSION: u8 = 0x02;
31const PREFIX: &[u8] = b"MATRIX";
33
34#[derive(Debug, Error)]
36#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
37pub enum LoginQrCodeDecodeError {
38 #[error("The QR code data is missing some fields.")]
40 NotEnoughData(#[from] std::io::Error),
41 #[error("One of the URLs in the QR code data is not a valid UTF-8 string")]
43 NotUtf8(#[from] Utf8Error),
44 #[error("One of the URLs in the QR code data could not be parsed: {0:?}")]
46 UrlParse(#[from] url::ParseError),
47 #[error(
50 "The QR code data contains an invalid QR code login mode, expected 0x03 or 0x04, got {0}"
51 )]
52 InvalidMode(u8),
53 #[error("The QR code data contains an unsupported version, expected {VERSION}, got {0}")]
55 InvalidVersion(u8),
56 #[error("The QR code data could not have been decoded from a base64 string: {0:?}")]
59 Base64(#[from] vodozemac::Base64DecodeError),
60 #[error("The QR code data has an unexpected prefix, expected: {expected:?}, got {got:?}")]
62 InvalidPrefix {
63 expected: &'static [u8],
65 got: [u8; 6],
67 },
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum QrCodeModeData {
79 Login,
82 Reciprocate {
85 server_name: String,
88 },
89}
90
91impl QrCodeModeData {
92 pub fn mode(&self) -> QrCodeMode {
95 self.into()
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum QrCodeMode {
108 Login = 0x03,
111 Reciprocate = 0x04,
114}
115
116impl TryFrom<u8> for QrCodeMode {
117 type Error = LoginQrCodeDecodeError;
118
119 fn try_from(value: u8) -> Result<Self, Self::Error> {
120 match value {
121 0x03 => Ok(Self::Login),
122 0x04 => Ok(Self::Reciprocate),
123 mode => Err(LoginQrCodeDecodeError::InvalidMode(mode)),
124 }
125 }
126}
127
128impl From<&QrCodeModeData> for QrCodeMode {
129 fn from(value: &QrCodeModeData) -> Self {
130 match value {
131 QrCodeModeData::Login => Self::Login,
132 QrCodeModeData::Reciprocate { .. } => Self::Reciprocate,
133 }
134 }
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct QrCodeData {
143 pub public_key: Curve25519PublicKey,
146 pub rendezvous_url: Url,
149 pub mode_data: QrCodeModeData,
151}
152
153impl QrCodeData {
154 pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoginQrCodeDecodeError> {
158 let mut reader = Cursor::new(bytes);
171
172 let mut prefix = [0u8; PREFIX.len()];
175 reader.read_exact(&mut prefix)?;
176
177 if PREFIX != prefix {
178 return Err(LoginQrCodeDecodeError::InvalidPrefix { expected: PREFIX, got: prefix });
179 }
180
181 let version = reader.read_u8()?;
183 if version == VERSION {
184 let mode = QrCodeMode::try_from(reader.read_u8()?)?;
187
188 let mut public_key = [0u8; Curve25519PublicKey::LENGTH];
191 reader.read_exact(&mut public_key)?;
192 let public_key = Curve25519PublicKey::from_bytes(public_key);
193
194 let rendezvous_url_len = reader.read_u16::<BigEndian>()?;
196 let mut rendezvous_url = vec![0u8; rendezvous_url_len.into()];
198 reader.read_exact(&mut rendezvous_url)?;
199 let rendezvous_url = Url::parse(str::from_utf8(&rendezvous_url)?)?;
200
201 let mode_data = match mode {
202 QrCodeMode::Login => QrCodeModeData::Login,
203 QrCodeMode::Reciprocate => {
204 let server_name_len = reader.read_u16::<BigEndian>()?;
207
208 let mut server_name = vec![0u8; server_name_len.into()];
210 reader.read_exact(&mut server_name)?;
211 let server_name = String::from_utf8(server_name).map_err(|e| e.utf8_error())?;
212
213 QrCodeModeData::Reciprocate { server_name }
214 }
215 };
216
217 Ok(Self { public_key, rendezvous_url, mode_data })
218 } else {
219 Err(LoginQrCodeDecodeError::InvalidVersion(version))
220 }
221 }
222
223 pub fn to_bytes(&self) -> Vec<u8> {
228 let rendezvous_url_len = (self.rendezvous_url.as_str().len() as u16).to_be_bytes();
229
230 let encoded = [
231 PREFIX,
232 &[VERSION],
233 &[self.mode_data.mode() as u8],
234 self.public_key.as_bytes().as_slice(),
235 &rendezvous_url_len,
236 self.rendezvous_url.as_str().as_bytes(),
237 ]
238 .concat();
239
240 if let QrCodeModeData::Reciprocate { server_name } = &self.mode_data {
241 let server_name_len = (server_name.as_str().len() as u16).to_be_bytes();
242
243 [encoded.as_slice(), &server_name_len, server_name.as_str().as_bytes()].concat()
244 } else {
245 encoded
246 }
247 }
248
249 pub fn from_base64(data: &str) -> Result<Self, LoginQrCodeDecodeError> {
251 Self::from_bytes(&base64_decode(data)?)
252 }
253
254 pub fn to_base64(&self) -> String {
259 base64_encode(self.to_bytes())
260 }
261
262 pub fn mode(&self) -> QrCodeMode {
264 self.mode_data.mode()
265 }
266}
267
268#[cfg(test)]
269mod test {
270 use assert_matches2::assert_let;
271 use similar_asserts::assert_eq;
272
273 use super::*;
274
275 const QR_CODE_DATA: &[u8] = &[
277 0x4D, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x03, 0xd8, 0x86, 0x68, 0x6a, 0xb2, 0x19, 0x7b,
278 0x78, 0x0e, 0x30, 0x0a, 0x9d, 0x4a, 0x21, 0x47, 0x48, 0x07, 0x00, 0xd7, 0x92, 0x9f, 0x39,
279 0xab, 0x31, 0xb9, 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x00, 0x47, 0x68, 0x74, 0x74,
280 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73,
281 0x2e, 0x6c, 0x61, 0x62, 0x2e, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x64, 0x65,
282 0x76, 0x2f, 0x65, 0x38, 0x64, 0x61, 0x36, 0x33, 0x35, 0x35, 0x2d, 0x35, 0x35, 0x30, 0x62,
283 0x2d, 0x34, 0x61, 0x33, 0x32, 0x2d, 0x61, 0x31, 0x39, 0x33, 0x2d, 0x31, 0x36, 0x31, 0x39,
284 0x64, 0x39, 0x38, 0x33, 0x30, 0x36, 0x36, 0x38,
285 ];
286
287 const QR_CODE_DATA_RECIPROCATE: &[u8] = &[
290 0x4D, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x04, 0xd8, 0x86, 0x68, 0x6a, 0xb2, 0x19, 0x7b,
291 0x78, 0x0e, 0x30, 0x0a, 0x9d, 0x4a, 0x21, 0x47, 0x48, 0x07, 0x00, 0xd7, 0x92, 0x9f, 0x39,
292 0xab, 0x31, 0xb9, 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x00, 0x47, 0x68, 0x74, 0x74,
293 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73,
294 0x2e, 0x6c, 0x61, 0x62, 0x2e, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x64, 0x65,
295 0x76, 0x2f, 0x65, 0x38, 0x64, 0x61, 0x36, 0x33, 0x35, 0x35, 0x2d, 0x35, 0x35, 0x30, 0x62,
296 0x2d, 0x34, 0x61, 0x33, 0x32, 0x2d, 0x61, 0x31, 0x39, 0x33, 0x2d, 0x31, 0x36, 0x31, 0x39,
297 0x64, 0x39, 0x38, 0x33, 0x30, 0x36, 0x36, 0x38, 0x00, 0x0A, 0x6d, 0x61, 0x74, 0x72, 0x69,
298 0x78, 0x2e, 0x6f, 0x72, 0x67,
299 ];
300
301 const QR_CODE_DATA_BASE64: &str =
303 "TUFUUklYAgS0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PABaaHR0\
304 cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2L19zeW5hcHNlL2Ns\
305 aWVudC9yZW5kZXp2b3VzLzAxSFg5SzAwUTFINktQRDQ3RUc0RzFUM1hHACVo\
306 dHRwczovL3N5bmFwc2Utb2lkYy5sYWIuZWxlbWVudC5kZXYv";
307
308 #[test]
309 fn parse_qr_data() {
310 let expected_curve_key =
311 Curve25519PublicKey::from_base64("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws")
312 .unwrap();
313
314 let expected_rendezvous =
315 Url::parse("https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668")
316 .unwrap();
317
318 let data = QrCodeData::from_bytes(QR_CODE_DATA)
319 .expect("We should be able to parse the QR code data");
320
321 assert_eq!(
322 expected_curve_key, data.public_key,
323 "The parsed public key should match the expected one"
324 );
325
326 assert_eq!(
327 expected_rendezvous, data.rendezvous_url,
328 "The parsed rendezvous URL should match expected one",
329 );
330
331 assert_eq!(
332 data.mode(),
333 QrCodeMode::Login,
334 "The mode in the test bytes vector should be Login"
335 );
336
337 assert_eq!(
338 QrCodeModeData::Login,
339 data.mode_data,
340 "The parsed QR code mode should match expected one",
341 );
342 }
343
344 #[test]
345 fn parse_qr_data_reciprocate() {
346 let expected_curve_key =
347 Curve25519PublicKey::from_base64("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws")
348 .unwrap();
349
350 let expected_rendezvous =
351 Url::parse("https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668")
352 .unwrap();
353
354 let data = QrCodeData::from_bytes(QR_CODE_DATA_RECIPROCATE)
355 .expect("We should be able to parse the QR code data");
356
357 assert_eq!(
358 expected_curve_key, data.public_key,
359 "The parsed public key should match the expected one"
360 );
361
362 assert_eq!(
363 expected_rendezvous, data.rendezvous_url,
364 "The parsed rendezvous URL should match expected one",
365 );
366
367 assert_eq!(
368 data.mode(),
369 QrCodeMode::Reciprocate,
370 "The mode in the test bytes vector should be Reciprocate"
371 );
372
373 assert_let!(
374 QrCodeModeData::Reciprocate { server_name } = data.mode_data,
375 "The parsed QR code mode should match the expected one",
376 );
377
378 assert_eq!(
379 server_name, "matrix.org",
380 "We should have correctly found the matrix.org homeserver in the QR code data"
381 );
382 }
383
384 #[test]
385 fn parse_qr_data_base64() {
386 let expected_curve_key =
387 Curve25519PublicKey::from_base64("tMs2dUFaUNY5Z6MVl93eY61kRXxC8Y0toDe6c/cvtzw")
388 .unwrap();
389
390 let expected_rendezvous =
391 Url::parse("https://synapse-oidc.lab.element.dev/_synapse/client/rendezvous/01HX9K00Q1H6KPD47EG4G1T3XG")
392 .unwrap();
393
394 let expected_server_name = "https://synapse-oidc.lab.element.dev/";
395
396 let data = QrCodeData::from_base64(QR_CODE_DATA_BASE64)
397 .expect("We should be able to parse the QR code data");
398
399 assert_eq!(
400 expected_curve_key, data.public_key,
401 "The parsed public key should match the expected one"
402 );
403
404 assert_eq!(
405 data.mode(),
406 QrCodeMode::Reciprocate,
407 "The mode in the test bytes vector should be Reciprocate"
408 );
409
410 assert_eq!(
411 expected_rendezvous, data.rendezvous_url,
412 "The parsed rendezvous URL should match the expected one",
413 );
414
415 assert_let!(QrCodeModeData::Reciprocate { server_name } = data.mode_data);
416
417 assert_eq!(
418 server_name, expected_server_name,
419 "The parsed server name should match the expected one"
420 );
421 }
422
423 #[test]
424 fn qr_code_encoding_roundtrip() {
425 let data = QrCodeData::from_bytes(QR_CODE_DATA)
426 .expect("We should be able to parse the QR code data");
427
428 let encoded = data.to_bytes();
429
430 assert_eq!(
431 QR_CODE_DATA, &encoded,
432 "Decoding and re-encoding the QR code data should yield the same bytes"
433 );
434
435 let data = QrCodeData::from_base64(QR_CODE_DATA_BASE64)
436 .expect("We should be able to parse the QR code data");
437
438 let encoded = data.to_base64();
439
440 assert_eq!(
441 QR_CODE_DATA_BASE64, &encoded,
442 "Decoding and re-encoding the QR code data should yield the same base64 string"
443 );
444 }
445}