diff --git a/Cargo.toml b/Cargo.toml index 050a6c8c..07953e68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "matrix_sdk", + "matrix_qrcode", "matrix_sdk_base", "matrix_sdk_test", "matrix_sdk_test_macros", diff --git a/matrix_qrcode/Cargo.toml b/matrix_qrcode/Cargo.toml new file mode 100644 index 00000000..f9ab6f34 --- /dev/null +++ b/matrix_qrcode/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "matrix_qrcode" +version = "0.1.0" +authors = ["Damir Jelić "] +edition = "2018" + +[features] +default = ["decode_image"] +decode_image = ["image", "rqrr", "qrcode/image", "qrcode/svg"] + +[dependencies] +base64 = "0.13.0" +byteorder = "1.4.3" +image = { version = "0.23.14", optional = true } +qrcode = { version = "0.12.0", default-features = false } +rqrr = { version = "0.3.2" , optional = true } +ruma-identifiers = "0.19.1" +thiserror = "1.0.24" diff --git a/matrix_qrcode/data/self-no-master.png b/matrix_qrcode/data/self-no-master.png new file mode 100644 index 00000000..1bffd19e Binary files /dev/null and b/matrix_qrcode/data/self-no-master.png differ diff --git a/matrix_qrcode/data/self-verification.png b/matrix_qrcode/data/self-verification.png new file mode 100644 index 00000000..a8383e76 Binary files /dev/null and b/matrix_qrcode/data/self-verification.png differ diff --git a/matrix_qrcode/data/verification.png b/matrix_qrcode/data/verification.png new file mode 100644 index 00000000..e4a918ec Binary files /dev/null and b/matrix_qrcode/data/verification.png differ diff --git a/matrix_qrcode/src/error.rs b/matrix_qrcode/src/error.rs new file mode 100644 index 00000000..0dc3303c --- /dev/null +++ b/matrix_qrcode/src/error.rs @@ -0,0 +1,47 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum DecodingError { + #[cfg(feature = "decode_image")] + #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] + #[error(transparent)] + Qr(#[from] rqrr::DeQRError), + #[error("the decoded QR code is missing the Matrix header")] + Header, + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), + #[error("the QR code contains an invalid verification mode: {0}")] + Mode(u8), + #[error(transparent)] + Identifier(#[from] ruma_identifiers::Error), + #[error(transparent)] + Read(#[from] std::io::Error), + #[error("the QR code contains a too short shared secret, length: {0}")] + SharedSecret(usize), + #[error("the QR code contains an invalid or unsupported version: {0}")] + Version(u8), +} + +#[derive(Error, Debug)] +pub enum EncodingError { + #[error(transparent)] + Qr(#[from] qrcode::types::QrError), + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error("The verification flow id length can't be converted into a u16: {0}")] + FlowId(#[from] std::num::TryFromIntError), +} diff --git a/matrix_qrcode/src/lib.rs b/matrix_qrcode/src/lib.rs new file mode 100644 index 00000000..ada9bdad --- /dev/null +++ b/matrix_qrcode/src/lib.rs @@ -0,0 +1,204 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! This crate implements methods to parse and generate QR codes that are used +//! for interactive verification in [Matrix](https://matrix.org/). +//! +//! It implements the QR format defined in the Matrix [spec]. +//! +//! [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format +//! +//! ```no_run +//! # use matrix_qrcode::{QrVerification, DecodingError}; +//! # fn main() -> Result<(), DecodingError> { +//! use image; +//! +//! let image = image::open("/path/to/my/image.png").unwrap(); +//! let result = QrVerification::from_image(image)?; +//! # Ok(()) +//! # } +//! ``` + +#![deny( + missing_debug_implementations, + dead_code, + trivial_casts, + trivial_numeric_casts, + unused_extern_crates, + unused_import_braces, + unused_qualifications +)] + +mod error; +mod types; +mod utils; + +pub use error::{DecodingError, EncodingError}; +#[cfg(feature = "decode_image")] +#[cfg_attr(feature = "docs", doc(cfg(decode_image)))] +pub use image; +pub use qrcode; +#[cfg(feature = "decode_image")] +#[cfg_attr(feature = "docs", doc(cfg(decode_image)))] +pub use rqrr; +pub use types::{ + QrVerification, SelfVerificationData, SelfVerificationNoMasterKey, VerificationData, +}; + +#[cfg(test)] +mod test { + #[cfg(feature = "decode_image")] + use std::{convert::TryFrom, io::Cursor}; + + #[cfg(feature = "decode_image")] + use image::{ImageFormat, Luma}; + #[cfg(feature = "decode_image")] + use qrcode::QrCode; + + #[cfg(feature = "decode_image")] + use crate::utils::decode_qr; + use crate::{DecodingError, QrVerification}; + + #[cfg(feature = "decode_image")] + static VERIFICATION: &[u8; 4277] = include_bytes!("../data/verification.png"); + #[cfg(feature = "decode_image")] + static SELF_VERIFICATION: &[u8; 1467] = include_bytes!("../data/self-verification.png"); + #[cfg(feature = "decode_image")] + static SELF_NO_MASTER: &[u8; 1775] = include_bytes!("../data/self-no-master.png"); + + #[test] + #[cfg(feature = "decode_image")] + fn decode_qr_test() { + let image = Cursor::new(VERIFICATION); + let image = image::load(image, ImageFormat::Png).unwrap().to_luma8(); + decode_qr(image).expect("Couldn't decode the QR code"); + } + + #[test] + #[cfg(feature = "decode_image")] + fn decode_test() { + let image = Cursor::new(VERIFICATION); + let image = image::load(image, ImageFormat::Png).unwrap().to_luma8(); + let result = QrVerification::try_from(image).unwrap(); + + assert!(matches!(result, QrVerification::Verification(_))); + } + + #[test] + #[cfg(feature = "decode_image")] + fn decode_encode_cycle() { + let image = Cursor::new(VERIFICATION); + let image = image::load(image, ImageFormat::Png).unwrap(); + let result = QrVerification::from_image(image).unwrap(); + + assert!(matches!(result, QrVerification::Verification(_))); + + let encoded = result.to_qr_code().unwrap(); + let image = encoded.render::>().build(); + let second_result = QrVerification::try_from(image).unwrap(); + + assert_eq!(result, second_result); + + let bytes = result.to_bytes().unwrap(); + let third_result = QrVerification::from_bytes(bytes).unwrap(); + + assert_eq!(result, third_result); + } + + #[test] + #[cfg(feature = "decode_image")] + fn decode_encode_cycle_self() { + let image = Cursor::new(SELF_VERIFICATION); + let image = image::load(image, ImageFormat::Png).unwrap(); + let result = QrVerification::try_from(image).unwrap(); + + assert!(matches!(result, QrVerification::SelfVerification(_))); + + let encoded = result.to_qr_code().unwrap(); + let image = encoded.render::>().build(); + let second_result = QrVerification::from_luma(image).unwrap(); + + assert_eq!(result, second_result); + + let bytes = result.to_bytes().unwrap(); + let third_result = QrVerification::from_bytes(bytes).unwrap(); + + assert_eq!(result, third_result); + } + + #[test] + #[cfg(feature = "decode_image")] + fn decode_encode_cycle_self_no_master() { + let image = Cursor::new(SELF_NO_MASTER); + let image = image::load(image, ImageFormat::Png).unwrap(); + let result = QrVerification::from_image(image).unwrap(); + + assert!(matches!(result, QrVerification::SelfVerificationNoMasterKey(_))); + + let encoded = result.to_qr_code().unwrap(); + let image = encoded.render::>().build(); + let second_result = QrVerification::try_from(image).unwrap(); + + assert_eq!(result, second_result); + + let bytes = result.to_bytes().unwrap(); + let third_result = QrVerification::try_from(bytes).unwrap(); + + assert_eq!(result, third_result); + } + + #[test] + #[cfg(feature = "decode_image")] + fn decode_invalid_qr() { + let qr = QrCode::new(b"NonMatrixCode").expect("Can't build a simple QR code"); + let image = qr.render::>().build(); + let result = QrVerification::try_from(image); + assert!(matches!(result, Err(DecodingError::Header))) + } + + #[test] + fn decode_invalid_header() { + let data = b"NonMatrixCode"; + let result = QrVerification::from_bytes(data); + assert!(matches!(result, Err(DecodingError::Header))) + } + + #[test] + fn decode_invalid_mode() { + let data = b"MATRIX\x02\x03"; + let result = QrVerification::from_bytes(data); + assert!(matches!(result, Err(DecodingError::Mode(3)))) + } + + #[test] + fn decode_invalid_version() { + let data = b"MATRIX\x01\x03"; + let result = QrVerification::from_bytes(data); + assert!(matches!(result, Err(DecodingError::Version(1)))) + } + + #[test] + fn decode_missing_data() { + let data = b"MATRIX\x02\x02"; + let result = QrVerification::from_bytes(data); + assert!(matches!(result, Err(DecodingError::Read(_)))) + } + + #[test] + fn decode_short_secret() { + let data = b"MATRIX\x02\x02\x00\x07FLOW_IDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBSECRET"; + let result = QrVerification::from_bytes(data); + assert!(matches!(result, Err(DecodingError::SharedSecret(_)))) + } +} diff --git a/matrix_qrcode/src/types.rs b/matrix_qrcode/src/types.rs new file mode 100644 index 00000000..efac59f5 --- /dev/null +++ b/matrix_qrcode/src/types.rs @@ -0,0 +1,315 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::{ + convert::TryFrom, + io::{Cursor, Read}, +}; + +use byteorder::{BigEndian, ReadBytesExt}; +#[cfg(feature = "decode_image")] +use image::{DynamicImage, ImageBuffer, Luma}; +use qrcode::QrCode; +use ruma_identifiers::EventId; + +#[cfg(feature = "decode_image")] +#[cfg_attr(feature = "docs", doc(cfg(decode_image)))] +use crate::utils::decode_qr; +use crate::{ + error::{DecodingError, EncodingError}, + utils::{base_64_encode, to_bytes, to_qr_code, HEADER, MAX_MODE, MIN_SECRET_LEN, VERSION}, +}; + +#[derive(Clone, Debug, PartialEq)] +pub enum QrVerification { + Verification(VerificationData), + SelfVerification(SelfVerificationData), + SelfVerificationNoMasterKey(SelfVerificationNoMasterKey), +} + +#[cfg(feature = "decode_image")] +#[cfg_attr(feature = "docs", doc(cfg(decode_image)))] +impl TryFrom for QrVerification { + type Error = DecodingError; + + fn try_from(image: DynamicImage) -> Result { + Self::from_image(image) + } +} + +#[cfg(feature = "decode_image")] +#[cfg_attr(feature = "docs", doc(cfg(decode_image)))] +impl TryFrom, Vec>> for QrVerification { + type Error = DecodingError; + + fn try_from(image: ImageBuffer, Vec>) -> Result { + Self::from_luma(image) + } +} + +impl TryFrom> for QrVerification { + type Error = DecodingError; + + fn try_from(value: Vec) -> Result { + Self::from_bytes(value) + } +} + +impl QrVerification { + #[cfg(feature = "decode_image")] + #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] + pub fn from_image(image: DynamicImage) -> Result { + let image = image.to_luma8(); + Self::decode(image) + } + + #[cfg(feature = "decode_image")] + #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] + pub fn from_luma(image: ImageBuffer, Vec>) -> Result { + Self::decode(image) + } + + pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result { + Self::decode_bytes(bytes) + } + + pub fn to_qr_code(&self) -> Result { + match self { + QrVerification::Verification(v) => v.to_qr_code(), + QrVerification::SelfVerification(v) => v.to_qr_code(), + QrVerification::SelfVerificationNoMasterKey(v) => v.to_qr_code(), + } + } + + pub fn to_bytes(&self) -> Result, EncodingError> { + match self { + QrVerification::Verification(v) => v.to_bytes(), + QrVerification::SelfVerification(v) => v.to_bytes(), + QrVerification::SelfVerificationNoMasterKey(v) => v.to_bytes(), + } + } + + fn decode_bytes(bytes: impl AsRef<[u8]>) -> Result { + let mut decoded = Cursor::new(bytes); + + let mut header = [0u8; 6]; + let mut first_key = [0u8; 32]; + let mut second_key = [0u8; 32]; + + decoded.read_exact(&mut header)?; + let version = decoded.read_u8()?; + let mode = decoded.read_u8()?; + + if header != HEADER { + return Err(DecodingError::Header); + } else if version != VERSION { + return Err(DecodingError::Version(version)); + } else if mode > MAX_MODE { + return Err(DecodingError::Mode(mode)); + } + + let flow_id_len = decoded.read_u16::()?; + let mut flow_id = vec![0; flow_id_len.into()]; + + decoded.read_exact(&mut flow_id)?; + decoded.read_exact(&mut first_key)?; + decoded.read_exact(&mut second_key)?; + + let mut shared_secret = Vec::new(); + + decoded.read_to_end(&mut shared_secret)?; + + if shared_secret.len() < MIN_SECRET_LEN { + return Err(DecodingError::SharedSecret(shared_secret.len())); + } + + QrVerification::new(mode, flow_id, first_key, second_key, shared_secret) + } + + #[cfg(feature = "decode_image")] + #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] + fn decode(image: ImageBuffer, Vec>) -> Result { + let decoded = decode_qr(image)?; + Self::decode_bytes(decoded) + } + + fn new( + mode: u8, + flow_id: Vec, + first_key: [u8; 32], + second_key: [u8; 32], + shared_secret: Vec, + ) -> Result { + let first_key = base_64_encode(&first_key); + let second_key = base_64_encode(&second_key); + let flow_id = String::from_utf8(flow_id)?; + let shared_secret = base_64_encode(&shared_secret); + + match mode { + VerificationData::QR_MODE => { + let event_id = EventId::try_from(flow_id)?; + Ok(VerificationData::new(event_id, first_key, second_key, shared_secret).into()) + } + SelfVerificationData::QR_MODE => { + Ok(SelfVerificationData::new(flow_id, first_key, second_key, shared_secret).into()) + } + SelfVerificationNoMasterKey::QR_MODE => { + Ok(SelfVerificationNoMasterKey::new(flow_id, first_key, second_key, shared_secret) + .into()) + } + m => Err(DecodingError::Mode(m)), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct VerificationData { + event_id: EventId, + first_master_key: String, + second_master_key: String, + shared_secret: String, +} + +impl VerificationData { + const QR_MODE: u8 = 0x00; + + pub fn to_bytes(&self) -> Result, EncodingError> { + to_bytes( + Self::QR_MODE, + &self.event_id.as_str(), + &self.first_master_key, + &self.second_master_key, + &self.shared_secret, + ) + } + + pub fn to_qr_code(&self) -> Result { + to_qr_code( + Self::QR_MODE, + self.event_id.as_str(), + &self.first_master_key, + &self.second_master_key, + &self.shared_secret, + ) + } + + pub fn new( + event_id: EventId, + first_key: String, + second_key: String, + shared_secret: String, + ) -> Self { + Self { event_id, first_master_key: first_key, second_master_key: second_key, shared_secret } + } +} + +impl From for QrVerification { + fn from(data: VerificationData) -> Self { + Self::Verification(data) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SelfVerificationData { + transaction_id: String, + master_key: String, + device_key: String, + shared_secret: String, +} + +impl SelfVerificationData { + const QR_MODE: u8 = 0x01; + + pub fn to_bytes(&self) -> Result, EncodingError> { + to_bytes( + Self::QR_MODE, + &self.transaction_id, + &self.master_key, + &self.device_key, + &self.shared_secret, + ) + } + + pub fn to_qr_code(&self) -> Result { + to_qr_code( + Self::QR_MODE, + &self.transaction_id, + &self.master_key, + &self.device_key, + &self.shared_secret, + ) + } + + pub fn new( + transaction_id: String, + master_key: String, + device_key: String, + shared_secret: String, + ) -> Self { + Self { transaction_id, master_key, device_key, shared_secret } + } +} + +impl From for QrVerification { + fn from(data: SelfVerificationData) -> Self { + Self::SelfVerification(data) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct SelfVerificationNoMasterKey { + transaction_id: String, + device_key: String, + master_key: String, + shared_secret: String, +} + +impl SelfVerificationNoMasterKey { + const QR_MODE: u8 = 0x02; + + pub fn to_bytes(&self) -> Result, EncodingError> { + to_bytes( + Self::QR_MODE, + &self.transaction_id, + &self.device_key, + &self.master_key, + &self.shared_secret, + ) + } + + pub fn to_qr_code(&self) -> Result { + to_qr_code( + Self::QR_MODE, + &self.transaction_id, + &self.device_key, + &self.master_key, + &self.shared_secret, + ) + } + + pub fn new( + transaction_id: String, + device_key: String, + master_key: String, + shared_secret: String, + ) -> Self { + Self { transaction_id, device_key, master_key, shared_secret } + } +} + +impl From for QrVerification { + fn from(data: SelfVerificationNoMasterKey) -> Self { + Self::SelfVerificationNoMasterKey(data) + } +} diff --git a/matrix_qrcode/src/utils.rs b/matrix_qrcode/src/utils.rs new file mode 100644 index 00000000..6a30ff76 --- /dev/null +++ b/matrix_qrcode/src/utils.rs @@ -0,0 +1,100 @@ +// Copyright 2021 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::convert::TryInto; + +use base64::{decode_config, encode_config, STANDARD_NO_PAD}; +#[cfg(feature = "decode_image")] +use image::{ImageBuffer, Luma}; +use qrcode::QrCode; + +#[cfg(feature = "decode_image")] +use crate::error::DecodingError; +use crate::error::EncodingError; + +pub(crate) const HEADER: &[u8] = b"MATRIX"; +pub(crate) const VERSION: u8 = 0x2; +pub(crate) const MAX_MODE: u8 = 0x2; +pub(crate) const MIN_SECRET_LEN: usize = 8; + +pub(crate) fn base_64_encode(data: &[u8]) -> String { + encode_config(data, STANDARD_NO_PAD) +} + +pub(crate) fn base64_decode(data: &str) -> Result, base64::DecodeError> { + decode_config(data, STANDARD_NO_PAD) +} + +pub(crate) fn to_bytes( + mode: u8, + flow_id: &str, + first_key: &str, + second_key: &str, + shared_secret: &str, +) -> Result, EncodingError> { + let flow_id_len: u16 = flow_id.len().try_into()?; + let flow_id_len = flow_id_len.to_be_bytes(); + + let first_key = base64_decode(first_key)?; + let second_key = base64_decode(second_key)?; + let shared_secret = base64_decode(shared_secret)?; + + let data = [ + HEADER, + &[VERSION], + &[mode], + flow_id_len.as_ref(), + flow_id.as_bytes(), + &first_key, + &second_key, + &shared_secret, + ] + .concat(); + + Ok(data) +} + +pub(crate) fn to_qr_code( + mode: u8, + flow_id: &str, + first_key: &str, + second_key: &str, + shared_secret: &str, +) -> Result { + let data = to_bytes(mode, flow_id, first_key, second_key, shared_secret)?; + Ok(QrCode::new(data)?) +} + +#[cfg(feature = "decode_image")] +pub(crate) fn decode_qr(image: ImageBuffer, Vec>) -> Result, DecodingError> { + let mut image = rqrr::PreparedImage::prepare(image); + let grids = image.detect_grids(); + + let mut error = None; + + for grid in grids { + let mut decoded = Vec::new(); + + match grid.decode_to(&mut decoded) { + Ok(_) => { + if decoded.starts_with(HEADER) { + return Ok(decoded); + } + } + Err(e) => error = Some(e), + } + } + + Err(error.map(|e| e.into()).unwrap_or_else(|| DecodingError::Header)) +}