From 305766955b08770954b459b14ff396d32ad39494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 19 May 2021 16:10:48 +0200 Subject: [PATCH 1/7] matrix-sdk: Add a crate to generate and parse QR codes This patch adds types and methods to parse QR codes defined in the [spec]. It supports parsing the QR format from an image or from a byte string, converting back to an image and bytestring is possible as well. [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format --- Cargo.toml | 1 + matrix_qrcode/Cargo.toml | 18 ++ matrix_qrcode/data/self-no-master.png | Bin 0 -> 1775 bytes matrix_qrcode/data/self-verification.png | Bin 0 -> 1467 bytes matrix_qrcode/data/verification.png | Bin 0 -> 4277 bytes matrix_qrcode/src/error.rs | 47 ++++ matrix_qrcode/src/lib.rs | 204 +++++++++++++++ matrix_qrcode/src/types.rs | 315 +++++++++++++++++++++++ matrix_qrcode/src/utils.rs | 100 +++++++ 9 files changed, 685 insertions(+) create mode 100644 matrix_qrcode/Cargo.toml create mode 100644 matrix_qrcode/data/self-no-master.png create mode 100644 matrix_qrcode/data/self-verification.png create mode 100644 matrix_qrcode/data/verification.png create mode 100644 matrix_qrcode/src/error.rs create mode 100644 matrix_qrcode/src/lib.rs create mode 100644 matrix_qrcode/src/types.rs create mode 100644 matrix_qrcode/src/utils.rs 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 0000000000000000000000000000000000000000..1bffd19eef81114da9e9cce4af8cfb7fae37a4e9 GIT binary patch literal 1775 zcma)-dpOg39LGJaq{LFE<5H@VT$W1?c~()wgXjFMD}rOogO-2HD@v{Wm=pij;;6aj*8dO}KgnXV zJ#*ML3yrSb`p0gre4l4F9;?m%=-ch@z>gbSI^G#$hbFvJU}V@ zey1?=2JJpsosCo}aV;R1wMZ@v&ZR04HAUCOPi9%4t08OSU13&%nK!M~x>ZY@&EHFP z`WMY#OyaLvG(3gLJcP-(BtNs0WKpw+vvXm2g!@n)B`9gN*NuSDmeU0{pVQiK!X@cg zlA{kWQ6yRU#;#8qy>*9#x`poM$jTtupwmi6hL(b@btzUsC8J=_|l#?;;XG!G6}11ibDOyv8DLC zfj8#zS9ML49QUou-!+}j+L=4;HPDMIFz_uSX&{%Y4>-tJ)cM+&Pc>6<~y?a8r_Y(f#x@mcwhEh}d+=QXac$Zh(sJx65+ran zPjs;Gdweu^Rx|Z#NeM0Q@T+dz?$k2j5dn=v>7@@Z2-tpx1XJCV*Y8>U&6J)*-f$w5 z{u-cIpG{acLzbxQN&}8Ri`Ole9!-XUoz^+70%j|@5ONtOfN|wz{2A}xri=M+ZufJ} z37e%+O5sFMCvo)F;8#wm$I$u6#BM!Cv4R0u>v_7_6sHC$R6fp;Sl42v6Gl zpM&iUR)*?NMtxkH=li?5U#OIB8MOLJOH zb8J5~d_tLe=qn$T#-AOW2~q5ZuU(S}7<-bJ@LFVYn6^GxK?lJ!YO<@4CoU!wPxZo0 zFFY3Z$#drpPBrXQo05p7s8Q&Wl>!vp8dY~MGz>`{QWtq5^X1Q4xxL7Eu*D*=I8 zXkYmj9VkHaP~oDs?odK&e~w4jux(#{_T<%~%JCMV`9x#5L2H=o`l#1S+Z(YWvc)}8AYL3Yh+-G1rZ{F016$5K+-}K$|Xv1q=cK8tdN9@EnbGP zq6iFxZK9<#2?P==(y1wgs;gq-=seGu=T^1c91}JbaZ_B2Wi^I#*RjaK0q${ z_T}@q>gWC&>_)qHtinqVAG>edaYu3!CkV`YEgv}7bmh2FYbviGoP`S`&a8QzCv3g9 z>zhp1-~HdsYCIbde|PKd%nh~v|G9p{=Du1feRZKk-Kt9O#YsD{LY$PwQQihwfI7`H zime7{$1Q(xm&cPp(+oKIWvUms-&@tf&oPlXq4jGOt^4;{+O$)7{2b?fJtz3Q)@qV` z54cT~w1wNAfc0QoP~2$p$xM>vsrX6&7aX+laKsNG*5dT!z=Zi0DD}}TqG}>6aGBNf zTa9H7dm&q}a+!`l&~XRQ`zK(=7l=b~XyJNSUAp6PL8{SMXzE@X_o)BSGV7oMF$H*% z%OZR3k(NUnDO&q#D;Fshyaeb+1~$Kw&ti%n+)@+7t@(%`czF80g}Fcxo7WX9X~^nF zOg}3Fs=`Si!>6rWb<)a6RW~+fdEcb&%0jF`$I@S(jr@v$DRAcR}33)=*UePS$9HQr~@9)}lEVUnV; z50~1XX7a=4*0`aci=eu-_qKJ31vJPhC|kBTt^w!1Qd_}yG7Aej_vnutEu>a3jZGjv zlan1+dT$72G!2Y1@Lm`#XLQ$P|=F*hRxejTxx1lmg!COrUM`0hs z`|%aT+$MV2$(x2O$Bp2~X5G zOnUUPds{`lPZZ@#uy24EL6I9zJB``4UTsWbPYETO&0Pi|t)O|hGGf>Z{C?rE0Nem;lItt4o&4-Lxl^cyhxV^4A; zBhH%xwn;fmzbR6MJ$>v9XFc(N8Sfj;%u~%#i*J!f#x!q)$E!iP@g6O;YT1X0pJ;Vx<&-MMZ} zJ*AV5Es~^IzR=z5w;KF(lB}x}l28D6h?Os+VdY{`5?jBIgxdXfvpjNH+qd>XoA-+W zMtkjQZ8|7(`**G6xs?2=F+!SG$%5ipK}Z|4D1wwhmQf4{?h$B+AF)J!Omrh z$ziBYy`6sQ(n1H&qk1+J6sq7E{PC#n>GErYhz6^9{?AghoA_v~26HDaYkeEr5I!MB Jiw++9`fuI4&2j($ literal 0 HcmV?d00001 diff --git a/matrix_qrcode/data/verification.png b/matrix_qrcode/data/verification.png new file mode 100644 index 0000000000000000000000000000000000000000..e4a918ecbda618e9a085a53500f52a3edc9176d2 GIT binary patch literal 4277 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 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)) +} From b07332308980825ce5ea6d87d21ef11e6baa90e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 20 May 2021 14:04:07 +0200 Subject: [PATCH 2/7] qrcode: Add another TryFrom implementation --- matrix_qrcode/src/types.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/matrix_qrcode/src/types.rs b/matrix_qrcode/src/types.rs index efac59f5..38022a5b 100644 --- a/matrix_qrcode/src/types.rs +++ b/matrix_qrcode/src/types.rs @@ -58,6 +58,14 @@ impl TryFrom, Vec>> for QrVerification { } } +impl TryFrom<&[u8]> for QrVerification { + type Error = DecodingError; + + fn try_from(value: &[u8]) -> Result { + Self::from_bytes(value) + } +} + impl TryFrom> for QrVerification { type Error = DecodingError; From f49f5f16367a49c3991ec2c23da6ab2f4f98f883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 20 May 2021 14:04:40 +0200 Subject: [PATCH 3/7] qrcode: Add some more tests --- matrix_qrcode/src/lib.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/matrix_qrcode/src/lib.rs b/matrix_qrcode/src/lib.rs index ada9bdad..a297e908 100644 --- a/matrix_qrcode/src/lib.rs +++ b/matrix_qrcode/src/lib.rs @@ -197,8 +197,27 @@ mod test { #[test] fn decode_short_secret() { - let data = b"MATRIX\x02\x02\x00\x07FLOW_IDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBSECRET"; + let data = b"MATRIX\ + \x02\x02\x00\x07\ + FLOW_ID\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + SECRET"; + let result = QrVerification::from_bytes(data); assert!(matches!(result, Err(DecodingError::SharedSecret(_)))) } + + #[test] + fn decode_invalid_room_id() { + let data = b"MATRIX\ + \x02\x00\x00\x0f\ + test:localhost\ + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + SECRETISLONGENOUGH"; + + let result = QrVerification::from_bytes(data); + assert!(matches!(result, Err(DecodingError::Identifier(_)))) + } } From 8018b43443c752772fbd14495f1b3edd12ba9bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 20 May 2021 15:02:14 +0200 Subject: [PATCH 4/7] qrcode: Document the qrcode crate. --- matrix_qrcode/Cargo.toml | 6 + matrix_qrcode/src/error.rs | 14 ++ matrix_qrcode/src/lib.rs | 2 + matrix_qrcode/src/types.rs | 367 ++++++++++++++++++++++++++++++++++--- 4 files changed, 361 insertions(+), 28 deletions(-) diff --git a/matrix_qrcode/Cargo.toml b/matrix_qrcode/Cargo.toml index f9ab6f34..f3d26a12 100644 --- a/matrix_qrcode/Cargo.toml +++ b/matrix_qrcode/Cargo.toml @@ -4,10 +4,16 @@ version = "0.1.0" authors = ["Damir Jelić "] edition = "2018" +[package.metadata.docs.rs] +features = ["docs"] +rustdoc-args = ["--cfg", "feature=\"docs\""] + [features] default = ["decode_image"] decode_image = ["image", "rqrr", "qrcode/image", "qrcode/svg"] +docs = ["decode_image"] + [dependencies] base64 = "0.13.0" byteorder = "1.4.3" diff --git a/matrix_qrcode/src/error.rs b/matrix_qrcode/src/error.rs index 0dc3303c..cb628c09 100644 --- a/matrix_qrcode/src/error.rs +++ b/matrix_qrcode/src/error.rs @@ -14,34 +14,48 @@ use thiserror::Error; +/// Error type describing errors that happen while QR data is being decoded. #[derive(Error, Debug)] pub enum DecodingError { + /// Error decoding the QR code. #[cfg(feature = "decode_image")] #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] #[error(transparent)] Qr(#[from] rqrr::DeQRError), + /// The QR code data is missing the mandatory Matrix header. #[error("the decoded QR code is missing the Matrix header")] Header, + /// The QR code data is containing an invalid, non UTF-8, flow id. #[error(transparent)] Utf8(#[from] std::string::FromUtf8Error), + /// The QR code data is using an unsupported or invalid verification mode. #[error("the QR code contains an invalid verification mode: {0}")] Mode(u8), + /// The flow id is not a valid event ID. #[error(transparent)] Identifier(#[from] ruma_identifiers::Error), #[error(transparent)] + /// The QR code data does not contain all the necessary fields. Read(#[from] std::io::Error), + /// The QR code data uses an invalid shared secret. #[error("the QR code contains a too short shared secret, length: {0}")] SharedSecret(usize), + /// The QR code data uses an invalid or unsupported version. #[error("the QR code contains an invalid or unsupported version: {0}")] Version(u8), } +/// Error type describing errors that happen while QR data is being encoded. #[derive(Error, Debug)] pub enum EncodingError { + /// Error generating a QR code from the data, likely because the data + /// doesn't fit into a QR code. #[error(transparent)] Qr(#[from] qrcode::types::QrError), + /// Error decoding the identity keys as base64. #[error(transparent)] Base64(#[from] base64::DecodeError), + /// Error encoding the given flow id, the flow id is too large. #[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 index a297e908..91e60735 100644 --- a/matrix_qrcode/src/lib.rs +++ b/matrix_qrcode/src/lib.rs @@ -30,10 +30,12 @@ //! # } //! ``` +#![cfg_attr(feature = "docs", feature(doc_cfg))] #![deny( missing_debug_implementations, dead_code, trivial_casts, + missing_docs, trivial_numeric_casts, unused_extern_crates, unused_import_braces, diff --git a/matrix_qrcode/src/types.rs b/matrix_qrcode/src/types.rs index 38022a5b..d31161a4 100644 --- a/matrix_qrcode/src/types.rs +++ b/matrix_qrcode/src/types.rs @@ -31,10 +31,16 @@ use crate::{ utils::{base_64_encode, to_bytes, to_qr_code, HEADER, MAX_MODE, MIN_SECRET_LEN, VERSION}, }; +/// An enum representing the different modes a QR verification can be in. #[derive(Clone, Debug, PartialEq)] pub enum QrVerification { + /// The QR verification is verifying another user Verification(VerificationData), + /// The QR verification is self-verifying and the current device trusts or + /// owns the master key SelfVerification(SelfVerificationData), + /// The QR verification is self-verifying in which the current device does + /// not yet trust the master key SelfVerificationNoMasterKey(SelfVerificationNoMasterKey), } @@ -75,6 +81,26 @@ impl TryFrom> for QrVerification { } impl QrVerification { + /// Decode and parse an image of a QR code into a `QrVerification` + /// + /// The image will be converted into a grey scale image before decoding is + /// attempted + /// + /// # Arguments + /// + /// * `image` - The image containing the QR code. + /// + /// # Example + /// ```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(()) + /// # } + /// ``` #[cfg(feature = "decode_image")] #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] pub fn from_image(image: DynamicImage) -> Result { @@ -82,16 +108,85 @@ impl QrVerification { Self::decode(image) } + /// Decode and parse an grey scale image of a QR code into a + /// `QrVerification` + /// + /// # Arguments + /// + /// * `image` - The grey scale image containing the QR code. + /// + /// # Example + /// ```no_run + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// use image; + /// + /// let image = image::open("/path/to/my/image.png").unwrap(); + /// let image = image.to_luma8(); + /// let result = QrVerification::from_luma(image)?; + /// # Ok(()) + /// # } + /// ``` #[cfg(feature = "decode_image")] #[cfg_attr(feature = "docs", doc(cfg(decode_image)))] pub fn from_luma(image: ImageBuffer, Vec>) -> Result { Self::decode(image) } + /// Parse the decoded payload of a QR code in byte slice form as a + /// `QrVerification` + /// + /// This method is useful if you would like to do your own custom QR code + /// decoding. + /// + /// # Arguments + /// + /// * `bytes` - The raw bytes of a decoded QR code. + /// + /// # Example + /// ``` + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// let data = b"MATRIX\ + /// \x02\x02\x00\x07\ + /// FLOW_ID\ + /// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + /// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + /// SHARED_SECRET"; + /// + /// let result = QrVerification::from_bytes(data)?; + /// # Ok(()) + /// # } + /// ``` pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result { Self::decode_bytes(bytes) } + /// Encode the `QrVerification` into a `QrCode`. + /// + /// This method turns the `QrVerification` into a QR code that can be + /// rendered and presented to be scanned. + /// + /// The encoding can fail if the data doesn't fit into a QR code or if the + /// identity keys that should be encoded into the QR code are not valid + /// base64. + /// + /// # Example + /// ``` + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// let data = b"MATRIX\ + /// \x02\x02\x00\x07\ + /// FLOW_ID\ + /// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + /// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + /// SHARED_SECRET"; + /// + /// let result = QrVerification::from_bytes(data)?; + /// let encoded = result.to_qr_code().unwrap(); + /// # Ok(()) + /// # } + /// ``` pub fn to_qr_code(&self) -> Result { match self { QrVerification::Verification(v) => v.to_qr_code(), @@ -100,6 +195,30 @@ impl QrVerification { } } + /// Encode the `QrVerification` into a vector of bytes that can be encoded + /// as a QR code. + /// + /// The encoding can fail if the identity keys that should be encoded are + /// not valid base64. + /// + /// # Example + /// ``` + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// let data = b"MATRIX\ + /// \x02\x02\x00\x07\ + /// FLOW_ID\ + /// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + /// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + /// SHARED_SECRET"; + /// + /// let result = QrVerification::from_bytes(data)?; + /// let encoded = result.to_bytes().unwrap(); + /// + /// assert_eq!(data.as_ref(), encoded.as_slice()); + /// # Ok(()) + /// # } + /// ``` pub fn to_bytes(&self) -> Result, EncodingError> { match self { QrVerification::Verification(v) => v.to_bytes(), @@ -108,6 +227,34 @@ impl QrVerification { } } + /// Decode the byte slice containing the decoded QR code data. + /// + /// The format is defined in the [spec]. + /// + /// The byte slice consists of the following parts: + /// + /// * the ASCII string MATRIX + /// * one byte indicating the QR code version (must be 0x02) + /// * one byte indicating the QR code verification mode. one of the + /// following + /// values: + /// * 0x00 verifying another user with cross-signing + /// * 0x01 self-verifying in which the current device does trust the + /// master key + /// * 0x02 self-verifying in which the current device does not yet trust + /// the master key + /// * the event ID or transaction_id of the associated verification request + /// event, encoded as: + /// * two bytes in network byte order (big-endian) indicating the length + /// in bytes of the ID as a UTF-8 string + /// * the ID as a UTF-8 string + /// * the first key, as 32 bytes + /// * the second key, as 32 bytes + /// * a random shared secret, as a byte string. as we do not share the + /// length of the secret, and it is not a fixed size, clients will just + /// use the remainder of binary string as the shared secret. + /// + /// [spec]: https://spec.matrix.org/unstable/client-server-api/#qr-code-format fn decode_bytes(bytes: impl AsRef<[u8]>) -> Result { let mut decoded = Cursor::new(bytes); @@ -145,8 +292,9 @@ impl QrVerification { QrVerification::new(mode, flow_id, first_key, second_key, shared_secret) } + /// Decode the given image of an QR code and if we find a valid code, try to + /// decode it as a `QrVerification`. #[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) @@ -181,6 +329,10 @@ impl QrVerification { } } +/// The non-encoded data for the first mode of QR code verification. +/// +/// This mode is used for verification between two users using their master +/// cross signing keys. #[derive(Clone, Debug, PartialEq)] pub struct VerificationData { event_id: EventId, @@ -192,6 +344,55 @@ pub struct VerificationData { impl VerificationData { const QR_MODE: u8 = 0x00; + /// Create a new `VerificationData` struct that can be encoded as a QR code. + /// + /// # Arguments + /// * `event_id` - The event id of the `m.key.verification.request` event + /// that initiated the verification flow this QR code should be part of. + /// + /// * `first_key` - Our own cross signing master key. Needs to be encoded as + /// unpadded base64 + /// + /// * `second_key` - The cross signing master key of the other user. + /// + /// * ` shared_secret` - A random bytestring encoded as unpadded base64, + /// needs to be at least 8 bytes long. + 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 } + } + + /// Encode the `VerificationData` into a vector of bytes that can be + /// encoded as a QR code. + /// + /// The encoding can fail if the master keys that should be encoded are not + /// valid base64. + /// + /// # Example + /// ``` + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// let data = b"MATRIX\ + /// \x02\x00\x00\x0f\ + /// $test:localhost\ + /// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + /// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + /// SHARED_SECRET"; + /// + /// let result = QrVerification::from_bytes(data)?; + /// if let QrVerification::Verification(decoded) = result { + /// let encoded = decoded.to_bytes().unwrap(); + /// assert_eq!(data.as_ref(), encoded.as_slice()); + /// } else { + /// panic!("Data was encoded as an incorrect mode"); + /// } + /// # Ok(()) + /// # } + /// ``` pub fn to_bytes(&self) -> Result, EncodingError> { to_bytes( Self::QR_MODE, @@ -202,6 +403,13 @@ impl VerificationData { ) } + /// Encode the `VerificationData` into a `QrCode`. + /// + /// This method turns the `VerificationData` into a QR code that can be + /// rendered and presented to be scanned. + /// + /// The encoding can fail if the data doesn't fit into a QR code or if the + /// keys that should be encoded into the QR code are not valid base64. pub fn to_qr_code(&self) -> Result { to_qr_code( Self::QR_MODE, @@ -211,15 +419,6 @@ impl VerificationData { &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 { @@ -228,6 +427,11 @@ impl From for QrVerification { } } +/// The non-encoded data for the second mode of QR code verification. +/// +/// This mode is used for verification between two devices of the same user +/// where this device, that is creating this QR code, is trusting or owning +/// the cross signing master key. #[derive(Clone, Debug, PartialEq)] pub struct SelfVerificationData { transaction_id: String, @@ -239,6 +443,59 @@ pub struct SelfVerificationData { impl SelfVerificationData { const QR_MODE: u8 = 0x01; + /// Create a new `SelfVerificationData` struct that can be encoded as a QR + /// code. + /// + /// # Arguments + /// * `transaction_id` - The transaction id of this verification flow, the + /// transaction id was sent by the `m.key.verification.request` event + /// that initiated the verification flow this QR code should be part of. + /// + /// * `master_key` - Our own cross signing master key. Needs to be encoded + /// as + /// unpadded base64 + /// + /// * `device_key` - The ed25519 key of the other device, encoded as + /// unpadded base64. + /// + /// * ` shared_secret` - A random bytestring encoded as unpadded base64, + /// needs to be at least 8 bytes long. + 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 } + } + + /// Encode the `SelfVerificationData` into a vector of bytes that can be + /// encoded as a QR code. + /// + /// The encoding can fail if the keys that should be encoded are not valid + /// base64. + /// + /// # Example + /// ``` + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// let data = b"MATRIX\ + /// \x02\x01\x00\x06\ + /// FLOWID\ + /// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + /// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + /// SHARED_SECRET"; + /// + /// let result = QrVerification::from_bytes(data)?; + /// if let QrVerification::SelfVerification(decoded) = result { + /// let encoded = decoded.to_bytes().unwrap(); + /// assert_eq!(data.as_ref(), encoded.as_slice()); + /// } else { + /// panic!("Data was encoded as an incorrect mode"); + /// } + /// # Ok(()) + /// # } + /// ``` pub fn to_bytes(&self) -> Result, EncodingError> { to_bytes( Self::QR_MODE, @@ -249,6 +506,13 @@ impl SelfVerificationData { ) } + /// Encode the `SelfVerificationData` into a `QrCode`. + /// + /// This method turns the `SelfVerificationData` into a QR code that can be + /// rendered and presented to be scanned. + /// + /// The encoding can fail if the data doesn't fit into a QR code or if the + /// keys that should be encoded into the QR code are not valid base64. pub fn to_qr_code(&self) -> Result { to_qr_code( Self::QR_MODE, @@ -258,15 +522,6 @@ impl SelfVerificationData { &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 { @@ -275,6 +530,11 @@ impl From for QrVerification { } } +/// The non-encoded data for the third mode of QR code verification. +/// +/// This mode is used for verification between two devices of the same user +/// where this device, that is creating this QR code, is not trusting the +/// cross signing master key. #[derive(Clone, Debug, PartialEq)] pub struct SelfVerificationNoMasterKey { transaction_id: String, @@ -286,6 +546,59 @@ pub struct SelfVerificationNoMasterKey { impl SelfVerificationNoMasterKey { const QR_MODE: u8 = 0x02; + /// Create a new `SelfVerificationData` struct that can be encoded as a QR + /// code. + /// + /// # Arguments + /// * `transaction_id` - The transaction id of this verification flow, the + /// transaction id was sent by the `m.key.verification.request` event + /// that initiated the verification flow this QR code should be part of. + /// + /// * `device_key` - The ed25519 key of our own device, encoded as unpadded + /// base64. + /// + /// * `master_key` - Our own cross signing master key. Needs to be encoded + /// as + /// unpadded base64 + /// + /// * ` shared_secret` - A random bytestring encoded as unpadded base64, + /// needs to be at least 8 bytes long. + 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 } + } + + /// Encode the `SelfVerificationNoMasterKey` into a vector of bytes that can + /// be encoded as a QR code. + /// + /// The encoding can fail if the keys that should be encoded are not valid + /// base64. + /// + /// # Example + /// ``` + /// # use matrix_qrcode::{QrVerification, DecodingError}; + /// # fn main() -> Result<(), DecodingError> { + /// let data = b"MATRIX\ + /// \x02\x02\x00\x06\ + /// FLOWID\ + /// AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ + /// BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\ + /// SHARED_SECRET"; + /// + /// let result = QrVerification::from_bytes(data)?; + /// if let QrVerification::SelfVerificationNoMasterKey(decoded) = result { + /// let encoded = decoded.to_bytes().unwrap(); + /// assert_eq!(data.as_ref(), encoded.as_slice()); + /// } else { + /// panic!("Data was encoded as an incorrect mode"); + /// } + /// # Ok(()) + /// # } + /// ``` pub fn to_bytes(&self) -> Result, EncodingError> { to_bytes( Self::QR_MODE, @@ -296,6 +609,13 @@ impl SelfVerificationNoMasterKey { ) } + /// Encode the `SelfVerificationNoMasterKey` into a `QrCode`. + /// + /// This method turns the `SelfVerificationNoMasterKey` into a QR code that + /// can be rendered and presented to be scanned. + /// + /// The encoding can fail if the data doesn't fit into a QR code or if the + /// keys that should be encoded into the QR code are not valid base64. pub fn to_qr_code(&self) -> Result { to_qr_code( Self::QR_MODE, @@ -305,15 +625,6 @@ impl SelfVerificationNoMasterKey { &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 { From c174c4fda285698259fdbe2b639bc9f3ccfafd77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 24 May 2021 16:37:27 +0200 Subject: [PATCH 5/7] matrix-qrcode: Rename the crate. --- matrix_qrcode/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix_qrcode/Cargo.toml b/matrix_qrcode/Cargo.toml index f3d26a12..28edd7d6 100644 --- a/matrix_qrcode/Cargo.toml +++ b/matrix_qrcode/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "matrix_qrcode" +name = "matrix-qrcode" version = "0.1.0" authors = ["Damir Jelić "] edition = "2018" From 37c23f1761ed5c99337da4369dbdb3ca51e6c2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 25 May 2021 13:31:12 +0200 Subject: [PATCH 6/7] matrix-qrcode: Add accessors for our keys/secrets. --- matrix_qrcode/src/types.rs | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/matrix_qrcode/src/types.rs b/matrix_qrcode/src/types.rs index d31161a4..76c9804f 100644 --- a/matrix_qrcode/src/types.rs +++ b/matrix_qrcode/src/types.rs @@ -327,6 +327,44 @@ impl QrVerification { m => Err(DecodingError::Mode(m)), } } + + /// Get the flow id for this `QrVerification`. + /// + /// This represents the ID as a string even if it is a `EventId`. + pub fn flow_id(&self) -> &str { + match self { + QrVerification::Verification(v) => &v.event_id.as_str(), + QrVerification::SelfVerification(v) => &v.transaction_id, + QrVerification::SelfVerificationNoMasterKey(v) => &v.transaction_id, + } + } + + /// Get the first key of this `QrVerification`. + pub fn first_key(&self) -> &str { + match self { + QrVerification::Verification(v) => &v.first_master_key, + QrVerification::SelfVerification(v) => &v.master_key, + QrVerification::SelfVerificationNoMasterKey(v) => &v.device_key, + } + } + + /// Get the second key of this `QrVerification`. + pub fn second_key(&self) -> &str { + match self { + QrVerification::Verification(v) => &v.second_master_key, + QrVerification::SelfVerification(v) => &v.device_key, + QrVerification::SelfVerificationNoMasterKey(v) => &v.master_key, + } + } + + /// Get the secret of this `QrVerification`. + pub fn secret(&self) -> &str { + match self { + QrVerification::Verification(v) => &v.shared_secret, + QrVerification::SelfVerification(v) => &v.shared_secret, + QrVerification::SelfVerificationNoMasterKey(v) => &v.shared_secret, + } + } } /// The non-encoded data for the first mode of QR code verification. From 63dc939081c3a892bda0be905c7c954324c63c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 27 May 2021 11:24:44 +0200 Subject: [PATCH 7/7] matrix-qrcode: Modify the QR code generation so mobile clients can decode --- matrix_qrcode/src/utils.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/matrix_qrcode/src/utils.rs b/matrix_qrcode/src/utils.rs index 6a30ff76..faff2f71 100644 --- a/matrix_qrcode/src/utils.rs +++ b/matrix_qrcode/src/utils.rs @@ -17,7 +17,7 @@ use std::convert::TryInto; use base64::{decode_config, encode_config, STANDARD_NO_PAD}; #[cfg(feature = "decode_image")] use image::{ImageBuffer, Luma}; -use qrcode::QrCode; +use qrcode::{bits::Bits, EcLevel, QrCode, Version}; #[cfg(feature = "decode_image")] use crate::error::DecodingError; @@ -73,7 +73,21 @@ pub(crate) fn to_qr_code( shared_secret: &str, ) -> Result { let data = to_bytes(mode, flow_id, first_key, second_key, shared_secret)?; - Ok(QrCode::new(data)?) + + // Mobile clients seem to have trouble decoding the QR code that gets + // generated by `QrCode::new()` it seems to add a couple of data segments + // with different data modes/types. The parsers seem to assume a single + // data type and since we start with an ASCII `MATRIX` header the rest of + // the data gets treated as a string as well. + // + // We make sure that there isn't an ECI bit set and we just push the bytes, + // this seems to help since the decoder doesn't assume an encoding and + // treats everything as raw bytes. + let mut bits = Bits::new(Version::Normal(7)); + bits.push_byte_data(&data)?; + bits.push_terminator(EcLevel::L)?; + + Ok(QrCode::with_bits(bits, EcLevel::L)?) } #[cfg(feature = "decode_image")]