use std::{
io::{Cursor, Read},
str::{self, Utf8Error},
};
use byteorder::{BigEndian, ReadBytesExt};
use thiserror::Error;
use url::Url;
use vodozemac::{base64_decode, base64_encode, Curve25519PublicKey};
const VERSION: u8 = 0x02;
const PREFIX: &[u8] = b"MATRIX";
#[derive(Debug, Error)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
pub enum LoginQrCodeDecodeError {
#[error("The QR code data is missing some fields.")]
NotEnoughData(#[from] std::io::Error),
#[error("One of the URLs in the QR code data is not a valid UTF-8 string")]
NotUtf8(#[from] Utf8Error),
#[error("One of the URLs in the QR code data could not be parsed: {0:?}")]
UrlParse(#[from] url::ParseError),
#[error(
"The QR code data contains an invalid QR code login mode, expected 0x03 or 0x04, got {0}"
)]
InvalidMode(u8),
#[error("The QR code data contains an unsupported version, expected {VERSION}, got {0}")]
InvalidVersion(u8),
#[error("The QR code data could not have been decoded from a base64 string: {0:?}")]
Base64(#[from] vodozemac::Base64DecodeError),
#[error("The QR code data has an unexpected prefix, expected: {expected:?}, got {got:?}")]
InvalidPrefix {
expected: &'static [u8],
got: [u8; 6],
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QrCodeModeData {
Login,
Reciprocate {
server_name: String,
},
}
impl QrCodeModeData {
pub fn mode(&self) -> QrCodeMode {
self.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QrCodeMode {
Login = 0x03,
Reciprocate = 0x04,
}
impl TryFrom<u8> for QrCodeMode {
type Error = LoginQrCodeDecodeError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0x03 => Ok(Self::Login),
0x04 => Ok(Self::Reciprocate),
mode => Err(LoginQrCodeDecodeError::InvalidMode(mode)),
}
}
}
impl From<&QrCodeModeData> for QrCodeMode {
fn from(value: &QrCodeModeData) -> Self {
match value {
QrCodeModeData::Login => Self::Login,
QrCodeModeData::Reciprocate { .. } => Self::Reciprocate,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QrCodeData {
pub public_key: Curve25519PublicKey,
pub rendezvous_url: Url,
pub mode_data: QrCodeModeData,
}
impl QrCodeData {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, LoginQrCodeDecodeError> {
let mut reader = Cursor::new(bytes);
let mut prefix = [0u8; PREFIX.len()];
reader.read_exact(&mut prefix)?;
if PREFIX != prefix {
return Err(LoginQrCodeDecodeError::InvalidPrefix { expected: PREFIX, got: prefix });
}
let version = reader.read_u8()?;
if version == VERSION {
let mode = QrCodeMode::try_from(reader.read_u8()?)?;
let mut public_key = [0u8; Curve25519PublicKey::LENGTH];
reader.read_exact(&mut public_key)?;
let public_key = Curve25519PublicKey::from_bytes(public_key);
let rendezvous_url_len = reader.read_u16::<BigEndian>()?;
let mut rendezvous_url = vec![0u8; rendezvous_url_len.into()];
reader.read_exact(&mut rendezvous_url)?;
let rendezvous_url = Url::parse(str::from_utf8(&rendezvous_url)?)?;
let mode_data = match mode {
QrCodeMode::Login => QrCodeModeData::Login,
QrCodeMode::Reciprocate => {
let server_name_len = reader.read_u16::<BigEndian>()?;
let mut server_name = vec![0u8; server_name_len.into()];
reader.read_exact(&mut server_name)?;
let server_name = String::from_utf8(server_name).map_err(|e| e.utf8_error())?;
QrCodeModeData::Reciprocate { server_name }
}
};
Ok(Self { public_key, rendezvous_url, mode_data })
} else {
Err(LoginQrCodeDecodeError::InvalidVersion(version))
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let rendezvous_url_len = (self.rendezvous_url.as_str().len() as u16).to_be_bytes();
let encoded = [
PREFIX,
&[VERSION],
&[self.mode_data.mode() as u8],
self.public_key.as_bytes().as_slice(),
&rendezvous_url_len,
self.rendezvous_url.as_str().as_bytes(),
]
.concat();
if let QrCodeModeData::Reciprocate { server_name } = &self.mode_data {
let server_name_len = (server_name.as_str().len() as u16).to_be_bytes();
[encoded.as_slice(), &server_name_len, server_name.as_str().as_bytes()].concat()
} else {
encoded
}
}
pub fn from_base64(data: &str) -> Result<Self, LoginQrCodeDecodeError> {
Self::from_bytes(&base64_decode(data)?)
}
pub fn to_base64(&self) -> String {
base64_encode(self.to_bytes())
}
pub fn mode(&self) -> QrCodeMode {
self.mode_data.mode()
}
}
#[cfg(test)]
mod test {
use assert_matches2::assert_let;
use similar_asserts::assert_eq;
use super::*;
const QR_CODE_DATA: &[u8] = &[
0x4D, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x03, 0xd8, 0x86, 0x68, 0x6a, 0xb2, 0x19, 0x7b,
0x78, 0x0e, 0x30, 0x0a, 0x9d, 0x4a, 0x21, 0x47, 0x48, 0x07, 0x00, 0xd7, 0x92, 0x9f, 0x39,
0xab, 0x31, 0xb9, 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x00, 0x47, 0x68, 0x74, 0x74,
0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73,
0x2e, 0x6c, 0x61, 0x62, 0x2e, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x64, 0x65,
0x76, 0x2f, 0x65, 0x38, 0x64, 0x61, 0x36, 0x33, 0x35, 0x35, 0x2d, 0x35, 0x35, 0x30, 0x62,
0x2d, 0x34, 0x61, 0x33, 0x32, 0x2d, 0x61, 0x31, 0x39, 0x33, 0x2d, 0x31, 0x36, 0x31, 0x39,
0x64, 0x39, 0x38, 0x33, 0x30, 0x36, 0x36, 0x38,
];
const QR_CODE_DATA_RECIPROCATE: &[u8] = &[
0x4D, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x04, 0xd8, 0x86, 0x68, 0x6a, 0xb2, 0x19, 0x7b,
0x78, 0x0e, 0x30, 0x0a, 0x9d, 0x4a, 0x21, 0x47, 0x48, 0x07, 0x00, 0xd7, 0x92, 0x9f, 0x39,
0xab, 0x31, 0xb9, 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x00, 0x47, 0x68, 0x74, 0x74,
0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x7a, 0x76, 0x6f, 0x75, 0x73,
0x2e, 0x6c, 0x61, 0x62, 0x2e, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x64, 0x65,
0x76, 0x2f, 0x65, 0x38, 0x64, 0x61, 0x36, 0x33, 0x35, 0x35, 0x2d, 0x35, 0x35, 0x30, 0x62,
0x2d, 0x34, 0x61, 0x33, 0x32, 0x2d, 0x61, 0x31, 0x39, 0x33, 0x2d, 0x31, 0x36, 0x31, 0x39,
0x64, 0x39, 0x38, 0x33, 0x30, 0x36, 0x36, 0x38, 0x00, 0x0A, 0x6d, 0x61, 0x74, 0x72, 0x69,
0x78, 0x2e, 0x6f, 0x72, 0x67,
];
const QR_CODE_DATA_BASE64: &str = "\
TUFUUklYAgS0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PABaaHR0\
cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2L19zeW5hcHNlL2Ns\
aWVudC9yZW5kZXp2b3VzLzAxSFg5SzAwUTFINktQRDQ3RUc0RzFUM1hHACVo\
dHRwczovL3N5bmFwc2Utb2lkYy5sYWIuZWxlbWVudC5kZXYv";
#[test]
fn parse_qr_data() {
let expected_curve_key =
Curve25519PublicKey::from_base64("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws")
.unwrap();
let expected_rendezvous =
Url::parse("https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668")
.unwrap();
let data = QrCodeData::from_bytes(QR_CODE_DATA)
.expect("We should be able to parse the QR code data");
assert_eq!(
expected_curve_key, data.public_key,
"The parsed public key should match the expected one"
);
assert_eq!(
expected_rendezvous, data.rendezvous_url,
"The parsed rendezvous URL should match expected one",
);
assert_eq!(
data.mode(),
QrCodeMode::Login,
"The mode in the test bytes vector should be Login"
);
assert_eq!(
QrCodeModeData::Login,
data.mode_data,
"The parsed QR code mode should match expected one",
);
}
#[test]
fn parse_qr_data_reciprocate() {
let expected_curve_key =
Curve25519PublicKey::from_base64("2IZoarIZe3gOMAqdSiFHSAcA15KfOasxueUUNwJI7Ws")
.unwrap();
let expected_rendezvous =
Url::parse("https://rendezvous.lab.element.dev/e8da6355-550b-4a32-a193-1619d9830668")
.unwrap();
let data = QrCodeData::from_bytes(QR_CODE_DATA_RECIPROCATE)
.expect("We should be able to parse the QR code data");
assert_eq!(
expected_curve_key, data.public_key,
"The parsed public key should match the expected one"
);
assert_eq!(
expected_rendezvous, data.rendezvous_url,
"The parsed rendezvous URL should match expected one",
);
assert_eq!(
data.mode(),
QrCodeMode::Reciprocate,
"The mode in the test bytes vector should be Reciprocate"
);
assert_let!(
QrCodeModeData::Reciprocate { server_name } = data.mode_data,
"The parsed QR code mode should match the expected one",
);
assert_eq!(
server_name, "matrix.org",
"We should have correctly found the matrix.org homeserver in the QR code data"
);
}
#[test]
fn parse_qr_data_base64() {
let expected_curve_key =
Curve25519PublicKey::from_base64("tMs2dUFaUNY5Z6MVl93eY61kRXxC8Y0toDe6c/cvtzw")
.unwrap();
let expected_rendezvous =
Url::parse("https://synapse-oidc.lab.element.dev/_synapse/client/rendezvous/01HX9K00Q1H6KPD47EG4G1T3XG")
.unwrap();
let expected_server_name = "https://synapse-oidc.lab.element.dev/";
let data = QrCodeData::from_base64(QR_CODE_DATA_BASE64)
.expect("We should be able to parse the QR code data");
assert_eq!(
expected_curve_key, data.public_key,
"The parsed public key should match the expected one"
);
assert_eq!(
data.mode(),
QrCodeMode::Reciprocate,
"The mode in the test bytes vector should be Reciprocate"
);
assert_eq!(
expected_rendezvous, data.rendezvous_url,
"The parsed rendezvous URL should match the expected one",
);
assert_let!(QrCodeModeData::Reciprocate { server_name } = data.mode_data);
assert_eq!(
server_name, expected_server_name,
"The parsed server name should match the expected one"
);
}
#[test]
fn qr_code_encoding_roundtrip() {
let data = QrCodeData::from_bytes(QR_CODE_DATA)
.expect("We should be able to parse the QR code data");
let encoded = data.to_bytes();
assert_eq!(
QR_CODE_DATA, &encoded,
"Decoding and re-encoding the QR code data should yield the same bytes"
);
let data = QrCodeData::from_base64(QR_CODE_DATA_BASE64)
.expect("We should be able to parse the QR code data");
let encoded = data.to_base64();
assert_eq!(
QR_CODE_DATA_BASE64, &encoded,
"Decoding and re-encoding the QR code data should yield the same base64 string"
);
}
}