From 22e812655c944af4c62131e6eca197f2ab2497ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 5 Dec 2025 13:09:14 +0100 Subject: [PATCH 01/29] chore: Bump vodozemac --- Cargo.lock | 788 ++++++++++++++---- Cargo.toml | 4 +- bindings/matrix-sdk-ffi/src/qr_code.rs | 4 +- crates/matrix-sdk-crypto/Cargo.toml | 2 +- crates/matrix-sdk-qrcode/Cargo.toml | 2 +- .../src/authentication/oauth/qrcode/grant.rs | 21 +- .../src/authentication/oauth/qrcode/login.rs | 6 +- .../oauth/qrcode/secure_channel/mod.rs | 7 +- 8 files changed, 628 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08900fbd978..c920fba393b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,7 +11,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -35,10 +35,20 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "generic-array", ] +[[package]] +name = "aead" +version = "0.6.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" +dependencies = [ + "crypto-common 0.2.1", + "inout 0.2.2", +] + [[package]] name = "aes" version = "0.8.4" @@ -46,8 +56,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", - "cpufeatures 0.2.12", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +dependencies = [ + "cipher 0.5.1", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" +dependencies = [ + "aead 0.6.0-rc.10", + "aes 0.9.0", + "cipher 0.5.1", + "ctr 0.10.0-rc.4", + "ghash", + "subtle", ] [[package]] @@ -130,7 +165,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -193,7 +228,7 @@ dependencies = [ "rustc-hash", "serde", "serde_derive", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -308,7 +343,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -319,7 +354,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -445,6 +480,12 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.22.1" @@ -534,13 +575,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" -version = "0.3.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" dependencies = [ - "generic-array", + "hybrid-array", ] [[package]] @@ -565,7 +615,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -668,11 +718,11 @@ dependencies = [ [[package]] name = "cbc" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" dependencies = [ - "cipher", + "cipher 0.5.1", ] [[package]] @@ -718,8 +768,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", - "cpufeatures 0.2.12", + "cipher 0.4.4", + "cpufeatures 0.2.17", ] [[package]] @@ -729,6 +779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", + "cipher 0.5.1", "cpufeatures 0.3.0", "rand_core 0.10.0", ] @@ -739,13 +790,25 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ - "aead", + "aead 0.5.2", "chacha20 0.9.1", - "cipher", - "poly1305", + "cipher 0.4.4", + "poly1305 0.8.0", "zeroize", ] +[[package]] +name = "chacha20poly1305" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c9ed179664f12fd6f155f6dd632edf5f3806d48c228c67ff78366f2a0eb6b5e" +dependencies = [ + "aead 0.6.0-rc.10", + "chacha20 0.10.0", + "cipher 0.5.1", + "poly1305 0.9.0-rc.6", +] + [[package]] name = "chrono" version = "0.4.42" @@ -793,11 +856,22 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.6", + "inout 0.1.3", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.1", + "inout 0.2.2", +] + [[package]] name = "clap" version = "4.5.53" @@ -828,7 +902,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -839,13 +913,19 @@ checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3" + [[package]] name = "codspeed" version = "4.2.1" @@ -855,7 +935,7 @@ dependencies = [ "anyhow", "cc", "colored", - "getrandom 0.2.15", + "getrandom 0.2.17", "glob", "libc", "nix", @@ -1006,6 +1086,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_panic" version = "0.2.15" @@ -1036,11 +1122,17 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpubits" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" + [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1138,6 +1230,21 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" +dependencies = [ + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.0", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1145,10 +1252,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "getrandom 0.4.2", + "hybrid-array", + "rand_core 0.10.0", +] + [[package]] name = "ctor" version = "0.2.9" @@ -1156,7 +1273,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1165,7 +1282,26 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "ctr" +version = "0.10.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee683dd898fbd052617b4514bc31f98bc32081a83b69ec46adef3b1ef4ae36f" +dependencies = [ + "cipher 0.5.1", +] + +[[package]] +name = "ctutils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4" +dependencies = [ + "cmov", + "subtle", ] [[package]] @@ -1175,10 +1311,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.12", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", - "fiat-crypto", + "digest 0.11.2", + "fiat-crypto 0.3.0", "rustc_version", "serde", "subtle", @@ -1193,7 +1345,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1227,7 +1379,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1241,7 +1393,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1252,7 +1404,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core 0.20.10", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1263,7 +1415,7 @@ checksum = "2b5be8a7a562d315a5b92a630c30cec6bcf663e6673f00fbb69cca66a6f521b9" dependencies = [ "darling_core 0.21.1", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1346,7 +1498,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1355,7 +1507,17 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ - "const-oid", + "const-oid 0.9.6", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", "zeroize", ] @@ -1398,7 +1560,7 @@ dependencies = [ "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1408,7 +1570,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1437,7 +1599,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1448,7 +1610,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", "unicode-xid", ] @@ -1464,11 +1626,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1498,7 +1672,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1520,8 +1694,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ "pkcs8", + "signature 2.2.0", +] + +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ "serde", - "signature", + "signature 3.0.0-rc.10", ] [[package]] @@ -1530,11 +1713,25 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ - "curve25519-dalek", - "ed25519", - "rand_core 0.6.4", + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" +dependencies = [ + "curve25519-dalek 5.0.0-pre.6", + "ed25519 3.0.0-rc.4", + "rand_core 0.10.0", "serde", - "sha2", + "sha2 0.11.0", "subtle", "zeroize", ] @@ -1545,6 +1742,26 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.14.0-rc.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda94f31325c4275e9706adecbb6f0650dee2f904c915a98e3d81adaaaa757aa" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.1", + "digest 0.11.2", + "hkdf 0.13.0", + "hybrid-array", + "rand_core 0.10.0", + "rustcrypto-ff", + "rustcrypto-group", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "emojis" version = "0.8.0" @@ -1787,7 +2004,7 @@ checksum = "dd65f1b59dd22d680c7a626cc4a000c1e03d241c51c3e034d2bc9f1e90734f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1861,7 +2078,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -1882,6 +2099,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.7" @@ -2012,7 +2235,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -2080,9 +2303,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2121,6 +2344,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + [[package]] name = "gimli" version = "0.32.3" @@ -2309,7 +2541,16 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", +] + +[[package]] +name = "hkdf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" +dependencies = [ + "hmac 0.13.0", ] [[package]] @@ -2318,7 +2559,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -2332,6 +2582,26 @@ dependencies = [ "windows-link 0.1.1", ] +[[package]] +name = "hpke" +version = "0.14.0-pre.2" +source = "git+https://github.com/rozbb/rust-hpke/?rev=d1c44674#d1c446740e1cb615be7e4bfff1573001c7ab2e4e" +dependencies = [ + "aead 0.6.0-rc.10", + "aes-gcm", + "chacha20poly1305 0.11.0-rc.3", + "getrandom 0.4.2", + "hkdf 0.13.0", + "hybrid-array", + "p256", + "rand_core 0.10.0", + "sha2 0.11.0", + "sha3", + "subtle", + "x25519-dalek", + "zeroize", +] + [[package]] name = "html5ever" version = "0.39.0" @@ -2403,6 +2673,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.7.0" @@ -2614,7 +2895,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -2694,7 +2975,7 @@ checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -2749,10 +3030,19 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "block-padding", "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + [[package]] name = "insta" version = "1.44.1" @@ -2777,7 +3067,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -2922,6 +3212,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "keccak" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", +] + [[package]] name = "konst" version = "0.4.3" @@ -3090,7 +3390,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3102,7 +3402,7 @@ dependencies = [ "proc-macro2", "quote", "sealed", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3115,7 +3415,7 @@ dependencies = [ "macroific_core", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3152,25 +3452,25 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "matrix-pickle" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2551de3bba2cc65b52dc6b268df6114011fe118ac24870fbcf1b35537bd721" +checksum = "6c34e6db65145740459f2ca56623b40cd4e6000ffae2a7d91515fa82aa935dbf" dependencies = [ "matrix-pickle-derive", - "thiserror 1.0.63", + "thiserror 2.0.18", ] [[package]] name = "matrix-pickle-derive" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75de44c3120d78e978adbcf6d453b20ba011f3c46363e52d1dbbc72f545e9fb" +checksum = "a962fc9981f823f6555416dcb2ae9ae67ca412d767ee21ecab5150113ee6285b" dependencies = [ "proc-macro-crate", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3235,7 +3535,7 @@ dependencies = [ "serde_html_form", "serde_json", "serde_urlencoded", - "sha2", + "sha2 0.10.9", "similar-asserts", "stream_assert", "tempfile", @@ -3335,7 +3635,7 @@ dependencies = [ name = "matrix-sdk-crypto" version = "0.16.0" dependencies = [ - "aes", + "aes 0.8.4", "anyhow", "aquamarine", "as_variant", @@ -3345,13 +3645,13 @@ dependencies = [ "bs58", "byteorder", "cfg-if", - "ctr", + "ctr 0.9.2", "eyeball", "futures-core", "futures-executor", "futures-util", - "hkdf", - "hmac", + "hkdf 0.12.4", + "hmac 0.12.1", "http", "insta", "itertools 0.14.0", @@ -3367,7 +3667,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "similar-asserts", "stream_assert", "subtle", @@ -3390,7 +3690,7 @@ dependencies = [ "anyhow", "assert_matches2", "futures-util", - "hmac", + "hmac 0.12.1", "http", "js_int", "matrix-sdk-common", @@ -3402,7 +3702,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tokio", @@ -3465,7 +3765,7 @@ version = "0.7.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3481,7 +3781,7 @@ dependencies = [ "gloo-timers", "gloo-utils", "growable-bloom-filter", - "hkdf", + "hkdf 0.12.4", "js-sys", "matrix-sdk-base", "matrix-sdk-common", @@ -3495,7 +3795,7 @@ dependencies = [ "serde", "serde-wasm-bindgen", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "tokio", "tracing", @@ -3555,16 +3855,16 @@ dependencies = [ name = "matrix-sdk-search" version = "0.16.0" dependencies = [ - "aes", + "aes 0.8.4", "byteorder", - "ctr", - "hkdf", - "hmac", + "ctr 0.9.2", + "hkdf 0.12.4", + "hmac 0.12.1", "matrix-sdk-test", "pbkdf2", "rand 0.10.1", "ruma", - "sha2", + "sha2 0.10.9", "tantivy", "tempfile", "thiserror 2.0.18", @@ -3612,16 +3912,16 @@ dependencies = [ "anyhow", "base64", "blake3", - "chacha20poly1305", - "getrandom 0.2.15", + "chacha20poly1305 0.10.1", + "getrandom 0.2.17", "getrandom 0.4.2", - "hmac", + "hmac 0.12.1", "pbkdf2", "rand 0.10.1", "rmp-serde", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", "zeroize", ] @@ -3640,7 +3940,7 @@ dependencies = [ "ruma", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tokio", "tracing-subscriber", "vodozemac", @@ -3653,7 +3953,7 @@ name = "matrix-sdk-test-macros" version = "0.16.0" dependencies = [ "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3749,7 +4049,7 @@ dependencies = [ "macroific", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -3962,13 +4262,13 @@ checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ "base64", "chrono", - "getrandom 0.2.15", + "getrandom 0.2.17", "http", "rand 0.8.5", "serde", "serde_json", "serde_path_to_error", - "sha2", + "sha2 0.10.9", "thiserror 1.0.63", "url", ] @@ -4054,6 +4354,17 @@ version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +[[package]] +name = "p256" +version = "0.14.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b97e3bf0465157ae90975ff52dbeb1362ba618924878c9f74c25baa27a65f9a" +dependencies = [ + "elliptic-curve", + "primefield", + "primeorder", +] + [[package]] name = "paranoid-android" version = "0.2.2" @@ -4109,7 +4420,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -4149,7 +4460,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -4160,7 +4471,7 @@ checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ "once_cell", "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4220,7 +4531,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", + "der 0.7.9", "spki", ] @@ -4270,9 +4581,30 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures 0.2.12", + "cpufeatures 0.2.17", "opaque-debug", - "universal-hash", + "universal-hash 0.5.1", +] + +[[package]] +name = "poly1305" +version = "0.9.0-rc.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19feddcbdf17fad33f40041c7f9e768faf19455f32a6d52ba1b8b65ffc7b1cae" +dependencies = [ + "cpufeatures 0.3.0", + "universal-hash 0.6.1", +] + +[[package]] +name = "polyval" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", ] [[package]] @@ -4300,14 +4632,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", - "syn 2.0.101", + "syn 2.0.117", +] + +[[package]] +name = "primefield" +version = "0.14.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.1", + "rand_core 0.10.0", + "rustcrypto-ff", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0-rc.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a" +dependencies = [ + "elliptic-curve", ] [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -4377,7 +4732,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -4473,9 +4828,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4550,7 +4905,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", ] [[package]] @@ -4667,7 +5022,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.2.17", "libredox", "thiserror 2.0.18", ] @@ -4751,7 +5106,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -4957,7 +5312,7 @@ dependencies = [ "quote", "ruma-identifiers-validation", "serde", - "syn 2.0.101", + "syn 2.0.117", "toml 1.1.2+spec-1.1.0", ] @@ -4968,12 +5323,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134d4b6b5b039d3f52b3a28516f348c718ab5f0785fed1328588636c9c67b54c" dependencies = [ "base64", - "ed25519-dalek", + "ed25519-dalek 2.1.1", "pkcs8", "rand 0.10.1", "ruma-common", "serde_json", - "sha2", + "sha2 0.10.9", "thiserror 2.0.18", ] @@ -5022,6 +5377,27 @@ dependencies = [ "semver", ] +[[package]] +name = "rustcrypto-ff" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" +dependencies = [ + "rand_core 0.10.0", + "subtle", +] + +[[package]] +name = "rustcrypto-group" +version = "0.14.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" +dependencies = [ + "rand_core 0.10.0", + "rustcrypto-ff", + "subtle", +] + [[package]] name = "rustix" version = "0.38.41" @@ -5183,7 +5559,7 @@ checksum = "7f81c2fde025af7e69b1d1420531c8a8811ca898919db177141a85313b1cb932" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -5194,7 +5570,21 @@ checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", +] + +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der 0.8.0", + "hybrid-array", + "subtle", + "zeroize", ] [[package]] @@ -5384,7 +5774,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -5402,15 +5792,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -5452,8 +5842,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.12", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -5463,8 +5853,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.12", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", +] + +[[package]] +name = "sha3" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" +dependencies = [ + "digest 0.11.2", + "keccak", ] [[package]] @@ -5521,6 +5932,12 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + [[package]] name = "simd-adler32" version = "0.3.7" @@ -5609,7 +6026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.9", ] [[package]] @@ -5701,7 +6118,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -5714,7 +6131,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -5736,9 +6153,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.101" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -5762,7 +6179,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -5971,7 +6388,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -5982,7 +6399,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -6095,7 +6512,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -6175,12 +6592,6 @@ dependencies = [ "winnow 1.0.1", ] -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" - [[package]] name = "toml_datetime" version = "0.7.2" @@ -6201,13 +6612,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", - "toml_datetime 0.6.8", - "winnow 0.6.20", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 0.7.13", ] [[package]] @@ -6305,7 +6717,7 @@ source = "git+https://github.com/tokio-rs/tracing.git?rev=20f5b3d8ba057ca9c4ae00 dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -6587,7 +6999,7 @@ dependencies = [ "indexmap", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -6602,7 +7014,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.101", + "syn 2.0.117", "toml 0.9.7", "uniffi_meta", ] @@ -6650,10 +7062,20 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -6773,27 +7195,28 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vodozemac" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98bf83c0992966775b8012f194b07b44928996163e5a05b741b43891571ae5b" +source = "git+https://github.com/matrix-org/vodozemac/?rev=e5aa25d7#e5aa25d794f3f5a9eec8d6b83485ad09fb6696b2" dependencies = [ - "aes", + "aes 0.9.0", "arrayvec", "base64", "base64ct", "cbc", - "chacha20poly1305", - "curve25519-dalek", - "ed25519-dalek", - "getrandom 0.2.15", - "hkdf", - "hmac", + "chacha20poly1305 0.11.0-rc.3", + "cipher 0.5.1", + "curve25519-dalek 5.0.0-pre.6", + "ed25519-dalek 3.0.0-pre.6", + "getrandom 0.4.2", + "hkdf 0.13.0", + "hmac 0.13.0", + "hpke", "matrix-pickle", "prost", - "rand 0.8.5", + "rand 0.10.1", "serde", "serde_bytes", "serde_json", - "sha2", + "sha2 0.11.0", "subtle", "thiserror 2.0.18", "x25519-dalek", @@ -6889,7 +7312,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -6932,7 +7355,7 @@ checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -7166,7 +7589,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -7177,7 +7600,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -7526,15 +7949,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "0.7.13" @@ -7603,7 +8017,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.101", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -7619,7 +8033,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -7675,12 +8089,12 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "x25519-dalek" -version = "2.0.1" +version = "3.0.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "b3d5d6ff67acd3945b933e592bfa7143db4fcbb2f871754b6b9fbd7847fc5aea" dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", + "curve25519-dalek 5.0.0-pre.6", + "rand_core 0.10.0", "serde", "zeroize", ] @@ -7744,7 +8158,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", "synstructure", ] @@ -7765,7 +8179,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -7785,7 +8199,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", "synstructure", ] @@ -7806,7 +8220,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] [[package]] @@ -7828,9 +8242,15 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.101", + "syn 2.0.117", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index e5e022b2358..c6247e07ce8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ gloo-timers = { version = "0.3.0", default-features = false } gloo-utils = { version = "0.2.0", default-features = false, features = ["serde"] } growable-bloom-filter = { version = "2.1.1", default-features = false } hkdf = { version = "0.12.4", default-features = false } -hmac = { version = "0.12.1", default-features = false } +hmac = { version = "0.12.1", default-features = false, features = ["std"] } http = { version = "1.3.1", default-features = false } imbl = { version = "6.1.0", default-features = false } indexed_db_futures = { version = "0.7.0", package = "matrix_indexed_db_futures", default-features = false } @@ -119,7 +119,7 @@ uniffi_bindgen = { version = "0.31.0", default-features = false, features = ["ca url = { version = "2.5.7", default-features = false } uuid = { version = "1.18.1", default-features = false } vergen-gitcl = { version = "1.0.8", default-features = false } -vodozemac = { version = "0.10.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] } +vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "e5aa25d7", default-features = false, features = ["libolm-compat", "insecure-pk-encryption", "experimental-session-config"] } wasm-bindgen = { version = "0.2.105", default-features = false } wasm-bindgen-test = { version = "0.3.55", default-features = false, features = ["std"] } web-sys = { version = "0.3.82", default-features = false } diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 96ffc4cef10..b87555a51b2 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -531,7 +531,7 @@ impl From> for QrLoginProgress { match value { LoginProgress::Starting => Self::Starting, LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let check_code = check_code.to_digit(); + let check_code = check_code.to_digit(DigitMode::AllowLeadingZero); Self::EstablishingSecureChannel { check_code, @@ -633,7 +633,7 @@ impl From> for GrantQrLoginProgress { match value { GrantLoginProgress::Starting => Self::Starting, GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let check_code = check_code.to_digit(); + let check_code = check_code.to_digit(DigitMode::AllowLeadingZero); Self::EstablishingSecureChannel { check_code, diff --git a/crates/matrix-sdk-crypto/Cargo.toml b/crates/matrix-sdk-crypto/Cargo.toml index 9a64930fdf9..2612de3de67 100644 --- a/crates/matrix-sdk-crypto/Cargo.toml +++ b/crates/matrix-sdk-crypto/Cargo.toml @@ -26,7 +26,7 @@ experimental-encrypted-state-events = [ "ruma/unstable-msc4362" ] -js = ["ruma/js", "vodozemac/js", "matrix-sdk-common/js"] +js = ["ruma/js", "vodozemac/wasm_js", "matrix-sdk-common/js"] qrcode = ["dep:matrix-sdk-qrcode"] experimental-algorithms = [] uniffi = ["dep:uniffi"] diff --git a/crates/matrix-sdk-qrcode/Cargo.toml b/crates/matrix-sdk-qrcode/Cargo.toml index 2742b295045..f17c823626e 100644 --- a/crates/matrix-sdk-qrcode/Cargo.toml +++ b/crates/matrix-sdk-qrcode/Cargo.toml @@ -16,7 +16,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"] [features] -js = ["vodozemac/js"] +js = ["vodozemac/wasm_js"] [dependencies] byteorder.workspace = true diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index b2aced751a2..bc973ac834d 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -404,6 +404,7 @@ mod test { use ruma::{owned_device_id, owned_user_id}; use tokio::sync::oneshot; use tracing::debug; + use vodozemac::hpke::DigitMode; use super::*; use crate::{ @@ -454,7 +455,7 @@ mod test { // Let Alice know about the checkcode so she can verify the channel. check_code_tx - .send(bob.check_code().to_digit()) + .send(bob.check_code().to_digit(DigitMode::AllowLeadingZero)) .expect("Bob should be able to send the checkcode"); match behaviour { @@ -1000,7 +1001,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -1129,7 +1130,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -1369,7 +1370,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); break; } @@ -1619,7 +1620,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); } _ => { @@ -1876,7 +1877,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -2266,7 +2267,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); break; } @@ -2531,7 +2532,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -2805,7 +2806,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -3045,7 +3046,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to forward the checkcode"); break; } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 7a118ec0ae2..d618607c534 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -495,7 +495,7 @@ mod test { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::async_test; use serde_json::json; - use vodozemac::ecies::CheckCode; + use vodozemac::{ecies::CheckCode, hpke::DigitMode}; use super::*; use crate::{ @@ -549,7 +549,7 @@ mod test { check_code_receiver.await.expect("We should receive the check code from bob"); let mut alice = alice - .confirm(check_code.to_digit()) + .confirm(check_code.to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to confirm the secure channel"); let message = alice @@ -688,7 +688,7 @@ mod test { // The other side isn't yet sure that it's talking to the right device, show // a check code so they can confirm. - let check_code = channel.check_code().to_digit(); + let check_code = channel.check_code().to_digit(DigitMode::AllowLeadingZero); let check_code_sender = cctx_receiver.await.expect("Alice should receive the CheckCodeSender"); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 29c0fa025f7..eabef669d1c 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -20,7 +20,7 @@ use serde::{Serialize, de::DeserializeOwned}; use tracing::{instrument, trace}; use url::Url; use vodozemac::ecies::{ - CheckCode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult, + CheckCode, DigitMode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult, }; use super::{ @@ -140,7 +140,7 @@ impl AlmostEstablishedSecureChannel { /// The check code needs to be received out of band from the other side of /// the secure channel. pub(super) fn confirm(self, check_code: u8) -> Result { - if check_code == self.secure_channel.check_code().to_digit() { + if check_code == self.secure_channel.check_code().to_digit(DigitMode::AllowLeadingZero) { Ok(self.secure_channel) } else { Err(Error::InvalidCheckCode) @@ -288,6 +288,7 @@ pub(super) mod test { use serde_json::json; use similar_asserts::assert_eq; use url::Url; + use vodozemac::hpke::DigitMode; use wiremock::{ Mock, MockGuard, MockServer, ResponseTemplate, matchers::{method, path}, @@ -465,7 +466,7 @@ pub(super) mod test { assert_eq!(alice.secure_channel.check_code(), bob.check_code()); let alice = alice - .confirm(bob.check_code().to_digit()) + .confirm(bob.check_code().to_digit(DigitMode::AllowLeadingZero)) .expect("Alice should be able to confirm the established secure channel."); assert_eq!(bob.channel.rendezvous_info(), alice.channel.rendezvous_info()); From 146fc6f40d0c1a18c80dea717b42e4eafe0208b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 5 Dec 2025 13:09:14 +0100 Subject: [PATCH 02/29] feat(qr-login): Support HPKE for the cryptographic channel --- .../qrcode/secure_channel/crypto_channel.rs | 32 ++++++++++- .../oauth/qrcode/secure_channel/mod.rs | 55 ++++++++++++++++--- 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs index ba8c32d2c3f..ebec5bed074 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs @@ -31,6 +31,7 @@ use vodozemac::{ Curve25519PublicKey, ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message}, + hpke::{self, EstablishedHpkeChannel, HpkeRecipientChannel, RecipientCreationResult}, }; use crate::authentication::oauth::qrcode::SecureChannelError as Error; @@ -38,6 +39,7 @@ use crate::authentication::oauth::qrcode::SecureChannelError as Error; /// A cryptographic communication channel. pub(super) enum CryptoChannel { Ecies(Ecies), + Hpke(HpkeRecipientChannel), } impl CryptoChannel { @@ -46,10 +48,16 @@ impl CryptoChannel { CryptoChannel::Ecies(Ecies::new()) } + /// Create a new HPKE-based [`CryptoChannel`]. + pub(super) fn new_hpke() -> Self { + CryptoChannel::Hpke(HpkeRecipientChannel::new()) + } + /// Get the [`Curve25519PublicKey`] of this cryptographic channel. pub(super) fn public_key(&self) -> Curve25519PublicKey { match self { CryptoChannel::Ecies(ecies) => ecies.public_key(), + CryptoChannel::Hpke(hpke) => hpke.public_key(), } } @@ -63,12 +71,19 @@ impl CryptoChannel { let message = InitialMessage::decode(message)?; Ok(CryptoChannelCreationResult::Ecies(ecies.establish_inbound_channel(&message)?)) } + CryptoChannel::Hpke(hpke) => { + let message = hpke::InitialMessage::decode(message).unwrap(); + Ok(CryptoChannelCreationResult::Hpke( + hpke.establish_channel(&message, &[]).unwrap(), + )) + } } } } pub(super) enum CryptoChannelCreationResult { Ecies(InboundCreationResult), + Hpke(RecipientCreationResult), } impl CryptoChannelCreationResult { @@ -78,6 +93,7 @@ impl CryptoChannelCreationResult { CryptoChannelCreationResult::Ecies(inbound_creation_result) => { &inbound_creation_result.message } + CryptoChannelCreationResult::Hpke(result) => &result.message, } } } @@ -88,6 +104,7 @@ impl CryptoChannelCreationResult { /// cryptographic messages. pub(super) enum EstablishedCryptoChannel { Ecies(EstablishedEcies), + Hpke(EstablishedHpkeChannel), } impl EstablishedCryptoChannel { @@ -95,26 +112,37 @@ impl EstablishedCryptoChannel { pub(super) fn check_code(&self) -> &CheckCode { match self { EstablishedCryptoChannel::Ecies(established_ecies) => established_ecies.check_code(), + EstablishedCryptoChannel::Hpke(established_hpke_channel) => { + established_hpke_channel.check_code() + } } } /// Seal the given plaintext using this [`EstablishedCryptoChannel`]. - pub(super) fn seal(&mut self, plaintext: &str) -> String { + pub(super) fn seal(&mut self, plaintext: &str, aad: &[u8]) -> String { match self { EstablishedCryptoChannel::Ecies(channel) => { let message = channel.encrypt(plaintext.as_bytes()); message.encode() } + EstablishedCryptoChannel::Hpke(channel) => { + let message = channel.seal(plaintext.as_bytes(), aad); + message.encode() + } } } /// Open the given sealed message using this [`EstablishedCryptoChannel`]. - pub(super) fn open(&mut self, message: &str) -> Result { + pub(super) fn open(&mut self, message: &str, aad: &[u8]) -> Result { let plaintext = match self { EstablishedCryptoChannel::Ecies(channel) => { let message = Message::decode(message)?; channel.decrypt(&message)? } + EstablishedCryptoChannel::Hpke(channel) => { + let message = hpke::Message::decode(message).unwrap(); + channel.open(&message, aad).unwrap() + } }; Ok(String::from_utf8(plaintext).map_err(|e| e.utf8_error())?) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index eabef669d1c..a700dccb6c9 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -19,8 +19,12 @@ use matrix_sdk_base::crypto::types::qr_login::{ use serde::{Serialize, de::DeserializeOwned}; use tracing::{instrument, trace}; use url::Url; -use vodozemac::ecies::{ - CheckCode, DigitMode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult, +use vodozemac::{ + ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult}, + hpke::{ + BidirectionalCreationResult, DigitMode, HpkeSenderChannel, InitialResponse, + RecipientCreationResult, SenderCreationResult, UnidirectionalSenderChannel, + }, }; use super::{ @@ -116,6 +120,15 @@ impl SecureChannel { secure_channel.send(LOGIN_OK_MESSAGE).await?; secure_channel } + CryptoChannelCreationResult::Hpke(RecipientCreationResult { channel, .. }) => { + let BidirectionalCreationResult { channel, message } = + channel.establish_bidirectional_channel(LOGIN_OK_MESSAGE.as_bytes(), &[]); + self.channel.send(message.encode()).await?; + + let crypto_channel = EstablishedCryptoChannel::Hpke(channel); + + EstablishedSecureChannel { channel: self.channel, crypto_channel } + } }; Ok(AlmostEstablishedSecureChannel { secure_channel }) @@ -163,6 +176,7 @@ impl EstablishedSecureChannel { ) -> Result { enum ChannelType { Ecies(EstablishedEcies), + Hpke(UnidirectionalSenderChannel), } if qr_code_data.intent() == expected_mode { @@ -176,7 +190,7 @@ impl EstablishedSecureChannel { // it's talking to us, the device that scanned the QR code, until it // receives and successfully decrypts the initial message. We're here encrypting // the `LOGIN_INITIATE_MESSAGE`. - let (crypto_channel, encoded_message) = { + let (crypto_channel, encoded_message) = if true { let ecies = Ecies::new(); let OutboundCreationResult { ecies, message } = ecies.establish_outbound_channel( @@ -184,6 +198,15 @@ impl EstablishedSecureChannel { LOGIN_INITIATE_MESSAGE.as_bytes(), )?; (ChannelType::Ecies(ecies), message.encode()) + } else { + let SenderCreationResult { channel, message } = HpkeSenderChannel::new() + .establish_channel( + qr_code_data.public_key(), + LOGIN_INITIATE_MESSAGE.as_bytes(), + // TODO: Do we want to include some additional authenticated data here? + &[], + ); + (ChannelType::Hpke(channel), message.encode()) }; // The other side has crated a rendezvous channel, we're going to connect to it @@ -213,15 +236,32 @@ impl EstablishedSecureChannel { trace!("Waiting for the LOGIN OK message"); let (response, channel) = match crypto_channel { - ChannelType::Ecies(ecies) => { + ChannelType::Ecies(crypto_channel) => { // We can create our EstablishedSecureChannel struct now and use the // convenient helpers which transparently decrypt on receival. - let crypto_channel = EstablishedCryptoChannel::Ecies(ecies); + let crypto_channel = EstablishedCryptoChannel::Ecies(crypto_channel); let mut channel = Self { channel, crypto_channel }; let response = channel.receive().await?; (response, channel) } + ChannelType::Hpke(crypto_channel) => { + let response = channel.receive().await?; + let response = InitialResponse::decode(&response).unwrap(); + + let BidirectionalCreationResult { channel: crypto_channel, message } = + crypto_channel.establish_bidirectional_channel(&response, &[]).unwrap(); + let response = String::from_utf8(message).unwrap(); + let crypto_channel = EstablishedCryptoChannel::Hpke(crypto_channel); + + // We can create our EstablishedSecureChannel struct now and use the + // convenient helpers which transparently decrypt on receival. + let channel = Self { channel, crypto_channel }; + + // We can create our EstablishedSecureChannel struct now and use the + // convenient helpers which transparently decrypt on receival. + (response, channel) + } }; trace!("Received the LOGIN OK message, maybe."); @@ -260,14 +300,13 @@ impl EstablishedSecureChannel { } async fn send(&mut self, message: &str) -> Result<(), Error> { - let message = self.crypto_channel.seal(message); - + let message = self.crypto_channel.seal(message, &[]); Ok(self.channel.send(message).await?) } async fn receive(&mut self) -> Result { let message = self.channel.receive().await?; - self.crypto_channel.open(&message) + self.crypto_channel.open(&message, &[]) } } From e38c3ff4b5ec0ca75deba4a5547b6aa43e7301b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 16 Jan 2026 10:50:43 +0100 Subject: [PATCH 03/29] feat(qr-login): Properly handle HPKE errors in the secure channel --- bindings/matrix-sdk-ffi/src/qr_code.rs | 4 +-- .../src/authentication/oauth/qrcode/mod.rs | 30 +++++++++++++++++-- .../qrcode/secure_channel/crypto_channel.rs | 23 ++++++++------ .../oauth/qrcode/secure_channel/mod.rs | 25 +++++++++++----- 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index b87555a51b2..be7fb3cb9f3 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -369,7 +369,7 @@ impl From for HumanQrLoginError { HumanQrLoginError::UnsupportedQrCodeType } SecureChannelError::SecureChannelMessage { .. } - | SecureChannelError::Ecies(_) + | SecureChannelError::Decryption(_) | SecureChannelError::InvalidCheckCode | SecureChannelError::CannotReceiveCheckCode => { HumanQrLoginError::ConnectionInsecure @@ -474,7 +474,7 @@ impl From for HumanQrGrantLoginError { | SecureChannelError::RendezvousChannel(_) => Self::Unknown(e.to_string()), SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType, SecureChannelError::SecureChannelMessage { .. } - | SecureChannelError::Ecies(_) + | SecureChannelError::Decryption(_) | SecureChannelError::InvalidCheckCode | SecureChannelError::CannotReceiveCheckCode => Self::ConnectionInsecure, SecureChannelError::InvalidIntent => Self::OtherDeviceAlreadySignedIn, diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 7857eb22c69..6bf032f5885 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -38,7 +38,10 @@ use thiserror::Error; use tokio::sync::Mutex; use url::Url; use vodozemac::ecies::CheckCode; -pub use vodozemac::ecies::{Error as EciesError, MessageDecodeError}; +pub use vodozemac::{ + ecies::{Error as EciesError, MessageDecodeError as EciesMessageDecodeError}, + hpke::{Error as HpkeError, MessageDecodeError as HpkeMessageDecodeError}, +}; mod grant; mod login; @@ -253,6 +256,29 @@ impl DeviceAuthorizationOAuthError { } } +/// Error type which describes failures when messages which are received over +/// the secure channel fail to be decoded. +#[derive(Debug, Error)] +pub enum MessageDecodeError { + /// A received message has failed to be decoded. + #[error(transparent)] + Ecies(#[from] EciesMessageDecodeError), + /// A received message has failed to be decoded. + #[error(transparent)] + Hpke(#[from] HpkeMessageDecodeError), +} + +/// Error type for decryption failures of the secure channel. +#[derive(Debug, Error)] +pub enum DecryptionError { + /// A ECIES message failed to be decrypted. + #[error(transparent)] + Ecies(#[from] EciesError), + /// A HPKE message failed to be decrypted. + #[error(transparent)] + Hpke(#[from] HpkeError), +} + /// Error type for failures in when receiving or sending messages over the /// secure channel. #[derive(Debug, Error)] @@ -264,7 +290,7 @@ pub enum SecureChannelError { /// A message has failed to be decrypted. #[error(transparent)] - Ecies(#[from] EciesError), + Decryption(#[from] DecryptionError), /// A received message has failed to be decoded. #[error(transparent)] diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs index ebec5bed074..e4b8d23a697 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs @@ -34,7 +34,9 @@ use vodozemac::{ hpke::{self, EstablishedHpkeChannel, HpkeRecipientChannel, RecipientCreationResult}, }; -use crate::authentication::oauth::qrcode::SecureChannelError as Error; +use crate::authentication::oauth::qrcode::{ + DecryptionError, MessageDecodeError, SecureChannelError as Error, +}; /// A cryptographic communication channel. pub(super) enum CryptoChannel { @@ -68,13 +70,16 @@ impl CryptoChannel { ) -> Result { match self { CryptoChannel::Ecies(ecies) => { - let message = InitialMessage::decode(message)?; - Ok(CryptoChannelCreationResult::Ecies(ecies.establish_inbound_channel(&message)?)) + let message = InitialMessage::decode(message).map_err(MessageDecodeError::from)?; + Ok(CryptoChannelCreationResult::Ecies( + ecies.establish_inbound_channel(&message).map_err(DecryptionError::from)?, + )) } CryptoChannel::Hpke(hpke) => { - let message = hpke::InitialMessage::decode(message).unwrap(); + let message = + hpke::InitialMessage::decode(message).map_err(MessageDecodeError::from)?; Ok(CryptoChannelCreationResult::Hpke( - hpke.establish_channel(&message, &[]).unwrap(), + hpke.establish_channel(&message, &[]).map_err(DecryptionError::from)?, )) } } @@ -136,12 +141,12 @@ impl EstablishedCryptoChannel { pub(super) fn open(&mut self, message: &str, aad: &[u8]) -> Result { let plaintext = match self { EstablishedCryptoChannel::Ecies(channel) => { - let message = Message::decode(message)?; - channel.decrypt(&message)? + let message = Message::decode(message).map_err(MessageDecodeError::from)?; + channel.decrypt(&message).map_err(DecryptionError::from)? } EstablishedCryptoChannel::Hpke(channel) => { - let message = hpke::Message::decode(message).unwrap(); - channel.open(&message, aad).unwrap() + let message = hpke::Message::decode(message).map_err(MessageDecodeError::from)?; + channel.open(&message, aad).map_err(DecryptionError::from)? } }; diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index a700dccb6c9..b3fe908d1ac 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -31,7 +31,11 @@ use super::{ SecureChannelError as Error, rendezvous_channel::{InboundChannelCreationResult, RendezvousChannel, RendezvousInfo}, }; -use crate::{config::RequestConfig, http_client::HttpClient}; +use crate::{ + authentication::oauth::qrcode::{DecryptionError, MessageDecodeError}, + config::RequestConfig, + http_client::HttpClient, +}; mod crypto_channel; const LOGIN_INITIATE_MESSAGE: &str = "MATRIX_QR_CODE_LOGIN_INITIATE"; @@ -193,10 +197,12 @@ impl EstablishedSecureChannel { let (crypto_channel, encoded_message) = if true { let ecies = Ecies::new(); - let OutboundCreationResult { ecies, message } = ecies.establish_outbound_channel( - qr_code_data.public_key(), - LOGIN_INITIATE_MESSAGE.as_bytes(), - )?; + let OutboundCreationResult { ecies, message } = ecies + .establish_outbound_channel( + qr_code_data.public_key(), + LOGIN_INITIATE_MESSAGE.as_bytes(), + ) + .map_err(DecryptionError::from)?; (ChannelType::Ecies(ecies), message.encode()) } else { let SenderCreationResult { channel, message } = HpkeSenderChannel::new() @@ -247,11 +253,14 @@ impl EstablishedSecureChannel { } ChannelType::Hpke(crypto_channel) => { let response = channel.receive().await?; - let response = InitialResponse::decode(&response).unwrap(); + let response = + InitialResponse::decode(&response).map_err(MessageDecodeError::from)?; let BidirectionalCreationResult { channel: crypto_channel, message } = - crypto_channel.establish_bidirectional_channel(&response, &[]).unwrap(); - let response = String::from_utf8(message).unwrap(); + crypto_channel + .establish_bidirectional_channel(&response, &[]) + .map_err(DecryptionError::from)?; + let response = String::from_utf8(message).map_err(|e| e.utf8_error())?; let crypto_channel = EstablishedCryptoChannel::Hpke(crypto_channel); // We can create our EstablishedSecureChannel struct now and use the From 901597ee98c8e04922322ed68e1586cfa2dc3148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 16 Jan 2026 11:16:22 +0100 Subject: [PATCH 04/29] refactor(qr-login): Move some more error variants into the MessageDecodeError --- bindings/matrix-sdk-ffi/src/qr_code.rs | 14 ++++++-------- .../src/authentication/oauth/qrcode/grant.rs | 10 +++++++--- .../src/authentication/oauth/qrcode/mod.rs | 16 +++++++--------- .../oauth/qrcode/rendezvous_channel/mod.rs | 6 ++++-- .../qrcode/secure_channel/crypto_channel.rs | 2 +- .../oauth/qrcode/secure_channel/mod.rs | 9 +++++---- 6 files changed, 30 insertions(+), 27 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index be7fb3cb9f3..6598ea3ce5c 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -361,10 +361,9 @@ impl From for HumanQrLoginError { } QRCodeLoginError::SecureChannel(e) => match e { - SecureChannelError::Utf8(_) - | SecureChannelError::MessageDecode(_) - | SecureChannelError::Json(_) - | SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown, + SecureChannelError::MessageDecode(_) | SecureChannelError::RendezvousChannel(_) => { + HumanQrLoginError::Unknown + } SecureChannelError::UnsupportedQrCodeType => { HumanQrLoginError::UnsupportedQrCodeType } @@ -468,10 +467,9 @@ impl From for HumanQrGrantLoginError { } QRCodeGrantLoginError::NotFound => Self::NotFound, QRCodeGrantLoginError::SecureChannel(e) => match e { - SecureChannelError::Utf8(_) - | SecureChannelError::MessageDecode(_) - | SecureChannelError::Json(_) - | SecureChannelError::RendezvousChannel(_) => Self::Unknown(e.to_string()), + SecureChannelError::MessageDecode(_) | SecureChannelError::RendezvousChannel(_) => { + Self::Unknown(e.to_string()) + } SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType, SecureChannelError::SecureChannelMessage { .. } | SecureChannelError::Decryption(_) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index bc973ac834d..08db08448d9 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -409,7 +409,7 @@ mod test { use super::*; use crate::{ authentication::oauth::qrcode::{ - LoginFailureReason, QrAuthMessage, + LoginFailureReason, MessageDecodeError, QrAuthMessage, messages::{AuthorizationGrant, LoginProtocolType}, secure_channel::{EstablishedSecureChannel, test::MockedRendezvousServer}, }, @@ -2969,7 +2969,9 @@ mod test { // Wait for all tasks to finish / fail. assert_matches!( grant.await, - Err(QRCodeGrantLoginError::SecureChannel(SecureChannelError::Json(_))), + Err(QRCodeGrantLoginError::SecureChannel(SecureChannelError::MessageDecode( + MessageDecodeError::Json(_) + ))), "Alice should abort the login with a SecureChannel error" ); updates_task.await.expect("Alice should run through all progress states"); @@ -3077,7 +3079,9 @@ mod test { // Wait for all tasks to finish / fail. assert_matches!( grant.await, - Err(QRCodeGrantLoginError::SecureChannel(SecureChannelError::Json(_))), + Err(QRCodeGrantLoginError::SecureChannel(SecureChannelError::MessageDecode( + MessageDecodeError::Json(_) + ))), "Alice should abort the login with a SecureChannel error" ); updates_task.await.expect("Alice should run through all progress states"); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 6bf032f5885..87a04af8168 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -266,6 +266,13 @@ pub enum MessageDecodeError { /// A received message has failed to be decoded. #[error(transparent)] Hpke(#[from] HpkeMessageDecodeError), + /// A message we received over the secure channel was not a valid UTF-8 + /// encoded string. + #[error(transparent)] + Utf8(#[from] std::str::Utf8Error), + /// A message couldn't be deserialized from JSON. + #[error(transparent)] + Json(#[from] serde_json::Error), } /// Error type for decryption failures of the secure channel. @@ -283,11 +290,6 @@ pub enum DecryptionError { /// secure channel. #[derive(Debug, Error)] pub enum SecureChannelError { - /// A message we received over the secure channel was not a valid UTF-8 - /// encoded string. - #[error(transparent)] - Utf8(#[from] std::str::Utf8Error), - /// A message has failed to be decrypted. #[error(transparent)] Decryption(#[from] DecryptionError), @@ -296,10 +298,6 @@ pub enum SecureChannelError { #[error(transparent)] MessageDecode(#[from] MessageDecodeError), - /// A message couldn't be deserialized from JSON. - #[error(transparent)] - Json(#[from] serde_json::Error), - /// The secure channel failed to be established because it received an /// unexpected message. #[error( diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs index a2275c487e2..ed805fac3df 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs @@ -16,7 +16,9 @@ use tracing::instrument; use url::Url; use crate::{ - HttpError, authentication::oauth::qrcode::SecureChannelError, http_client::HttpClient, + HttpError, + authentication::oauth::qrcode::{MessageDecodeError, SecureChannelError}, + http_client::HttpClient, }; mod msc_4108; @@ -103,6 +105,6 @@ impl RendezvousChannel { RendezvousChannel::Msc4108(channel) => channel.receive().await?, }; - Ok(String::from_utf8(message).map_err(|e| e.utf8_error())?) + Ok(String::from_utf8(message).map_err(|e| MessageDecodeError::from(e.utf8_error()))?) } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs index e4b8d23a697..01ed8a6afff 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs @@ -150,6 +150,6 @@ impl EstablishedCryptoChannel { } }; - Ok(String::from_utf8(plaintext).map_err(|e| e.utf8_error())?) + Ok(String::from_utf8(plaintext).map_err(|e| MessageDecodeError::from(e.utf8_error()))?) } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index b3fe908d1ac..c84924157d0 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -107,7 +107,7 @@ impl SecureChannel { let message = self.channel.receive().await?; let result = self.crypto_channel.establish_inbound_channel(&message)?; - let message = std::str::from_utf8(result.plaintext())?; + let message = std::str::from_utf8(result.plaintext()).map_err(MessageDecodeError::from)?; trace!("Received the initial secure channel message"); @@ -260,7 +260,8 @@ impl EstablishedSecureChannel { crypto_channel .establish_bidirectional_channel(&response, &[]) .map_err(DecryptionError::from)?; - let response = String::from_utf8(message).map_err(|e| e.utf8_error())?; + let response = String::from_utf8(message) + .map_err(|e| MessageDecodeError::from(e.utf8_error()))?; let crypto_channel = EstablishedCryptoChannel::Hpke(crypto_channel); // We can create our EstablishedSecureChannel struct now and use the @@ -295,7 +296,7 @@ impl EstablishedSecureChannel { /// The message will be encrypted before it is sent over the rendezvous /// channel. pub(super) async fn send_json(&mut self, message: impl Serialize) -> Result<(), Error> { - let message = serde_json::to_string(&message)?; + let message = serde_json::to_string(&message).map_err(MessageDecodeError::from)?; self.send(&message).await } @@ -305,7 +306,7 @@ impl EstablishedSecureChannel { /// rendezvous channel. pub(super) async fn receive_json(&mut self) -> Result { let message = self.receive().await?; - Ok(serde_json::from_str(&message)?) + Ok(serde_json::from_str(&message).map_err(MessageDecodeError::from)?) } async fn send(&mut self, message: &str) -> Result<(), Error> { From 84e708698c66e9bffa6704bd3199a3c9f8fe005a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 23 Jan 2026 15:17:01 +0100 Subject: [PATCH 05/29] MSC4388 support for the rendezvous channel --- Cargo.toml | 1 + .../oauth/qrcode/rendezvous_channel/mod.rs | 21 +- .../qrcode/rendezvous_channel/msc_4388.rs | 520 ++++++++++++++++++ .../oauth/qrcode/secure_channel/mod.rs | 6 + 4 files changed, 543 insertions(+), 5 deletions(-) create mode 100644 crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs diff --git a/Cargo.toml b/Cargo.toml index c6247e07ce8..1517ae4effb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ ruma = { version = "0.15.1", features = [ "unstable-msc4306", "unstable-msc4308", "unstable-msc4310", + "unstable-msc4388", ] } sentry = { version = "0.47.0", default-features = false } sentry-tracing = { version = "0.47.0", default-features = false } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs index ed805fac3df..356c1a2d9f6 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs @@ -22,6 +22,7 @@ use crate::{ }; mod msc_4108; +mod msc_4388; /// The result of the [`RendezvousChannel::create_inbound()`] method. pub(super) struct InboundChannelCreationResult { @@ -38,10 +39,13 @@ pub(super) struct InboundChannelCreationResult { #[derive(Debug, PartialEq, Eq)] pub(super) enum RendezvousInfo<'a> { Msc4108 { rendezvous_url: &'a Url }, + Msc4388 { rendezvous_id: &'a str }, } pub(super) enum RendezvousChannel { Msc4108(msc_4108::Channel), + #[allow(dead_code)] + Msc4388(msc_4388::Channel), } impl RendezvousChannel { @@ -78,6 +82,9 @@ impl RendezvousChannel { RendezvousChannel::Msc4108(channel) => { RendezvousInfo::Msc4108 { rendezvous_url: channel.rendezvous_url() } } + RendezvousChannel::Msc4388(channel) => { + RendezvousInfo::Msc4388 { rendezvous_id: &channel.rendezvous_id() } + } } } @@ -89,6 +96,7 @@ impl RendezvousChannel { pub(super) async fn send(&mut self, message: String) -> Result<(), HttpError> { match self { RendezvousChannel::Msc4108(channel) => channel.send(message.into_bytes()).await, + RendezvousChannel::Msc4388(channel) => channel.send(message).await, } } @@ -101,10 +109,13 @@ impl RendezvousChannel { /// This method will wait in a loop for the channel to give us a new /// message. pub(super) async fn receive(&mut self) -> Result { - let message = match self { - RendezvousChannel::Msc4108(channel) => channel.receive().await?, - }; - - Ok(String::from_utf8(message).map_err(|e| MessageDecodeError::from(e.utf8_error()))?) + match self { + RendezvousChannel::Msc4108(channel) => { + let message = channel.receive().await?; + Ok(String::from_utf8(message) + .map_err(|e| MessageDecodeError::from(e.utf8_error()))?) + } + RendezvousChannel::Msc4388(channel) => Ok(channel.receive().await?), + } } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs new file mode 100644 index 00000000000..403ac3e6b6b --- /dev/null +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs @@ -0,0 +1,520 @@ +// Copyright 2026 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::{borrow::Cow, time::Duration}; + +use http::StatusCode; +use matrix_sdk_base::sleep; +use ruma::api::{ + EndpointError as _, SupportedVersions, + client::rendezvous::{ + create_rendezvous_session, get_rendezvous_session, update_rendezvous_session, + }, + error::{FromHttpResponseError, IntoHttpError}, +}; +use tracing::{debug, instrument, trace}; +use url::Url; + +use crate::{HttpError, RumaApiError, http_client::HttpClient}; + +#[cfg(test)] +const POLL_TIMEOUT: Duration = Duration::from_millis(10); +#[cfg(not(test))] +const POLL_TIMEOUT: Duration = Duration::from_secs(1); + +/// The result of the [`RendezvousChannel::create_inbound()`] method. +pub(super) struct InboundChannelCreationResult { + /// The connected [`RendezvousChannel`]. + pub channel: Channel, + /// The initial message we received when we connected to the + /// [`RendezvousChannel`]. + /// + /// This is currently unused, but left in for completeness sake. + #[allow(dead_code)] + pub initial_message: String, +} + +struct RendezvousMessage { + pub status_code: StatusCode, + pub data: String, +} + +pub(crate) struct Channel { + client: HttpClient, + base_url: Url, + rendezvous_id: String, + sequence_token: String, +} + +fn response_to_error(status: StatusCode, data: String) -> HttpError { + match http::Response::builder().status(status).body(data).map_err(IntoHttpError::from) { + Ok(response) => { + let error = FromHttpResponseError::::Server(RumaApiError::ClientApi( + ruma::api::error::Error::from_http_response(response), + )); + + error.into() + } + Err(e) => HttpError::IntoHttp(e), + } +} + +impl Channel { + /// Create a new outbound [`RendezvousChannel`]. + /// + /// By outbound we mean that we're going to tell the Matrix server to create + /// a new rendezvous session. We're going to send an initial empty message + /// through the channel. + pub(super) async fn create_outbound( + client: HttpClient, + base_url: &Url, + ) -> Result { + use std::borrow::Cow; + + let request = create_rendezvous_session::unstable_msc4388::Request::new("".to_owned()); + + // TODO: Use the `Client::send()` method here? + let response = client + .send( + request, + None, + base_url.to_string(), + None, // TODO: should also support passing in access token + Cow::Owned(SupportedVersions { + versions: Default::default(), + features: Default::default(), + }), + Default::default(), + ) + .await?; + + let rendezvous_id = response.id; + let sequence_token = response.sequence_token; + + Ok(Self { client, base_url: base_url.to_owned(), rendezvous_id, sequence_token }) + } + + /// Create a new inbound [`RendezvousChannel`]. + /// + /// By inbound we mean that we're going to attempt to read an initial + /// message from the rendezvous session on the given [`rendezvous_url`]. + pub(super) async fn create_inbound( + client: HttpClient, + base_url: &Url, + rendezvous_id: &str, + ) -> Result { + // Receive the initial message, which should be empty. But we need the ETAG to + // fully establish the rendezvous channel. + let response = Self::receive_message_impl(&client, base_url, rendezvous_id).await?; + + let sequence_token = response.sequence_token.clone(); + + let initial_message = + RendezvousMessage { status_code: StatusCode::OK, data: response.data }; + + let channel = Self { + client, + base_url: base_url.to_owned(), + rendezvous_id: rendezvous_id.to_owned(), + sequence_token, + }; + + Ok(InboundChannelCreationResult { channel, initial_message: initial_message.data }) + } + + /// Get the ID of the rendezvous session we're using to exchange messages + /// through the channel. + pub(super) fn rendezvous_id(&self) -> &str { + &self.rendezvous_id + } + + /// Send the given `message` through the [`RendezvousChannel`] to the other + /// device. + #[instrument(skip_all)] + pub(super) async fn send(&mut self, message: String) -> Result<(), HttpError> { + let request = update_rendezvous_session::unstable::Request::new( + self.rendezvous_id.clone(), + self.sequence_token.clone(), + message, + ); + + let response = self + .client + .send( + request, + None, + self.base_url.to_string(), + None, + Cow::Owned(SupportedVersions { + versions: Default::default(), + features: Default::default(), + }), + Default::default(), + ) + .await?; + + debug!("Response for the rendezvous sending request {response:?}"); + + // We successfully send out a message, get the sequence_token and update our + // internal copy of the sequence_token. + self.sequence_token = response.sequence_token; + + Ok(()) + } + + /// Attempt to receive a message from the [`RendezvousChannel`] from the + /// other device. + /// + /// The content should be of the `text/plain` content type but the parsing + /// and verification of this fact is left up to the caller. + /// + /// This method will wait in a loop for the channel to give us a new + /// message. + pub(super) async fn receive(&mut self) -> Result { + loop { + let message = self.receive_single_message().await?; + + trace!( + status_code = %message.status_code, + "Received data from the rendezvous channel" + ); + // if sequence token is the same as our current one, it means no new message and + // map into not modified HTTP status + + if message.status_code == StatusCode::OK { + return Ok(message.data); + } else if message.status_code == StatusCode::NOT_MODIFIED { + sleep::sleep(POLL_TIMEOUT).await; + continue; + } else { + let error = response_to_error(message.status_code, message.data); + + return Err(error); + } + } + } + + async fn receive_message_impl( + client: &HttpClient, + base_url: &Url, + rendezvous_id: &str, + ) -> Result { + let request = get_rendezvous_session::unstable::Request::new(rendezvous_id.to_owned()); + client + .send( + request, + None, + base_url.to_string(), + None, + Cow::Owned(SupportedVersions { + versions: Default::default(), + features: Default::default(), + }), + Default::default(), + ) + .await + } + + async fn receive_single_message(&mut self) -> Result { + let response = + Self::receive_message_impl(&self.client, &self.base_url, &self.rendezvous_id).await?; + + // since the rendezvous API has changed it doesn't make sense to use the http + // status code at this layer + if response.sequence_token == self.sequence_token { + return Ok(RendezvousMessage { + status_code: StatusCode::NOT_MODIFIED, + data: response.data, + }); + } + + // We received a new sequence_token, put it into the copy of our sequence_token. + self.sequence_token = response.sequence_token; + + let message = RendezvousMessage { status_code: StatusCode::OK, data: response.data }; + + Ok(message) + } +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod test { + use matrix_sdk_test::async_test; + use serde_json::json; + use similar_asserts::assert_eq; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{method, path}, + }; + + use super::*; + use crate::config::RequestConfig; + + const BASE_PATH: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous"; + + async fn mock_rendezvous_create( + server: &MockServer, + rendezvous_id: &str, + ) -> Result { + server + .register(Mock::given(method("POST")).and(path(BASE_PATH)).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "id": rendezvous_id, + "sequence_token": "1", + "expires_in_ms": 10_000, + })), + )) + .await; + + Ok(format!("{BASE_PATH}/{rendezvous_id}")) + } + + #[async_test] + async fn test_creation() { + let server = MockServer::start().await; + let base_url = + Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"); + let rendezvous_id = "abcdEFG12345"; + + let rendezvous_path = mock_rendezvous_create(&server, rendezvous_id) + .await + .expect("We should be able to create a rendezvous"); + + let client = HttpClient::new(reqwest::Client::new(), RequestConfig::new().disable_retry()); + + let mut alice = Channel::create_outbound(client, &base_url) + .await + .expect("We should be able to create an outbound rendezvous channel"); + + assert_eq!( + alice.rendezvous_id(), + rendezvous_id, + "Alice should have configured the rendezvous ID correctly." + ); + + assert_eq!( + alice.sequence_token, "1", + "Alice should have remembered the sequence_token the server gave us." + ); + + { + let _scope = server + .register_as_scoped( + Mock::given(method("GET")).and(path(rendezvous_path.clone())).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "data": "", + "sequence_token": "1", + "expires_in_ms": 10_000, + })), + ), + ) + .await; + + let response = alice + .receive_single_message() + .await + .expect("Alice should be able to wait for data on the rendezvous channel."); + assert_eq!(response.status_code, StatusCode::NOT_MODIFIED); + } + + let mut bob = { + let _scope = server + .register_as_scoped( + Mock::given(method("GET")).and(path(rendezvous_path.clone())).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "data": "", + "sequence_token": "2", + "expires_in_ms": 10_000, + })), + ), + ) + .await; + + let client = HttpClient::new(reqwest::Client::new(), RequestConfig::short_retry()); + let InboundChannelCreationResult { channel: bob, initial_message: _ } = + Channel::create_inbound(client, &base_url, &rendezvous_id.to_owned()).await.expect( + "We should be able to create a rendezvous channel from a received message", + ); + + assert_eq!(alice.rendezvous_id(), bob.rendezvous_id()); + + bob + }; + + assert_eq!( + bob.sequence_token, "2", + "Bob should have remembered the sequence_token the server gave us." + ); + + { + let _scope = server + .register_as_scoped( + Mock::given(method("GET")).and(path(rendezvous_path.clone())).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "data": "", + "sequence_token": "2", + "expires_in_ms": 10_000, + })), + ), + ) + .await; + + let response = alice + .receive_single_message() + .await + .expect("We should be able to wait for data on the rendezvous channel."); + assert_eq!(response.status_code, StatusCode::OK); + } + + { + let _scope = server + .register_as_scoped( + Mock::given(method("PUT")).and(path(rendezvous_path.clone())).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "sequence_token": "3", + "expires_in_ms": 10_000, + })), + ), + ) + .await; + + bob.send("Hello world".to_owned()) + .await + .expect("We should be able to send data to the rendezvous server."); + } + + { + let _scope = server + .register_as_scoped( + Mock::given(method("GET")).and(path(rendezvous_path.clone())).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "data": "Hello world", + "sequence_token": "3", + "expires_in_ms": 10_000, + })), + ), + ) + .await; + + let response = alice + .receive_single_message() + .await + .expect("We should be able to wait and get data on the rendezvous channel."); + + assert_eq!(response.status_code, StatusCode::OK); + assert_eq!(response.data, "Hello world"); + } + } + + #[async_test] + async fn test_retry_mechanism() { + let server = MockServer::start().await; + let base_url = + Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"); + let rendezvous_path = mock_rendezvous_create(&server, "abcdEFG12345") + .await + .expect("We should be able to create a rendezvous"); + + let client = HttpClient::new(reqwest::Client::new(), RequestConfig::new().disable_retry()); + + let mut alice = Channel::create_outbound(client, &base_url) + .await + .expect("We should be able to create an outbound rendezvous channel"); + + server + .register( + Mock::given(method("GET")) + .and(path(rendezvous_path.clone())) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "sequence_token": alice.sequence_token, + "data": "old data", + "expires_in_ms": 10_000, + }))) + .up_to_n_times(1) + .expect(1), + ) + .await; + + server + .register( + Mock::given(method("GET")) + .and(path(rendezvous_path.clone())) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "sequence_token": "3", + "data": "Hello world", + "expires_in_ms": 10_000, + }))) + .expect(1), + ) + .await; + + let response = alice + .receive() + .await + .expect("We should be able to wait and get data on the rendezvous channel."); + + assert_eq!(response, "Hello world"); + } + + #[async_test] + async fn test_receive_error() { + let server = MockServer::start().await; + let url = + Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"); + let rendezvous_path = mock_rendezvous_create(&server, "abcdEFG12345") + .await + .expect("We should be able to create a rendezvous"); + + let client = HttpClient::new(reqwest::Client::new(), RequestConfig::new().disable_retry()); + + let mut alice = Channel::create_outbound(client, &url) + .await + .expect("We should be able to create an outbound rendezvous channel"); + + { + let _scope = server + .register_as_scoped( + Mock::given(method("GET")) + .and(path(rendezvous_path.clone())) + .respond_with(ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "No resource was found for this request.", + }))) + .expect(1), + ) + .await; + + alice.receive().await.expect_err("We should return an error if we receive a 404"); + } + + { + let _scope = server + .register_as_scoped( + Mock::given(method("GET")) + .and(path(rendezvous_path.clone())) + .respond_with(ResponseTemplate::new(504).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "No resource was found for this request.", + }))) + .expect(1), + ) + .await; + + alice + .receive() + .await + .expect_err("We should return an error if we receive a gateway timeout"); + } + } +} diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index c84924157d0..2b2d4329ec3 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -68,6 +68,9 @@ impl SecureChannel { (crypto_channel, qr_code_data) } + RendezvousInfo::Msc4388 { .. } => { + unreachable!("We don't create an MSC4388 conforming channel as of yet") + } }; Ok(Self { channel, qr_code_data, crypto_channel }) @@ -91,6 +94,9 @@ impl SecureChannel { mode_data, ); } + RendezvousInfo::Msc4388 { .. } => { + unreachable!("We don't create an MSC4388 conforming channel as of yet") + } } Ok(channel) From 4b589a659d3e62e24154941d78066b70687114e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Feb 2026 13:08:24 +0100 Subject: [PATCH 06/29] feat(oauth): Add support to MSC4388 for the QR login This patch connects everything together, the new QR code data format, secure channel, and rendezvous channel together to work for the QR login. --- .../src/authentication/oauth/qrcode/grant.rs | 42 +++++++--- .../src/authentication/oauth/qrcode/login.rs | 44 ++++++---- .../oauth/qrcode/rendezvous_channel/mod.rs | 28 ++++++- .../oauth/qrcode/secure_channel/mod.rs | 83 ++++++++++++------- 4 files changed, 136 insertions(+), 61 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index 08db08448d9..a79a7bb251c 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -1,4 +1,4 @@ -// Copyright 2025 The Matrix.org Foundation C.I.C. +// Copyright 2025, 2026 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. @@ -307,6 +307,7 @@ pub struct GrantLoginWithGeneratedQrCode<'a> { client: &'a Client, device_creation_timeout: Duration, state: SharedObservable>, + msc_4388_support: bool, } impl<'a> GrantLoginWithGeneratedQrCode<'a> { @@ -314,7 +315,12 @@ impl<'a> GrantLoginWithGeneratedQrCode<'a> { client: &'a Client, device_creation_timeout: Duration, ) -> GrantLoginWithGeneratedQrCode<'a> { - GrantLoginWithGeneratedQrCode { client, device_creation_timeout, state: Default::default() } + GrantLoginWithGeneratedQrCode { + client, + device_creation_timeout, + state: Default::default(), + msc_4388_support: false, + } } } @@ -330,6 +336,14 @@ impl GrantLoginWithGeneratedQrCode<'_> { ) -> impl Stream> + use<> { self.state.subscribe() } + + /// Enable and generate a QR code which supports [MSC4388]. + /// + /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388 + pub fn with_msc4388_support(&mut self) -> &mut Self { + self.msc_4388_support = true; + self + } } impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { @@ -344,7 +358,9 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { let homeserver_url = self.client.homeserver(); let http_client = self.client.inner.http_client.clone(); let secrets_bundle = export_secrets_bundle(self.client).await?; - let channel = SecureChannel::reciprocate(http_client, &homeserver_url).await?; + let channel = + SecureChannel::reciprocate(http_client, &homeserver_url, self.msc_4388_support) + .await?; // Extract the QR code data and emit an update so that the caller can // present the QR code for scanning by the new device. @@ -952,7 +968,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -1081,7 +1097,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -1326,7 +1342,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -1576,7 +1592,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -1831,7 +1847,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -2020,7 +2036,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -2223,7 +2239,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -2486,7 +2502,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -2760,7 +2776,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -3004,7 +3020,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index d618607c534..9eb9cc6bbaf 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The Matrix.org Foundation C.I.C. +// Copyright 2024, 2026 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. @@ -354,6 +354,7 @@ pub struct LoginWithGeneratedQrCode<'a> { client: &'a Client, registration_data: Option<&'a ClientRegistrationData>, state: SharedObservable>, + msc_4388_support: bool, } impl LoginWithGeneratedQrCode<'_> { @@ -366,6 +367,14 @@ impl LoginWithGeneratedQrCode<'_> { ) -> impl Stream> + use<> { self.state.subscribe() } + + /// Enable and generate a QR code which supports [MSC4388]. + /// + /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388 + pub fn with_msc4388_support(&mut self) -> &mut Self { + self.msc_4388_support = true; + self + } } impl<'a> IntoFuture for LoginWithGeneratedQrCode<'a> { @@ -436,7 +445,7 @@ impl<'a> LoginWithGeneratedQrCode<'a> { client: &'a Client, registration_data: Option<&'a ClientRegistrationData>, ) -> Self { - Self { client, registration_data, state: Default::default() } + Self { client, registration_data, state: Default::default(), msc_4388_support: false } } async fn establish_secure_channel( @@ -447,7 +456,9 @@ impl<'a> LoginWithGeneratedQrCode<'a> { // Create a new ephemeral key pair and a rendezvous session to request a login // with. // -- MSC4108 Secure channel setup steps 1 & 2 - let secure_channel = SecureChannel::login(http_client, &self.client.homeserver()).await?; + let secure_channel = + SecureChannel::login(http_client, &self.client.homeserver(), self.msc_4388_support) + .await?; // Extract the QR code data and emit a progress update so that the caller can // present the QR code for scanning by the other device. @@ -611,7 +622,7 @@ mod test { server.mock_query_keys().ok().expect(1).named("query_keys").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) .await .expect("Alice should be able to create a secure channel."); @@ -791,9 +802,10 @@ mod test { .await .expect("Should be able to create a client for Bob"); - let secure_channel = SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url) - .await - .expect("Bob should be able to create a secure channel"); + let secure_channel = + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, false) + .await + .expect("Bob should be able to create a secure channel"); assert_matches!( secure_channel.qr_code_data().intent_data(), @@ -899,9 +911,10 @@ mod test { .await .expect("Should be able to create a client for Bob"); - let secure_channel = SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url) - .await - .expect("Bob should be able to create a secure channel"); + let secure_channel = + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, false) + .await + .expect("Bob should be able to create a secure channel"); assert_matches!( secure_channel.qr_code_data().intent_data(), @@ -1015,7 +1028,7 @@ mod test { server.mock_who_am_i().ok().named("whoami").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) .await .expect("Alice should be able to create a secure channel."); @@ -1134,9 +1147,10 @@ mod test { .await .expect("Should be able to create a client for Bob"); - let secure_channel = SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url) - .await - .expect("Bob should be able to create a secure channel"); + let secure_channel = + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, false) + .await + .expect("Bob should be able to create a secure channel"); assert_matches!( secure_channel.qr_code_data().intent_data(), @@ -1352,7 +1366,7 @@ mod test { server.mock_who_am_i().ok().named("whoami").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) .await .expect("Alice should be able to create a secure channel."); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs index 356c1a2d9f6..e7d682b763f 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs @@ -44,7 +44,6 @@ pub(super) enum RendezvousInfo<'a> { pub(super) enum RendezvousChannel { Msc4108(msc_4108::Channel), - #[allow(dead_code)] Msc4388(msc_4388::Channel), } @@ -57,8 +56,13 @@ impl RendezvousChannel { pub(super) async fn create_outbound( client: HttpClient, rendezvous_server: &Url, + msc_4388: bool, ) -> Result { - Ok(Self::Msc4108(msc_4108::Channel::create_outbound(client, rendezvous_server).await?)) + if msc_4388 { + Ok(Self::Msc4388(msc_4388::Channel::create_outbound(client, rendezvous_server).await?)) + } else { + Ok(Self::Msc4108(msc_4108::Channel::create_outbound(client, rendezvous_server).await?)) + } } /// Create a new inbound [`RendezvousChannel`]. @@ -75,6 +79,24 @@ impl RendezvousChannel { Ok(InboundChannelCreationResult { channel: Self::Msc4108(channel), initial_message }) } + /// Create a new inbound [`RendezvousChannel`]. + /// + /// By inbound we mean that we're going to attempt to read an initial + /// message from the rendezvous session on the given [`rendezvous_url`]. + pub(super) async fn create_inbound_msc4388( + client: HttpClient, + base_url: &Url, + rendezvous_id: &str, + ) -> Result { + let msc_4388::InboundChannelCreationResult { channel, initial_message } = + msc_4388::Channel::create_inbound(client, base_url, rendezvous_id).await?; + + Ok(InboundChannelCreationResult { + channel: Self::Msc4388(channel), + initial_message: initial_message.into(), + }) + } + /// Get MSC-specific information about the rendezvous session we're using to /// exchange messages through the channel. pub(super) fn rendezvous_info(&self) -> RendezvousInfo<'_> { @@ -83,7 +105,7 @@ impl RendezvousChannel { RendezvousInfo::Msc4108 { rendezvous_url: channel.rendezvous_url() } } RendezvousChannel::Msc4388(channel) => { - RendezvousInfo::Msc4388 { rendezvous_id: &channel.rendezvous_id() } + RendezvousInfo::Msc4388 { rendezvous_id: channel.rendezvous_id() } } } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 2b2d4329ec3..7f6b2991428 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2024 The Matrix.org Foundation C.I.C. +// Copyright 2024, 2026 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. @@ -52,8 +52,10 @@ impl SecureChannel { pub(super) async fn login( http_client: HttpClient, homeserver_url: &Url, + msc_4388: bool, ) -> Result { - let channel = RendezvousChannel::create_outbound(http_client, homeserver_url).await?; + let channel = + RendezvousChannel::create_outbound(http_client, homeserver_url, msc_4388).await?; let (crypto_channel, qr_code_data) = match channel.rendezvous_info() { RendezvousInfo::Msc4108 { rendezvous_url } => { @@ -68,8 +70,17 @@ impl SecureChannel { (crypto_channel, qr_code_data) } - RendezvousInfo::Msc4388 { .. } => { - unreachable!("We don't create an MSC4388 conforming channel as of yet") + RendezvousInfo::Msc4388 { rendezvous_id } => { + let crypto_channel = CryptoChannel::new_hpke(); + + let qr_code_data = QrCodeData::new_msc4388( + crypto_channel.public_key(), + rendezvous_id.to_owned(), + homeserver_url.clone(), + QrCodeIntent::Login, + ); + + (crypto_channel, qr_code_data) } }; @@ -80,8 +91,9 @@ impl SecureChannel { pub(super) async fn reciprocate( http_client: HttpClient, homeserver_url: &Url, + msc_4388: bool, ) -> Result { - let mut channel = SecureChannel::login(http_client, homeserver_url).await?; + let mut channel = SecureChannel::login(http_client, homeserver_url, msc_4388).await?; match channel.channel.rendezvous_info() { RendezvousInfo::Msc4108 { rendezvous_url } => { @@ -94,8 +106,13 @@ impl SecureChannel { mode_data, ); } - RendezvousInfo::Msc4388 { .. } => { - unreachable!("We don't create an MSC4388 conforming channel as of yet") + RendezvousInfo::Msc4388 { rendezvous_id } => { + channel.qr_code_data = QrCodeData::new_msc4388( + channel.crypto_channel.public_key(), + rendezvous_id.to_owned(), + homeserver_url.clone(), + QrCodeIntent::Reciprocate, + ); } } @@ -200,25 +217,28 @@ impl EstablishedSecureChannel { // it's talking to us, the device that scanned the QR code, until it // receives and successfully decrypts the initial message. We're here encrypting // the `LOGIN_INITIATE_MESSAGE`. - let (crypto_channel, encoded_message) = if true { - let ecies = Ecies::new(); - - let OutboundCreationResult { ecies, message } = ecies - .establish_outbound_channel( - qr_code_data.public_key(), - LOGIN_INITIATE_MESSAGE.as_bytes(), - ) - .map_err(DecryptionError::from)?; - (ChannelType::Ecies(ecies), message.encode()) - } else { - let SenderCreationResult { channel, message } = HpkeSenderChannel::new() - .establish_channel( - qr_code_data.public_key(), - LOGIN_INITIATE_MESSAGE.as_bytes(), - // TODO: Do we want to include some additional authenticated data here? - &[], - ); - (ChannelType::Hpke(channel), message.encode()) + let (crypto_channel, encoded_message) = match qr_code_data.intent_data() { + QrCodeIntentData::Msc4108 { .. } => { + let ecies = Ecies::new(); + + let OutboundCreationResult { ecies, message } = ecies + .establish_outbound_channel( + qr_code_data.public_key(), + LOGIN_INITIATE_MESSAGE.as_bytes(), + ) + .map_err(DecryptionError::from)?; + (ChannelType::Ecies(ecies), message.encode()) + } + QrCodeIntentData::Msc4388 { .. } => { + let SenderCreationResult { channel, message } = HpkeSenderChannel::new() + .establish_channel( + qr_code_data.public_key(), + LOGIN_INITIATE_MESSAGE.as_bytes(), + // TODO: Do we want to include some additional authenticated data here? + &[], + ); + (ChannelType::Hpke(channel), message.encode()) + } }; // The other side has crated a rendezvous channel, we're going to connect to it @@ -231,9 +251,12 @@ impl EstablishedSecureChannel { RendezvousChannel::create_inbound(client, rendezvous_url).await?; channel } - // TODO: We need to support the new rendezvous channel type and HPKE for the crypto - // channel when we encounter this QR code variant. - QrCodeIntentData::Msc4388 { .. } => return Err(Error::UnsupportedQrCodeType), + QrCodeIntentData::Msc4388 { rendezvous_id, base_url } => { + let InboundChannelCreationResult { channel, .. } = + RendezvousChannel::create_inbound_msc4388(client, base_url, rendezvous_id) + .await?; + channel + } }; trace!( @@ -492,7 +515,7 @@ pub(super) mod test { MockedRendezvousServer::new(&server, "abcdEFG12345", Duration::MAX).await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) .await .expect("Alice should be able to create a secure channel."); From 0ef9dce2472e1f2693c47819c402ee1d501f0f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 6 Feb 2026 17:27:06 +0100 Subject: [PATCH 07/29] test(oauth): Add some more tests for the MSC4388 QR login variant --- .../src/authentication/oauth/qrcode/grant.rs | 104 +++-- .../src/authentication/oauth/qrcode/login.rs | 405 ++++++++++++++---- .../qrcode/rendezvous_channel/msc_4388.rs | 2 +- .../oauth/qrcode/secure_channel/mod.rs | 163 ++++++- 4 files changed, 565 insertions(+), 109 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index a79a7bb251c..8a97542279b 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -789,11 +789,11 @@ mod test { ); } - #[async_test] - async fn test_grant_login_with_generated_qr_code() { + async fn test_grant_login_with_generated_qr_code(msc_4388: bool) { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, msc_4388) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -838,10 +838,15 @@ mod test { // Prepare the login granting future. let oauth = alice.oauth(); - let grant = oauth + let mut grant = oauth .grant_login_with_qr_code() .device_creation_timeout(Duration::from_secs(2)) .generate(); + + if msc_4388 { + grant.with_msc4388_support(); + } + let secrets_bundle = export_secrets_bundle(&alice) .await .expect("Alice should be able to export the secrets bundle"); @@ -934,10 +939,20 @@ mod test { } #[async_test] - async fn test_grant_login_with_scanned_qr_code() { + async fn test_grant_login_with_generated_qr_code_msc_4108() { + test_grant_login_with_generated_qr_code(false).await; + } + + #[async_test] + async fn test_grant_login_with_generated_qr_code_msc_4388() { + test_grant_login_with_generated_qr_code(true).await; + } + + async fn test_grant_login_with_scanned_qr_code(msc_4388: bool) { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, msc_4388) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -968,7 +983,7 @@ mod test { // Create a secure channel on the new client (Bob) and extract the QR code. let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, false) + let channel = SecureChannel::login(client, &rendezvous_server.homeserver_url, msc_4388) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -1060,11 +1075,22 @@ mod test { bob_task.await.expect("Bob's task should finish"); } + #[async_test] + async fn test_grant_login_with_scanned_qr_code_msc_4108() { + test_grant_login_with_scanned_qr_code(false).await; + } + + #[async_test] + async fn test_grant_login_with_scanned_qr_code_msc_4388() { + test_grant_login_with_scanned_qr_code(true).await; + } + #[async_test] async fn test_grant_login_with_scanned_qr_code_with_homeserver_swap() { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -1194,7 +1220,8 @@ mod test { { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -1320,7 +1347,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_unexpected_message_instead_of_login_protocol() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -1431,7 +1459,8 @@ mod test { async fn test_grant_login_with_generated_qr_code_device_already_exists() { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -1561,7 +1590,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_device_already_exists() { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -1676,7 +1706,8 @@ mod test { async fn test_grant_login_with_generated_qr_code_device_not_found() { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -1816,7 +1847,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_device_not_found() { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); let device_authorization_grant = AuthorizationGrant { @@ -1938,9 +1970,13 @@ mod test { #[async_test] async fn test_grant_login_with_generated_qr_code_session_expired() { let server = MatrixMockServer::new().await; - let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::from_secs(2)) - .await; + let rendezvous_server = MockedRendezvousServer::new( + server.server(), + "abcdEFG12345", + Duration::from_secs(2), + false, + ) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; @@ -2013,9 +2049,13 @@ mod test { #[async_test] async fn test_grant_login_with_scanned_qr_code_session_expired() { let server = MatrixMockServer::new().await; - let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::from_secs(2)) - .await; + let rendezvous_server = MockedRendezvousServer::new( + server.server(), + "abcdEFG12345", + Duration::from_secs(2), + false, + ) + .await; debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); server.mock_upload_keys().ok().expect(1).named("upload_keys").mount().await; @@ -2094,7 +2134,8 @@ mod test { async fn test_grant_login_with_generated_qr_code_login_failure_instead_of_login_protocol() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2217,7 +2258,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_login_failure_instead_of_login_protocol() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2325,7 +2367,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_login_failure_instead_of_login_success() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2470,7 +2513,8 @@ mod test { async fn test_grant_login_with_generated_qr_code_login_failure_instead_of_login_success() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2598,7 +2642,8 @@ mod test { async fn test_grant_login_with_generated_qr_code_unexpected_message_instead_of_login_success() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2744,7 +2789,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_unexpected_message_instead_of_login_success() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2873,7 +2919,8 @@ mod test { async fn test_grant_login_with_generated_qr_code_secure_channel_error() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); @@ -2998,7 +3045,8 @@ mod test { async fn test_grant_login_with_scanned_qr_code_secure_channel_error() { let server = MatrixMockServer::new().await; let rendezvous_server = Arc::new( - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await, + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await, ); debug!("Set up rendezvous server mock at {}", rendezvous_server.rendezvous_url); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 9eb9cc6bbaf..85614a1908c 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -301,6 +301,8 @@ impl<'a> IntoFuture for LoginWithQrCode<'a> { // scanned the QR code, we're certain that the secure channel is // secure, under the assumption that we didn't scan the wrong QR code. // -- MSC4108 Secure channel setup steps 3-5 + trace!("Trying to establish the secure channel"); + let channel = self.establish_secure_channel().await?; trace!("Established the secure channel."); @@ -385,6 +387,8 @@ impl<'a> IntoFuture for LoginWithGeneratedQrCode<'a> { Box::pin(async move { // Establish and verify the secure channel. // -- MSC4108 Secure channel setup all steps + trace!("Trying to establish the secure channel"); + let mut channel = self.establish_secure_channel().await?; trace!("Established the secure channel."); @@ -597,11 +601,11 @@ mod test { alice.send_json(message).await.unwrap(); } - #[async_test] - async fn test_qr_login() { + async fn test_qr_login(msc_4388: bool) { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, msc_4388) + .await; let (sender, receiver) = tokio::sync::oneshot::channel(); let oauth_server = server.oauth(); @@ -622,16 +626,28 @@ mod test { server.mock_query_keys().ok().expect(1).named("query_keys").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, msc_4388) .await .expect("Alice should be able to create a secure channel."); - assert_let!( - QrCodeIntentData::Msc4108 { - data: Msc4108IntentData::Reciprocate { server_name }, - .. - } = &alice.qr_code_data().intent_data() - ); + assert_eq!(alice.qr_code_data().intent(), QrCodeIntent::Reciprocate); + + let server_name = if msc_4388 { + assert_let!( + QrCodeIntentData::Msc4388 { base_url, .. } = &alice.qr_code_data().intent_data() + ); + + base_url.to_string() + } else { + assert_let!( + QrCodeIntentData::Msc4108 { + data: Msc4108IntentData::Reciprocate { server_name }, + .. + } = &alice.qr_code_data().intent_data() + ); + + server_name.to_owned() + }; let bob = Client::builder() .server_name_or_homeserver_url(server_name) @@ -679,6 +695,16 @@ mod test { assert!(own_identity.is_verified()); } + #[async_test] + async fn test_qr_login_msc_4108() { + test_qr_login(false).await; + } + + #[async_test] + async fn test_qr_login_msc_4388() { + test_qr_login(true).await; + } + async fn grant_login_with_generated_qr( alice: &Client, qr_receiver: tokio::sync::oneshot::Receiver, @@ -762,11 +788,11 @@ mod test { .expect("Alice should be able to send the `m.login.secrets` message to Bob"); } - #[async_test] - async fn test_generated_qr_login() { + async fn test_generated_qr_login(msc_4388: bool) { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, msc_4388) + .await; let (qr_sender, qr_receiver) = tokio::sync::oneshot::channel(); let (cctx_sender, cctx_receiver) = tokio::sync::oneshot::channel(); @@ -803,18 +829,32 @@ mod test { .expect("Should be able to create a client for Bob"); let secure_channel = - SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, false) + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, msc_4388) .await .expect("Bob should be able to create a secure channel"); - assert_matches!( - secure_channel.qr_code_data().intent_data(), - QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } - ); + assert_eq!(secure_channel.qr_code_data().intent(), QrCodeIntent::Login); + + if msc_4388 { + assert_matches!( + secure_channel.qr_code_data().intent_data(), + QrCodeIntentData::Msc4388 { .. } + ); + } else { + assert_matches!( + secure_channel.qr_code_data().intent_data(), + QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } + ); + } let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + let mut bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + + if msc_4388 { + bob_login.with_msc4388_support(); + } + let mut bob_updates = bob_login.subscribe_to_progress(); let updates_task = spawn(async move { @@ -868,10 +908,20 @@ mod test { } #[async_test] - async fn test_generated_qr_login_with_homeserver_swap() { + async fn test_generated_qr_login_msc_4108() { + test_generated_qr_login(false).await; + } + + #[async_test] + async fn test_generated_qr_login_msc_4388() { + test_generated_qr_login(true).await; + } + + async fn test_generated_qr_login_with_homeserver_swap(msc_4388: bool) { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, msc_4388) + .await; let (qr_sender, qr_receiver) = tokio::sync::oneshot::channel(); let (cctx_sender, cctx_receiver) = tokio::sync::oneshot::channel(); @@ -912,18 +962,32 @@ mod test { .expect("Should be able to create a client for Bob"); let secure_channel = - SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, false) + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, msc_4388) .await .expect("Bob should be able to create a secure channel"); - assert_matches!( - secure_channel.qr_code_data().intent_data(), - QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } - ); + assert_eq!(secure_channel.qr_code_data().intent(), QrCodeIntent::Login); + + if msc_4388 { + assert_matches!( + secure_channel.qr_code_data().intent_data(), + QrCodeIntentData::Msc4388 { .. } + ); + } else { + assert_matches!( + secure_channel.qr_code_data().intent_data(), + QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } + ); + } let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + let mut bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + + if msc_4388 { + bob_login.with_msc4388_support(); + } + let mut bob_updates = bob_login.subscribe_to_progress(); let updates_task = spawn(async move { @@ -976,9 +1040,20 @@ mod test { assert!(own_identity.is_verified()); } + #[async_test] + async fn test_generated_qr_login_with_homeserver_swap_msc_4108() { + test_generated_qr_login_with_homeserver_swap(false).await; + } + + #[async_test] + async fn test_generated_qr_login_with_homeserver_swap_msc_4388() { + test_generated_qr_login_with_homeserver_swap(true).await; + } + async fn test_failure( token_response: TokenResponse, alice_behavior: AliceBehaviour, + msc_4388: bool, ) -> Result<(), QRCodeLoginError> { let server = MatrixMockServer::new().await; let expiration = match alice_behavior { @@ -986,7 +1061,8 @@ mod test { _ => Duration::MAX, }; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", expiration).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", expiration, msc_4388) + .await; let (sender, receiver) = tokio::sync::oneshot::channel(); let oauth_server = server.oauth(); @@ -1028,16 +1104,30 @@ mod test { server.mock_who_am_i().ok().named("whoami").mount().await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, msc_4388) .await .expect("Alice should be able to create a secure channel."); - assert_let!( - QrCodeIntentData::Msc4108 { - data: Msc4108IntentData::Reciprocate { server_name }, - .. - } = &alice.qr_code_data().intent_data() - ); + assert_eq!(alice.qr_code_data().intent(), QrCodeIntent::Reciprocate); + + let server_name = if msc_4388 { + assert_let!( + QrCodeIntentData::Msc4388 { base_url, .. } = &alice.qr_code_data().intent_data() + ); + + base_url.to_string() + } else { + assert_let!( + QrCodeIntentData::Msc4108 { + data: Msc4108IntentData::Reciprocate { server_name }, + .. + } = &alice.qr_code_data().intent_data() + ); + + server_name.to_owned() + }; + + assert_eq!(alice.qr_code_data().intent(), QrCodeIntent::Reciprocate); let bob = Client::builder() .server_name_or_homeserver_url(server_name) @@ -1082,6 +1172,7 @@ mod test { async fn test_generated_failure( token_response: TokenResponse, alice_behavior: AliceBehaviour, + msc_4388: bool, ) -> Result<(), QRCodeLoginError> { let server = MatrixMockServer::new().await; let expiration = match alice_behavior { @@ -1089,7 +1180,8 @@ mod test { _ => Duration::MAX, }; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", expiration).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", expiration, msc_4388) + .await; let (qr_sender, qr_receiver) = tokio::sync::oneshot::channel(); let (cctx_sender, cctx_receiver) = tokio::sync::oneshot::channel(); @@ -1148,18 +1240,32 @@ mod test { .expect("Should be able to create a client for Bob"); let secure_channel = - SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, false) + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, msc_4388) .await .expect("Bob should be able to create a secure channel"); - assert_matches!( - secure_channel.qr_code_data().intent_data(), - QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } - ); + assert_eq!(secure_channel.qr_code_data().intent(), QrCodeIntent::Login); + + if msc_4388 { + assert_matches!( + secure_channel.qr_code_data().intent_data(), + QrCodeIntentData::Msc4388 { .. } + ); + } else { + assert_matches!( + secure_channel.qr_code_data().intent_data(), + QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } + ); + } let registration_data = mock_client_metadata().into(); let bob_oauth = bob.oauth(); - let bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + let mut bob_login = bob_oauth.login_with_qr_code(Some(®istration_data)).generate(); + + if msc_4388 { + bob_login.with_msc4388_support(); + } + let mut bob_updates = bob_login.subscribe_to_progress(); let _updates_task = spawn(async move { @@ -1200,9 +1306,9 @@ mod test { bob_login.await } - #[async_test] - async fn test_qr_login_refused_access_token() { - let result = test_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath).await; + async fn test_qr_login_refused_access_token(msc_4388: bool) { + let result = + test_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath, msc_4388).await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1213,9 +1319,22 @@ mod test { } #[async_test] - async fn test_generated_qr_login_refused_access_token() { - let result = - test_generated_failure(TokenResponse::AccessDenied, AliceBehaviour::HappyPath).await; + async fn test_qr_login_refused_access_token_msc_4108() { + test_qr_login_refused_access_token(false).await + } + + #[async_test] + async fn test_qr_login_refused_access_token_msc_4388() { + test_qr_login_refused_access_token(true).await + } + + async fn test_generated_qr_login_refused_access_token(msc_4388: bool) { + let result = test_generated_failure( + TokenResponse::AccessDenied, + AliceBehaviour::HappyPath, + msc_4388, + ) + .await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1226,8 +1345,18 @@ mod test { } #[async_test] - async fn test_qr_login_expired_token() { - let result = test_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath).await; + async fn test_generated_qr_login_refused_access_token_msc_4108() { + test_generated_qr_login_refused_access_token(false).await; + } + + #[async_test] + async fn test_generated_qr_login_refused_access_token_msc_4388() { + test_generated_qr_login_refused_access_token(true).await; + } + + async fn test_qr_login_expired_token(msc_4388: bool) { + let result = + test_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath, msc_4388).await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1238,9 +1367,22 @@ mod test { } #[async_test] - async fn test_generated_qr_login_expired_token() { - let result = - test_generated_failure(TokenResponse::ExpiredToken, AliceBehaviour::HappyPath).await; + async fn test_qr_login_expired_token_msc_4108() { + test_qr_login_expired_token(false).await; + } + + #[async_test] + async fn test_qr_login_expired_token_msc_4388() { + test_qr_login_expired_token(true).await; + } + + async fn test_generated_qr_login_expired_token(msc_4388: bool) { + let result = test_generated_failure( + TokenResponse::ExpiredToken, + AliceBehaviour::HappyPath, + msc_4388, + ) + .await; assert_let!(Err(QRCodeLoginError::OAuth(e)) = result); assert_eq!( @@ -1251,8 +1393,18 @@ mod test { } #[async_test] - async fn test_qr_login_declined_protocol() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol).await; + async fn test_generated_qr_login_expired_token_msc_4108() { + test_generated_qr_login_expired_token(false).await; + } + + #[async_test] + async fn test_generated_qr_login_expired_token_msc_4388() { + test_generated_qr_login_expired_token(true).await; + } + + async fn test_qr_login_declined_protocol(msc_4388: bool) { + let result = + test_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol, msc_4388).await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!( @@ -1263,9 +1415,19 @@ mod test { } #[async_test] - async fn test_generated_qr_login_declined_protocol() { + async fn test_qr_login_declined_protocol_msc_4108() { + test_qr_login_declined_protocol(false).await; + } + + #[async_test] + async fn test_qr_login_declined_protocol_msc_4388() { + test_qr_login_declined_protocol(true).await; + } + + async fn test_generated_qr_login_declined_protocol(msc_4388: bool) { let result = - test_generated_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol).await; + test_generated_failure(TokenResponse::Ok, AliceBehaviour::DeclinedProtocol, msc_4388) + .await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!( @@ -1276,37 +1438,79 @@ mod test { } #[async_test] - async fn test_qr_login_unexpected_message() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage).await; + async fn test_generated_qr_login_declined_protocol_msc_4108() { + test_generated_qr_login_declined_protocol(false).await; + } + + #[async_test] + async fn test_generated_qr_login_declined_protocol_msc_4388() { + test_generated_qr_login_declined_protocol(true).await; + } + + async fn test_qr_login_unexpected_message(msc_4388: bool) { + let result = + test_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage, msc_4388).await; assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); assert_eq!(expected, "m.login.protocol_accepted"); } #[async_test] - async fn test_generated_qr_login_unexpected_message() { + async fn test_qr_login_unexpected_message_msc_4108() { + test_qr_login_unexpected_message(false).await; + } + + #[async_test] + async fn test_qr_login_unexpected_message_msc_4388() { + test_qr_login_unexpected_message(true).await; + } + + async fn test_generated_qr_login_unexpected_message(msc_4388: bool) { let result = - test_generated_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage).await; + test_generated_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessage, msc_4388) + .await; assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); assert_eq!(expected, "m.login.protocol_accepted"); } #[async_test] - async fn test_qr_login_unexpected_message_instead_of_secrets() { - let result = - test_failure(TokenResponse::Ok, AliceBehaviour::UnexpectedMessageInsteadOfSecrets) - .await; + async fn test_generated_qr_login_unexpected_message_msc_4108() { + test_generated_qr_login_unexpected_message(false).await; + } + + #[async_test] + async fn test_generated_qr_login_unexpected_message_msc_4388() { + test_generated_qr_login_unexpected_message(true).await; + } + + async fn test_qr_login_unexpected_message_instead_of_secrets(msc_4388: bool) { + let result = test_failure( + TokenResponse::Ok, + AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + msc_4388, + ) + .await; assert_let!(Err(QRCodeLoginError::UnexpectedMessage { expected, .. }) = result); assert_eq!(expected, "m.login.secrets"); } #[async_test] - async fn test_generated_qr_login_unexpected_message_instead_of_secrets() { + async fn test_qr_login_unexpected_message_instead_of_secrets_msc_4108() { + test_qr_login_unexpected_message_instead_of_secrets(false).await; + } + + #[async_test] + async fn test_qr_login_unexpected_message_instead_of_secrets_msc_4388() { + test_qr_login_unexpected_message_instead_of_secrets(true).await; + } + + async fn test_generated_qr_login_unexpected_message_instead_of_secrets(msc_4388: bool) { let result = test_generated_failure( TokenResponse::Ok, AliceBehaviour::UnexpectedMessageInsteadOfSecrets, + msc_4388, ) .await; @@ -1315,41 +1519,92 @@ mod test { } #[async_test] - async fn test_qr_login_refuse_secrets() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets).await; + async fn test_generated_qr_login_unexpected_message_instead_of_secrets_msc_4108() { + test_generated_qr_login_unexpected_message_instead_of_secrets(false).await; + } + + #[async_test] + async fn test_generated_qr_login_unexpected_message_instead_of_secrets_msc_4388() { + test_generated_qr_login_unexpected_message_instead_of_secrets(true).await; + } + + async fn test_qr_login_refuse_secrets(msc_4388: bool) { + let result = test_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets, msc_4388).await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!(reason, LoginFailureReason::DeviceNotFound); } #[async_test] - async fn test_generated_qr_login_refuse_secrets() { - let result = test_generated_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets).await; + async fn test_qr_login_refuse_secrets_msc_4108() { + test_qr_login_refuse_secrets(false).await + } + + #[async_test] + async fn test_qr_login_refuse_secrets_msc_4388() { + test_qr_login_refuse_secrets(true).await + } + + async fn test_generated_qr_login_refuse_secrets(msc_4388: bool) { + let result = + test_generated_failure(TokenResponse::Ok, AliceBehaviour::RefuseSecrets, msc_4388) + .await; assert_let!(Err(QRCodeLoginError::LoginFailure { reason, .. }) = result); assert_eq!(reason, LoginFailureReason::DeviceNotFound); } #[async_test] - async fn test_qr_login_session_expired() { - let result = test_failure(TokenResponse::Ok, AliceBehaviour::LetSessionExpire).await; + async fn test_generated_qr_login_refuse_secrets_msc_4108() { + test_generated_qr_login_refuse_secrets(false).await; + } + + #[async_test] + async fn test_generated_qr_login_refuse_secrets_msc_4388() { + test_generated_qr_login_refuse_secrets(true).await; + } + + async fn test_qr_login_session_expired(msc_4388: bool) { + let result = + test_failure(TokenResponse::Ok, AliceBehaviour::LetSessionExpire, msc_4388).await; assert_matches!(result, Err(QRCodeLoginError::NotFound)); } #[async_test] - async fn test_generated_qr_login_session_expired() { + async fn test_qr_login_session_expired_msc_4108() { + test_qr_login_session_expired(false).await; + } + + #[async_test] + async fn test_qr_login_session_expired_msc_4388() { + test_qr_login_session_expired(true).await; + } + + async fn test_generated_qr_login_session_expired(msc_4388: bool) { let result = - test_generated_failure(TokenResponse::Ok, AliceBehaviour::LetSessionExpire).await; + test_generated_failure(TokenResponse::Ok, AliceBehaviour::LetSessionExpire, msc_4388) + .await; assert_matches!(result, Err(QRCodeLoginError::NotFound)); } + #[async_test] + async fn test_generated_qr_login_session_expired_msc_4108() { + test_generated_qr_login_session_expired(false).await; + } + + #[async_test] + async fn test_generated_qr_login_session_expired_msc_4388() { + test_generated_qr_login_session_expired(true).await; + } + #[async_test] async fn test_device_authorization_endpoint_missing() { let server = MatrixMockServer::new().await; let rendezvous_server = - MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(server.server(), "abcdEFG12345", Duration::MAX, false) + .await; let (sender, receiver) = tokio::sync::oneshot::channel(); let oauth_server = server.oauth(); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs index 403ac3e6b6b..25e977826dc 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs @@ -343,7 +343,7 @@ mod test { let client = HttpClient::new(reqwest::Client::new(), RequestConfig::short_retry()); let InboundChannelCreationResult { channel: bob, initial_message: _ } = - Channel::create_inbound(client, &base_url, &rendezvous_id.to_owned()).await.expect( + Channel::create_inbound(client, &base_url, rendezvous_id).await.expect( "We should be able to create a rendezvous channel from a received message", ); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 7f6b2991428..124a7794c8f 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -363,8 +363,10 @@ pub(super) mod test { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::async_test; use ruma::time::Instant; + use serde::Deserialize; use serde_json::json; use similar_asserts::assert_eq; + use tracing::trace; use url::Url; use vodozemac::hpke::DigitMode; use wiremock::{ @@ -389,7 +391,24 @@ pub(super) mod test { } impl MockedRendezvousServer { - pub async fn new(server: &MockServer, location: &str, expiration: Duration) -> Self { + pub async fn new( + server: &MockServer, + location: &str, + expiration: Duration, + msc_4388: bool, + ) -> Self { + if msc_4388 { + Self::new_msc4388(server, expiration).await + } else { + Self::new_msc4108(server, location, expiration).await + } + } + + pub async fn new_msc4108( + server: &MockServer, + location: &str, + expiration: Duration, + ) -> Self { let content: Arc>> = Mutex::default().into(); let created: Arc>> = Mutex::default().into(); let etag = Arc::new(AtomicU8::new(0)); @@ -506,16 +525,140 @@ pub(super) mod test { rendezvous_url, } } + + pub async fn new_msc4388(server: &MockServer, expiration: Duration) -> Self { + #[derive(Debug, Deserialize)] + struct PutContent { + #[allow(dead_code)] + sequence_token: String, + data: String, + } + + const RENDEZVOUS_ID: &str = "abcdEFG12345"; + + let content: Arc>> = Mutex::default().into(); + let created: Arc>> = Mutex::default().into(); + let sequence_token = Arc::new(AtomicU8::new(0)); + + let homeserver_url = Url::parse(&server.uri()) + .expect("We should be able to parse the example homeserver"); + + let rendezvous_url = homeserver_url + .join(RENDEZVOUS_ID) + .expect("We should be able to create a rendezvous URL"); + + let post_guard = server + .register_as_scoped( + Mock::given(method("POST")) + .and(path("/_matrix/client/unstable/io.element.msc4388/rendezvous")) + .respond_with({ + *created.lock().unwrap() = Some(Instant::now()); + + trace!("Creating a new rendezvous channel ID: {RENDEZVOUS_ID}"); + + ResponseTemplate::new(200).set_body_json(json!({ + "id": RENDEZVOUS_ID, + "sequence_token": "0", + "expires_in_ms": 100_000, + })) + }), + ) + .await; + + let put_guard = server + .register_as_scoped( + Mock::given(method("PUT")) + .and(path(format!( + "/_matrix/client/unstable/io.element.msc4388/rendezvous/{RENDEZVOUS_ID}" + ))) + .respond_with({ + let content = content.clone(); + let created = created.clone(); + let sequence_token = sequence_token.clone(); + + + move |request: &wiremock::Request| { + // Fail the request if the session has expired. + if created.lock().unwrap().unwrap().elapsed() > expiration { + return ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "This rendezvous session does not exist.", + })); + } + + let request_content: PutContent = request.body_json().unwrap(); + *content.lock().unwrap() = Some(request_content.data); + + let prev_token = + sequence_token.fetch_add(1, Ordering::SeqCst); + + trace!("Putting new content into the rendezvous channel ID: {RENDEZVOUS_ID}"); + + ResponseTemplate::new(200).set_body_json(json!({ + "sequence_token": (prev_token + 1 ).to_string(), + + })) + } + }), + ) + .await; + + let get_guard = server + .register_as_scoped( + Mock::given(method("GET")) + .and(path(format!( + "/_matrix/client/unstable/io.element.msc4388/rendezvous/{RENDEZVOUS_ID}" + ))) + .respond_with({ + let content = content.clone(); + let created = created.clone(); + let sequence_token = sequence_token.clone(); + + move |_: &wiremock::Request| { + // Fail the request if the session has expired. + if created.lock().unwrap().unwrap().elapsed() > expiration { + return ResponseTemplate::new(404).set_body_json(json!({ + "errcode": "M_NOT_FOUND", + "error": "This rendezvous session does not exist.", + })); + } + + let content = content.lock().unwrap(); + let current_sequence_token = sequence_token.load(Ordering::SeqCst); + + let content = content.clone(); + + ResponseTemplate::new(200).set_body_json(json!({ + "data": content.unwrap_or_default(), + "sequence_token": current_sequence_token.to_string(), + "expires_in_ms": 100_000, + })) + } + }), + ) + .await; + + Self { + expiration, + content, + created, + etag: sequence_token, + post_guard, + put_guard, + get_guard, + homeserver_url, + rendezvous_url, + } + } } - #[async_test] - async fn test_creation() { + async fn test_creation(msc_4388: bool) { let server = MockServer::start().await; let rendezvous_server = - MockedRendezvousServer::new(&server, "abcdEFG12345", Duration::MAX).await; + MockedRendezvousServer::new(&server, "abcdEFG12345", Duration::MAX, msc_4388).await; let client = HttpClient::new(reqwest::Client::new(), Default::default()); - let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, false) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, msc_4388) .await .expect("Alice should be able to create a secure channel."); @@ -549,4 +692,14 @@ pub(super) mod test { assert_eq!(bob.channel.rendezvous_info(), alice.channel.rendezvous_info()); } + + #[async_test] + async fn test_creation_msc4388() { + test_creation(true).await; + } + + #[async_test] + async fn test_creation_msc4108() { + test_creation(false).await; + } } From b3d3fc46fd9ebc48dfb97a67e78bd07f1d16a1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 23 Feb 2026 15:56:28 +0100 Subject: [PATCH 08/29] feat(qr-login): Include some additional authenticated data in the HPKE channel --- .../oauth/qrcode/rendezvous_channel/mod.rs | 24 +++++++ .../qrcode/rendezvous_channel/msc_4388.rs | 8 ++- .../qrcode/secure_channel/crypto_channel.rs | 3 +- .../oauth/qrcode/secure_channel/mod.rs | 67 +++++++++++-------- 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs index e7d682b763f..e9640aa9270 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs @@ -140,4 +140,28 @@ impl RendezvousChannel { RendezvousChannel::Msc4388(channel) => Ok(channel.receive().await?), } } + + /// Get additional authenticated data which should be used by the crypto + /// channel to bind individual messages to this specific rendezvous + /// channel run. + /// + /// This is only used in MSC4388, as such only the HPKE crypto channel will + /// use this. + pub(super) fn additional_authenticated_data(&self) -> Option> { + match self { + RendezvousChannel::Msc4108(_) => None, + RendezvousChannel::Msc4388(channel) => { + let msc_4388::Channel { base_url, rendezvous_id, sequence_token, .. } = channel; + + Some( + [ + base_url.as_str().as_bytes(), + rendezvous_id.as_bytes(), + sequence_token.as_bytes(), + ] + .concat(), + ) + } + } + } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs index 25e977826dc..91448e069e0 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs @@ -52,9 +52,11 @@ struct RendezvousMessage { pub(crate) struct Channel { client: HttpClient, - base_url: Url, - rendezvous_id: String, - sequence_token: String, + pub(super) base_url: Url, + /// The ID of the rendezvous session we're using to exchange messages + /// through the channel. + pub(super) rendezvous_id: String, + pub(super) sequence_token: String, } fn response_to_error(status: StatusCode, data: String) -> HttpError { diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs index 01ed8a6afff..dd25544604d 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs @@ -67,6 +67,7 @@ impl CryptoChannel { pub(super) fn establish_inbound_channel( self, message: &str, + aad: &[u8], ) -> Result { match self { CryptoChannel::Ecies(ecies) => { @@ -79,7 +80,7 @@ impl CryptoChannel { let message = hpke::InitialMessage::decode(message).map_err(MessageDecodeError::from)?; Ok(CryptoChannelCreationResult::Hpke( - hpke.establish_channel(&message, &[]).map_err(DecryptionError::from)?, + hpke.establish_channel(&message, aad).map_err(DecryptionError::from)?, )) } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 124a7794c8f..19c3b4df6bf 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -70,7 +70,7 @@ impl SecureChannel { (crypto_channel, qr_code_data) } - RendezvousInfo::Msc4388 { rendezvous_id } => { + RendezvousInfo::Msc4388 { rendezvous_id, .. } => { let crypto_channel = CryptoChannel::new_hpke(); let qr_code_data = QrCodeData::new_msc4388( @@ -106,7 +106,7 @@ impl SecureChannel { mode_data, ); } - RendezvousInfo::Msc4388 { rendezvous_id } => { + RendezvousInfo::Msc4388 { rendezvous_id, .. } => { channel.qr_code_data = QrCodeData::new_msc4388( channel.crypto_channel.public_key(), rendezvous_id.to_owned(), @@ -127,8 +127,9 @@ impl SecureChannel { pub(super) async fn connect(mut self) -> Result { trace!("Trying to connect the secure channel."); + let aad = self.channel.additional_authenticated_data().unwrap_or_default(); let message = self.channel.receive().await?; - let result = self.crypto_channel.establish_inbound_channel(&message)?; + let result = self.crypto_channel.establish_inbound_channel(&message, &aad)?; let message = std::str::from_utf8(result.plaintext()).map_err(MessageDecodeError::from)?; @@ -148,8 +149,10 @@ impl SecureChannel { secure_channel } CryptoChannelCreationResult::Hpke(RecipientCreationResult { channel, .. }) => { + let aad = self.channel.additional_authenticated_data().unwrap_or_default(); let BidirectionalCreationResult { channel, message } = - channel.establish_bidirectional_channel(LOGIN_OK_MESSAGE.as_bytes(), &[]); + channel.establish_bidirectional_channel(LOGIN_OK_MESSAGE.as_bytes(), &aad); + self.channel.send(message.encode()).await?; let crypto_channel = EstablishedCryptoChannel::Hpke(channel); @@ -213,7 +216,25 @@ impl EstablishedSecureChannel { let client = HttpClient::new(client, RequestConfig::short_retry()); - // Let's establish an outbound ECIES channel, the other side won't know that + // The other side has crated a rendezvous channel, we're going to connect to it + // and send this initial encrypted message through it. The initial message on + // the rendezvous channel will have an empty body, so we can just + // drop it. + let mut channel = match qr_code_data.intent_data() { + QrCodeIntentData::Msc4108 { rendezvous_url, .. } => { + let InboundChannelCreationResult { channel, .. } = + RendezvousChannel::create_inbound(client, rendezvous_url).await?; + channel + } + QrCodeIntentData::Msc4388 { rendezvous_id, base_url } => { + let InboundChannelCreationResult { channel, .. } = + RendezvousChannel::create_inbound_msc4388(client, base_url, rendezvous_id) + .await?; + channel + } + }; + + // Let's establish an outbound crypto channel, the other side won't know that // it's talking to us, the device that scanned the QR code, until it // receives and successfully decrypts the initial message. We're here encrypting // the `LOGIN_INITIATE_MESSAGE`. @@ -230,35 +251,18 @@ impl EstablishedSecureChannel { (ChannelType::Ecies(ecies), message.encode()) } QrCodeIntentData::Msc4388 { .. } => { + let aad = channel.additional_authenticated_data().unwrap_or_default(); + let SenderCreationResult { channel, message } = HpkeSenderChannel::new() .establish_channel( qr_code_data.public_key(), LOGIN_INITIATE_MESSAGE.as_bytes(), - // TODO: Do we want to include some additional authenticated data here? - &[], + &aad, ); (ChannelType::Hpke(channel), message.encode()) } }; - // The other side has crated a rendezvous channel, we're going to connect to it - // and send this initial encrypted message through it. The initial message on - // the rendezvous channel will have an empty body, so we can just - // drop it. - let mut channel = match qr_code_data.intent_data() { - QrCodeIntentData::Msc4108 { rendezvous_url, .. } => { - let InboundChannelCreationResult { channel, .. } = - RendezvousChannel::create_inbound(client, rendezvous_url).await?; - channel - } - QrCodeIntentData::Msc4388 { rendezvous_id, base_url } => { - let InboundChannelCreationResult { channel, .. } = - RendezvousChannel::create_inbound_msc4388(client, base_url, rendezvous_id) - .await?; - channel - } - }; - trace!( "Received the initial message from the rendezvous channel, sending the LOGIN \ INITIATE message" @@ -281,13 +285,14 @@ impl EstablishedSecureChannel { (response, channel) } ChannelType::Hpke(crypto_channel) => { + let aad = channel.additional_authenticated_data().unwrap_or_default(); let response = channel.receive().await?; let response = InitialResponse::decode(&response).map_err(MessageDecodeError::from)?; let BidirectionalCreationResult { channel: crypto_channel, message } = crypto_channel - .establish_bidirectional_channel(&response, &[]) + .establish_bidirectional_channel(&response, &aad) .map_err(DecryptionError::from)?; let response = String::from_utf8(message) .map_err(|e| MessageDecodeError::from(e.utf8_error()))?; @@ -339,13 +344,19 @@ impl EstablishedSecureChannel { } async fn send(&mut self, message: &str) -> Result<(), Error> { - let message = self.crypto_channel.seal(message, &[]); + let aad = self.channel.additional_authenticated_data().unwrap_or_default(); + + let message = self.crypto_channel.seal(message, &aad); Ok(self.channel.send(message).await?) } async fn receive(&mut self) -> Result { + let aad = self.channel.additional_authenticated_data().unwrap_or_default(); + let message = self.channel.receive().await?; - self.crypto_channel.open(&message, &[]) + let decrypted = self.crypto_channel.open(&message, &aad)?; + + Ok(decrypted) } } From 1452bb21530ad285340d04ece819348bfbc389a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 27 Feb 2026 11:13:52 +0100 Subject: [PATCH 09/29] feat(qr-login): Simplify the check code interface --- bindings/matrix-sdk-ffi/src/qr_code.rs | 4 --- .../src/authentication/oauth/mod.rs | 3 +-- .../src/authentication/oauth/qrcode/grant.rs | 25 ++++++++----------- .../src/authentication/oauth/qrcode/login.rs | 10 +++----- .../src/authentication/oauth/qrcode/mod.rs | 11 ++++---- .../qrcode/secure_channel/crypto_channel.rs | 14 +++++++---- .../oauth/qrcode/secure_channel/mod.rs | 13 +++++----- examples/qr-login/src/main.rs | 5 ++-- 8 files changed, 39 insertions(+), 46 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 6598ea3ce5c..c5b8e0eb433 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -529,8 +529,6 @@ impl From> for QrLoginProgress { match value { LoginProgress::Starting => Self::Starting, LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let check_code = check_code.to_digit(DigitMode::AllowLeadingZero); - Self::EstablishingSecureChannel { check_code, check_code_string: format!("{check_code:02}"), @@ -631,8 +629,6 @@ impl From> for GrantQrLoginProgress { match value { GrantLoginProgress::Starting => Self::Starting, GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let check_code = check_code.to_digit(DigitMode::AllowLeadingZero); - Self::EstablishingSecureChannel { check_code, check_code_string: format!("{check_code:02}"), diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 14eff34730b..8e00a27c568 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -1432,8 +1432,7 @@ impl<'a> LoginWithQrCodeBuilder<'a> { /// match state { /// LoginProgress::Starting | LoginProgress::SyncingSecrets => (), /// LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - /// let code = check_code.to_digit(); - /// println!("Please enter the following code into the other device {code:02}"); + /// println!("Please enter the following code into the other device {check_code:02}"); /// }, /// LoginProgress::WaitingForToken { user_code } => { /// println!("Please use your other device to confirm the log in {user_code}") diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index 8a97542279b..06b875286ee 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -269,7 +269,7 @@ impl<'a> IntoFuture for GrantLoginWithScannedQrCode<'a> { // The other side isn't yet sure that it's talking to the right device, show // a check code so they can confirm. // -- MSC4108 Secure channel setup step 6 - let check_code = channel.check_code().to_owned(); + let check_code = channel.check_code(); self.state .set(GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code })); @@ -420,7 +420,6 @@ mod test { use ruma::{owned_device_id, owned_user_id}; use tokio::sync::oneshot; use tracing::debug; - use vodozemac::hpke::DigitMode; use super::*; use crate::{ @@ -470,9 +469,7 @@ mod test { .expect("Bob should be able to connect the secure channel"); // Let Alice know about the checkcode so she can verify the channel. - check_code_tx - .send(bob.check_code().to_digit(DigitMode::AllowLeadingZero)) - .expect("Bob should be able to send the checkcode"); + check_code_tx.send(bob.check_code()).expect("Bob should be able to send the checkcode"); match behaviour { BobBehaviour::UnexpectedMessageInsteadOfLoginProtocol => { @@ -1032,7 +1029,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -1172,7 +1169,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -1414,7 +1411,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); break; } @@ -1666,7 +1663,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } _ => { @@ -1925,7 +1922,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -2325,7 +2322,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); break; } @@ -2592,7 +2589,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -2868,7 +2865,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } GrantLoginProgress::WaitingForAuth { verification_uri } => { @@ -3112,7 +3109,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit(DigitMode::AllowLeadingZero)) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); break; } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 85614a1908c..b91fcd58429 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -510,7 +510,6 @@ mod test { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::async_test; use serde_json::json; - use vodozemac::{ecies::CheckCode, hpke::DigitMode}; use super::*; use crate::{ @@ -555,7 +554,7 @@ mod test { /// existing device, of the QR login dance. async fn grant_login( alice: SecureChannel, - check_code_receiver: tokio::sync::oneshot::Receiver, + check_code_receiver: tokio::sync::oneshot::Receiver, behavior: AliceBehaviour, ) { let alice = alice.connect().await.expect("Alice should be able to connect the channel"); @@ -563,9 +562,8 @@ mod test { let check_code = check_code_receiver.await.expect("We should receive the check code from bob"); - let mut alice = alice - .confirm(check_code.to_digit(DigitMode::AllowLeadingZero)) - .expect("Alice should be able to confirm the secure channel"); + let mut alice = + alice.confirm(check_code).expect("Alice should be able to confirm the secure channel"); let message = alice .receive_json() @@ -725,7 +723,7 @@ mod test { // The other side isn't yet sure that it's talking to the right device, show // a check code so they can confirm. - let check_code = channel.check_code().to_digit(DigitMode::AllowLeadingZero); + let check_code = channel.check_code(); let check_code_sender = cctx_receiver.await.expect("Alice should receive the CheckCodeSender"); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 87a04af8168..4849fa91631 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -37,7 +37,6 @@ use ruma::api::error::{ErrorKind, FromHttpResponseError}; use thiserror::Error; use tokio::sync::Mutex; use url::Url; -use vodozemac::ecies::CheckCode; pub use vodozemac::{ ecies::{Error as EciesError, MessageDecodeError as EciesMessageDecodeError}, hpke::{Error as HpkeError, MessageDecodeError as HpkeMessageDecodeError}, @@ -345,12 +344,12 @@ pub enum SecureChannelError { /// this device is the one scanning the QR code. /// /// We have established the secure channel, but we need to let the other -/// side know about the [`CheckCode`] so they can verify that the secure +/// side know about the check code so they can verify that the secure /// channel is indeed secure. #[derive(Clone, Debug)] pub struct QrProgress { /// The check code we need to, out of band, send to the other device. - pub check_code: CheckCode, + pub check_code: u8, } /// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] and @@ -359,7 +358,7 @@ pub struct QrProgress { /// /// We have established the secure channel, but we need to let the /// other device know about the [`QrCodeData`] so they can connect to the -/// channel and let us know about the checkcode so we can verify that the +/// channel and let us know about the check code so we can verify that the /// channel is indeed secure. #[derive(Clone, Debug)] pub enum GeneratedQrProgress { @@ -367,7 +366,7 @@ pub enum GeneratedQrProgress { /// device to scan it. QrReady(QrCodeData), /// The QR code has been scanned by the other device and this device is - /// waiting for the user to put in the checkcode displayed on the + /// waiting for the user to put in the check code displayed on the /// other device. QrScanned(CheckCodeSender), } @@ -384,7 +383,7 @@ impl CheckCodeSender { Self { inner: Arc::new(Mutex::new(Some(tx))) } } - /// Send the checkcode. + /// Send the check code. /// /// Calling this method more than once will result in an error. /// diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs index dd25544604d..281b8263cc8 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs @@ -30,8 +30,10 @@ use vodozemac::{ Curve25519PublicKey, - ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message}, - hpke::{self, EstablishedHpkeChannel, HpkeRecipientChannel, RecipientCreationResult}, + ecies::{Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message}, + hpke::{ + self, DigitMode, EstablishedHpkeChannel, HpkeRecipientChannel, RecipientCreationResult, + }, }; use crate::authentication::oauth::qrcode::{ @@ -115,11 +117,13 @@ pub(super) enum EstablishedCryptoChannel { impl EstablishedCryptoChannel { /// Get the [`CheckCode`] of this [`EstablishedCryptoChannel`]. - pub(super) fn check_code(&self) -> &CheckCode { + pub(super) fn check_code(&self) -> u8 { match self { - EstablishedCryptoChannel::Ecies(established_ecies) => established_ecies.check_code(), + EstablishedCryptoChannel::Ecies(established_ecies) => { + established_ecies.check_code().to_digit(DigitMode::AllowLeadingZero) + } EstablishedCryptoChannel::Hpke(established_hpke_channel) => { - established_hpke_channel.check_code() + established_hpke_channel.check_code().to_digit(DigitMode::NoLeadingZero) } } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 19c3b4df6bf..5c14c4defbc 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -20,10 +20,10 @@ use serde::{Serialize, de::DeserializeOwned}; use tracing::{instrument, trace}; use url::Url; use vodozemac::{ - ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult}, + ecies::{Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult}, hpke::{ - BidirectionalCreationResult, DigitMode, HpkeSenderChannel, InitialResponse, - RecipientCreationResult, SenderCreationResult, UnidirectionalSenderChannel, + BidirectionalCreationResult, HpkeSenderChannel, InitialResponse, RecipientCreationResult, + SenderCreationResult, UnidirectionalSenderChannel, }, }; @@ -183,7 +183,7 @@ impl AlmostEstablishedSecureChannel { /// The check code needs to be received out of band from the other side of /// the secure channel. pub(super) fn confirm(self, check_code: u8) -> Result { - if check_code == self.secure_channel.check_code().to_digit(DigitMode::AllowLeadingZero) { + if check_code == self.secure_channel.check_code() { Ok(self.secure_channel) } else { Err(Error::InvalidCheckCode) @@ -321,7 +321,7 @@ impl EstablishedSecureChannel { /// Get the [`CheckCode`] which can be used to, out of band, verify that /// both sides of the channel are indeed communicating with each other and /// not with a 3rd party. - pub(super) fn check_code(&self) -> &CheckCode { + pub(super) fn check_code(&self) -> u8 { self.crypto_channel.check_code() } @@ -379,7 +379,6 @@ pub(super) mod test { use similar_asserts::assert_eq; use tracing::trace; use url::Url; - use vodozemac::hpke::DigitMode; use wiremock::{ Mock, MockGuard, MockServer, ResponseTemplate, matchers::{method, path}, @@ -698,7 +697,7 @@ pub(super) mod test { assert_eq!(alice.secure_channel.check_code(), bob.check_code()); let alice = alice - .confirm(bob.check_code().to_digit(DigitMode::AllowLeadingZero)) + .confirm(bob.check_code()) .expect("Alice should be able to confirm the established secure channel."); assert_eq!(bob.channel.rendezvous_info(), alice.channel.rendezvous_info()); diff --git a/examples/qr-login/src/main.rs b/examples/qr-login/src/main.rs index 84a521c2c7e..39e93e10d28 100644 --- a/examples/qr-login/src/main.rs +++ b/examples/qr-login/src/main.rs @@ -125,8 +125,9 @@ async fn login(proxy: Option) -> Result<()> { match state { LoginProgress::Starting | LoginProgress::SyncingSecrets => (), LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let code = check_code.to_digit(); - println!("Please enter the following code into the other device {code:02}"); + println!( + "Please enter the following code into the other device {check_code:02}" + ); } LoginProgress::WaitingForToken { user_code } => { println!("Please use your other device to confirm the log in {user_code}") From 7da888234bbab89715837aa35c7d328804325d09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 17 Mar 2026 13:08:42 +0100 Subject: [PATCH 10/29] feat(qr-login): Add a method to check if the server supports MSC4388 style QR login --- .../src/authentication/oauth/mod.rs | 26 +++++++ .../src/authentication/oauth/qrcode/mod.rs | 77 +++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 8e00a27c568..7548aeccd9a 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -370,6 +370,32 @@ impl OAuth { as_variant!(data, AuthData::OAuth) } + /// Check if the homeserver supports the [MSC4388] variant of the rendezvous + /// server. + /// + /// Returns `Ok(true)` if the rendezvous discovery endpoint returns a 200 OK + /// HTTP response, `Ok(false)` if the endpoint returns a 404 NOT_FOUND + /// HTTP response, otherwise an error is returned. + /// + /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388 + #[cfg(feature = "e2e-encryption")] + pub async fn msc_4388_rendezvous_server_supported(&self) -> Result { + use ruma::api::client::rendezvous::discover_rendezvous; + + match self.client.send(discover_rendezvous::unstable::Request::new()).await { + Ok(_) => Ok(true), + Err(e) => { + if e.as_client_api_error() + .is_some_and(|err| err.status_code == http::StatusCode::NOT_FOUND) + { + Ok(false) + } else { + Err(e) + } + } + } + } + /// Log in this device using a QR code. /// /// # Arguments diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 4849fa91631..4f951fb571f 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -408,3 +408,80 @@ pub enum CheckCodeSenderError { #[error("check code cannot be sent.")] CannotSend, } + +#[cfg(test)] +mod tests { + use matrix_sdk_test::async_test; + use wiremock::{ + Mock, ResponseTemplate, + matchers::{method, path}, + }; + + use crate::test_utils::mocks::MatrixMockServer; + + #[async_test] + async fn test_msc_4388_rendezvous_server_supported() { + const URL: &str = "/_matrix/client/unstable/io.element.msc4388/rendezvous"; + + let server = MatrixMockServer::new().await; + let client = server.client_builder().logged_in_with_oauth().build().await; + + { + let _discover_guard = server + .server() + .register_as_scoped( + Mock::given(method("GET")) + .and(path(URL)) + .respond_with(ResponseTemplate::new(200)) + .expect(1), + ) + .await; + + let supported = client + .oauth() + .msc_4388_rendezvous_server_supported() + .await + .expect("We should be able to check if the rendezvous server is supported"); + + assert!(supported, "The rendezvous server should be supported"); + } + + { + let _discover_guard = server + .server() + .register_as_scoped( + Mock::given(method("GET")) + .and(path(URL)) + .respond_with(ResponseTemplate::new(404)) + .expect(1), + ) + .await; + + let supported = client + .oauth() + .msc_4388_rendezvous_server_supported() + .await + .expect("We should be able to check if the rendezvous server is supported"); + + assert!(!supported, "The rendezvous server should not be supported"); + } + + { + let _discover_guard = server + .server() + .register_as_scoped( + Mock::given(method("GET")) + .and(path(URL)) + .respond_with(ResponseTemplate::new(500)) + .expect(1), + ) + .await; + + client + .oauth() + .msc_4388_rendezvous_server_supported() + .await + .expect_err("We should return an error if the homeserver can't tell us if the endpoint is supported or not"); + } + } +} From f2487387a403a65228a09a40f28fc292146b009a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 17 Mar 2026 13:08:42 +0100 Subject: [PATCH 11/29] feat(qr-login): Use the old QR login variant if the homeserver does not support the new one --- .../src/authentication/oauth/qrcode/grant.rs | 29 +++++++++++++------ .../src/authentication/oauth/qrcode/login.rs | 16 ++++++---- .../oauth/qrcode/secure_channel/mod.rs | 11 +++++++ 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index 06b875286ee..eae29621794 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -352,6 +352,18 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { fn into_future(self) -> Self::IntoFuture { Box::pin(async move { + let Self { client, device_creation_timeout, state, msc_4388_support } = self; + let rendezvous_server_supported = async { + client + .oauth() + .msc_4388_rendezvous_server_supported() + .await + .map_err(SecureChannelError::from) + }; + + // If MSC4388 support is enabled and the server supports it, then prefer it. + let use_msc_4388 = msc_4388_support && rendezvous_server_supported.await?; + // Create a new ephemeral key pair and a rendezvous session to grant a // login with. // -- MSC4108 Secure channel setup steps 1 & 2 @@ -359,15 +371,14 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { let http_client = self.client.inner.http_client.clone(); let secrets_bundle = export_secrets_bundle(self.client).await?; let channel = - SecureChannel::reciprocate(http_client, &homeserver_url, self.msc_4388_support) - .await?; + SecureChannel::reciprocate(http_client, &homeserver_url, use_msc_4388).await?; // Extract the QR code data and emit an update so that the caller can // present the QR code for scanning by the new device. // -- MSC4108 Secure channel setup step 3 - self.state.set(GrantLoginProgress::EstablishingSecureChannel( - GeneratedQrProgress::QrReady(channel.qr_code_data().clone()), - )); + state.set(GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady( + channel.qr_code_data().clone(), + ))); // Wait for the secure channel to connect. The other device now needs to scan // the QR code and send us the LoginInitiateMessage which we respond to @@ -379,7 +390,7 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { // user to enter the checkcode and feed it back to us. // -- MSC4108 Secure channel setup step 6 let (tx, rx) = tokio::sync::oneshot::channel(); - self.state.set(GrantLoginProgress::EstablishingSecureChannel( + state.set(GrantLoginProgress::EstablishingSecureChannel( GeneratedQrProgress::QrScanned(CheckCodeSender::new(tx)), )); let check_code = rx.await.map_err(|_| SecureChannelError::CannotReceiveCheckCode)?; @@ -396,11 +407,11 @@ impl<'a> IntoFuture for GrantLoginWithGeneratedQrCode<'a> { // Proceed with granting the login. // -- MSC4108 OAuth 2.0 login remaining steps finish_login_grant( - self.client, + client, &mut channel, - self.device_creation_timeout, + device_creation_timeout, &secrets_bundle, - &self.state, + &state, ) .await }) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index b91fcd58429..8a236784b89 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -455,21 +455,27 @@ impl<'a> LoginWithGeneratedQrCode<'a> { async fn establish_secure_channel( &self, ) -> Result { - let http_client = self.client.inner.http_client.clone(); + let Self { client, registration_data: _, state, msc_4388_support } = self; + + let http_client = client.inner.http_client.clone(); + let rendezvous_server_supported = + async { client.oauth().msc_4388_rendezvous_server_supported().await }; + + // If MSC4388 support is enabled and the server supports it, then prefer it. + let use_msc_4388 = *msc_4388_support && rendezvous_server_supported.await?; // Create a new ephemeral key pair and a rendezvous session to request a login // with. // -- MSC4108 Secure channel setup steps 1 & 2 let secure_channel = - SecureChannel::login(http_client, &self.client.homeserver(), self.msc_4388_support) - .await?; + SecureChannel::login(http_client, &client.homeserver(), use_msc_4388).await?; // Extract the QR code data and emit a progress update so that the caller can // present the QR code for scanning by the other device. // -- MSC4108 Secure channel setup step 3 let qr_code_data = secure_channel.qr_code_data().clone(); trace!("Generated QR code."); - self.state.set(LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady( + state.set(LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrReady( qr_code_data, ))); @@ -484,7 +490,7 @@ impl<'a> LoginWithGeneratedQrCode<'a> { // -- MSC4108 Secure channel setup step 6 trace!("Waiting for checkcode."); let (tx, rx) = tokio::sync::oneshot::channel(); - self.state.set(LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( + state.set(LoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( CheckCodeSender::new(tx), ))); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 5c14c4defbc..97a702e58a9 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -398,6 +398,7 @@ pub(super) mod test { post_guard: MockGuard, put_guard: MockGuard, get_guard: MockGuard, + discover_guard: Option, } impl MockedRendezvousServer { @@ -533,6 +534,7 @@ pub(super) mod test { get_guard, homeserver_url, rendezvous_url, + discover_guard: None, } } @@ -575,6 +577,14 @@ pub(super) mod test { ) .await; + let discover_guard = server + .register_as_scoped( + Mock::given(method("GET")) + .and(path("/_matrix/client/unstable/io.element.msc4388/rendezvous")) + .respond_with(ResponseTemplate::new(200)), + ) + .await; + let put_guard = server .register_as_scoped( Mock::given(method("PUT")) @@ -658,6 +668,7 @@ pub(super) mod test { get_guard, homeserver_url, rendezvous_url, + discover_guard: Some(discover_guard), } } } From 6a53c6462410a24bfc0be777e9ffb1d1a9126add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 17 Mar 2026 13:08:42 +0100 Subject: [PATCH 12/29] feat(ffi): Check for QR login support using the new discovery endpoint as well --- bindings/matrix-sdk-ffi/src/client.rs | 46 +++++++++++++++++++-------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 020b2409b0e..f03a036092a 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -75,16 +75,10 @@ use matrix_sdk_ui::{ use mime::Mime; use oauth2::Scope; use ruma::{ - OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName, - api::{ - client::{ - alias::get_alias, - discovery::get_authorization_server_metadata::v1::{ - AccountManagementActionData, DeviceDeleteData, DeviceViewData, - }, - profile::{AvatarUrl, DisplayName}, - room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent}, - uiaa::{EmailUserIdentifier, UserIdentifier}, + api::client::{ + alias::get_alias, + discovery::get_authorization_server_metadata::v1::{ + AccountManagementActionData, DeviceDeleteData, DeviceViewData, GrantType, }, error::ErrorKind, }, @@ -1976,10 +1970,36 @@ impl Client { .any(|focus| matches!(focus, RtcFocusInfo::LiveKit(_)))) } - /// Checks if the server supports login using a QR code. + /// Checks if this client will be able to log in another client using a QR + /// code. + /// + /// This checks if OAuth 2.0 was used to log in this client and if the auth + /// and homeserver support all the necessary APIs for the QR code login + /// to work. pub async fn is_login_with_qr_code_supported(&self) -> Result { - Ok(matches!(self.inner.auth_api(), Some(AuthApi::OAuth(_))) - && self.inner.unstable_features().await?.contains(&ruma::api::FeatureFlag::Msc4108)) + // We need to be using OAuth 2.0 API + Device Authorization Grant available + // and either MSC4108 or MSC4388 available. + + // We need to be using the OAuth 2.0 API + if !matches!(self.inner.auth_api(), Some(AuthApi::OAuth(_))) { + return Ok(false); + } + + let metadata = + self.inner.oauth().cached_server_metadata().await.map_err(ClientError::from_err)?; + + // We need the Device Authorization Grant available + if !metadata.grant_types_supported.contains(&GrantType::DeviceCode) { + return Ok(false); + } + + // We can use MSC4388 (from MSC4108 version 2025) if available: + if self.inner.oauth().msc_4388_rendezvous_server_supported().await? { + Ok(true) + } else { + // Otherwise we can use MSC4108 version 2024: + Ok(self.inner.unstable_features().await?.contains(&ruma::api::FeatureFlag::Msc4108)) + } } /// Get server vendor information from the federation API. From b5a51aa894777336e5d6e15d3d83744cd4f25083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 17 Mar 2026 13:08:42 +0100 Subject: [PATCH 13/29] feat(ffi): Request the MSC4388 variant of the QR code login by default --- bindings/matrix-sdk-ffi/src/qr_code.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index c5b8e0eb433..3d58c836bc6 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -120,7 +120,8 @@ impl LoginWithQrCodeHandler { .registration_data() .map_err(|_| HumanQrLoginError::OAuthMetadataInvalid)?; - let login = self.oauth.login_with_qr_code(Some(®istration_data)).generate(); + let mut login = self.oauth.login_with_qr_code(Some(®istration_data)).generate(); + login.with_msc4388_support(); let mut progress = login.subscribe_to_progress(); @@ -214,7 +215,8 @@ impl GrantLoginWithQrCodeHandler { self: Arc, progress_listener: Box, ) -> Result<(), HumanQrGrantLoginError> { - let grant = self.oauth.grant_login_with_qr_code().generate(); + let mut grant = self.oauth.grant_login_with_qr_code().generate(); + grant.with_msc4388_support(); let mut progress = grant.subscribe_to_progress(); From ccfa1ec3d077b836655cd2167c9a356903be4a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 18 Mar 2026 13:36:27 +0100 Subject: [PATCH 14/29] chore: Enable the wasm_js feature for yet another getrandom version --- crates/matrix-sdk-common/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 92500766ada..d0fd06949cf 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -53,6 +53,7 @@ tracing-subscriber = { workspace = true, features = ["fmt", "ansi"] } wasm-bindgen.workspace = true wasm-bindgen-futures = { version = "0.4.33", optional = true } web-sys = { workspace = true, features = ["console"] } +getrandom03 = { package = "getrandom", version = "0.3.4", default-features = false, features = ["wasm_js"] } [dev-dependencies] assert_matches.workspace = true From 8f0f7da07f2b3f5d20bd3a812a19e4739dfead21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 18 Mar 2026 14:13:47 +0100 Subject: [PATCH 15/29] chore: Bump vodozemac --- .../src/authentication/oauth/qrcode/secure_channel/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 97a702e58a9..0533d6930d2 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -258,7 +258,8 @@ impl EstablishedSecureChannel { qr_code_data.public_key(), LOGIN_INITIATE_MESSAGE.as_bytes(), &aad, - ); + ) + .map_err(DecryptionError::from)?; (ChannelType::Hpke(channel), message.encode()) } }; From b5593873d35c2f9b7cc5bd036484b9ae316d154c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 18 Mar 2026 16:34:35 +0100 Subject: [PATCH 16/29] fix(qrlogin): Return false upon a 403 response when we check for MSC4388 rendezvous support --- .../src/authentication/oauth/mod.rs | 11 ++++---- .../src/authentication/oauth/qrcode/mod.rs | 28 ++++++++++++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 7548aeccd9a..ca92b69dfd6 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -374,20 +374,21 @@ impl OAuth { /// server. /// /// Returns `Ok(true)` if the rendezvous discovery endpoint returns a 200 OK - /// HTTP response, `Ok(false)` if the endpoint returns a 404 NOT_FOUND - /// HTTP response, otherwise an error is returned. + /// HTTP response, `Ok(false)` if the endpoint returns a 404 NOT_FOUND or + /// 403 FORBIDDEN HTTP response, otherwise an error is returned. /// /// [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388 #[cfg(feature = "e2e-encryption")] pub async fn msc_4388_rendezvous_server_supported(&self) -> Result { + use http::StatusCode; use ruma::api::client::rendezvous::discover_rendezvous; match self.client.send(discover_rendezvous::unstable::Request::new()).await { Ok(_) => Ok(true), Err(e) => { - if e.as_client_api_error() - .is_some_and(|err| err.status_code == http::StatusCode::NOT_FOUND) - { + if e.as_client_api_error().is_some_and(|err| { + matches!(err.status_code, StatusCode::NOT_FOUND | StatusCode::FORBIDDEN) + }) { Ok(false) } else { Err(e) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 4f951fb571f..f60d3c7398d 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -463,7 +463,33 @@ mod tests { .await .expect("We should be able to check if the rendezvous server is supported"); - assert!(!supported, "The rendezvous server should not be supported"); + assert!( + !supported, + "The rendezvous server should not be supported if we receive a 404 response" + ); + } + + { + let _discover_guard = server + .server() + .register_as_scoped( + Mock::given(method("GET")) + .and(path(URL)) + .respond_with(ResponseTemplate::new(403)) + .expect(1), + ) + .await; + + let supported = client + .oauth() + .msc_4388_rendezvous_server_supported() + .await + .expect("We should be able to check if the rendezvous server is supported"); + + assert!( + !supported, + "The rendezvous server should not be supported if we receive a 403 response" + ); } { From 3fa8dc078b88331c1702c571ef08f1a10a8c33d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 19 Mar 2026 12:24:50 +0100 Subject: [PATCH 17/29] fix(qr-login): Support the new variant of the m.login.protocols message --- .../src/authentication/oauth/qrcode/grant.rs | 46 ++++++++++--- .../src/authentication/oauth/qrcode/login.rs | 46 +++++++++++-- .../authentication/oauth/qrcode/messages.rs | 66 ++++++++++++++++--- .../oauth/qrcode/secure_channel/mod.rs | 7 ++ 4 files changed, 142 insertions(+), 23 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index eae29621794..6653f3940e4 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -37,7 +37,7 @@ use crate::{ Client, authentication::oauth::qrcode::{ CheckCodeSender, GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError, - QrProgress, SecureChannelError, + QrProgress, SecureChannelError, messages::LoginProtocolsMessage, }, }; @@ -280,9 +280,16 @@ impl<'a> IntoFuture for GrantLoginWithScannedQrCode<'a> { // Inform the other device about the available login protocols and the // homeserver to use. // -- MSC4108 OAuth 2.0 login step 1 - let message = QrAuthMessage::LoginProtocols { - protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], - homeserver: self.client.homeserver(), + let message = if channel.is_using_msc_4388() { + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4388 { + protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], + base_url: self.client.homeserver(), + }) + } else { + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4108 { + protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], + homeserver: self.client.homeserver(), + }) }; channel.send_json(message).await?; @@ -424,7 +431,7 @@ mod test { use assert_matches2::{assert_let, assert_matches}; use futures_util::StreamExt; - use matrix_sdk_base::crypto::types::SecretsBundle; + use matrix_sdk_base::crypto::types::{SecretsBundle, qr_login::QrCodeIntentData}; use matrix_sdk_common::executor::spawn; use matrix_sdk_test::async_test; use oauth2::{EndUserVerificationUrl, VerificationUriComplete}; @@ -636,6 +643,11 @@ mod test { device_authorization_grant: Option, secrets_bundle: Option, ) { + let is_using_msc_4388 = match channel.qr_code_data().intent_data() { + QrCodeIntentData::Msc4108 { .. } => false, + QrCodeIntentData::Msc4388 { .. } => true, + }; + // Wait for Alice to scan the qr code and connect the secure channel. let channel = channel.connect().await.expect("Bob should be able to connect the secure channel"); @@ -651,9 +663,27 @@ mod test { .receive_json() .await .expect("Bob should receive the LoginProtocolAccepted message from Alice"); - assert_let!( - QrAuthMessage::LoginProtocols { protocols, homeserver: alice_homeserver } = message - ); + + let (protocols, alice_homeserver) = if is_using_msc_4388 { + assert_let!( + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4388 { + protocols, + base_url, + }) = message + ); + + (protocols, base_url) + } else { + assert_let!( + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4108 { + protocols, + homeserver, + }) = message + ); + + (protocols, homeserver) + }; + assert_eq!(protocols, vec![LoginProtocolType::DeviceAuthorizationGrant]); assert_eq!(alice_homeserver, homeserver); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 8a236784b89..a8b80f5db62 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs @@ -40,7 +40,10 @@ use crate::{ Client, authentication::oauth::{ ClientRegistrationData, OAuth, OAuthError, - qrcode::{CheckCodeSender, GeneratedQrProgress, LoginProtocolType, QrProgress}, + qrcode::{ + CheckCodeSender, GeneratedQrProgress, LoginProtocolType, QrProgress, + messages::LoginProtocolsMessage, + }, }, }; @@ -401,7 +404,10 @@ impl<'a> IntoFuture for LoginWithGeneratedQrCode<'a> { // Verify that the device authorization grant is supported and extract // the homeserver URL. let homeserver = match message { - QrAuthMessage::LoginProtocols { protocols, homeserver } => { + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4108 { + protocols, + homeserver, + }) if !channel.is_using_msc_4388() => { if !protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant) { channel .send_json(QrAuthMessage::LoginFailure { @@ -418,6 +424,27 @@ impl<'a> IntoFuture for LoginWithGeneratedQrCode<'a> { homeserver } + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4388 { + protocols, + base_url, + }) if channel.is_using_msc_4388() => { + if !protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant) { + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::UnsupportedProtocol, + homeserver: None, + }) + .await?; + + return Err(QRCodeLoginError::LoginFailure { + reason: LoginFailureReason::UnsupportedProtocol, + homeserver: None, + }); + } + + base_url + } + _ => { send_unexpected_message_error(&mut channel).await?; @@ -520,7 +547,7 @@ mod test { use super::*; use crate::{ authentication::oauth::qrcode::{ - messages::LoginProtocolType, + messages::{LoginProtocolType, LoginProtocolsMessage}, secure_channel::{SecureChannel, test::MockedRendezvousServer}, }, config::RequestConfig, @@ -740,9 +767,16 @@ mod test { .expect("Alice should be able to send the check code to Bob"); // Alice sends m.login.protocols message - let message = QrAuthMessage::LoginProtocols { - protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], - homeserver: alice.homeserver(), + let message = if channel.is_using_msc_4388() { + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4388 { + protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], + base_url: alice.homeserver(), + }) + } else { + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4108 { + protocols: vec![LoginProtocolType::DeviceAuthorizationGrant], + homeserver: alice.homeserver(), + }) }; channel .send_json(message) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs index a65aa789d2c..af2e2866e8d 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs @@ -33,12 +33,7 @@ pub enum QrAuthMessage { /// Message declaring the available protocols for sign in. Sent by the /// existing device. #[serde(rename = "m.login.protocols")] - LoginProtocols { - /// The login protocols the existing device supports. - protocols: Vec, - /// The homeserver we're going to log in to. - homeserver: Url, - }, + LoginProtocols(LoginProtocolsMessage), /// Message declaring which protocols from the previous `m.login.protocols` /// message the new device has picked. Sent by the new device. @@ -88,6 +83,28 @@ pub enum QrAuthMessage { LoginSecrets(SecretsBundle), } +/// Message declaring the available protocols for sign in. Sent by the +/// existing device. +/// +/// Supports both the MSC4108 variant of the m.login.protocols message as well +/// as the MSC4388 variant. +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum LoginProtocolsMessage { + Msc4388 { + /// The login protocols the existing device supports. + protocols: Vec, + /// The homeserver we're going to log in to. + base_url: Url, + }, + Msc4108 { + /// The login protocols the existing device supports. + protocols: Vec, + /// The homeserver we're going to log in to. + homeserver: Url, + }, +} + impl QrAuthMessage { /// Create a new [`QrAuthMessage::LoginProtocol`] message with the /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol type. @@ -170,17 +187,48 @@ mod test { use super::*; #[test] - fn test_protocols_serialization() { + fn test_protocols_serialization_msc_4108() { + const HOMESERVER: &str = "https://matrix-client.matrix.org/"; + let json = json!({ "type": "m.login.protocols", "protocols": ["device_authorization_grant"], - "homeserver": "https://matrix-client.matrix.org/" + "homeserver": HOMESERVER, }); let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap(); - assert_let!(QrAuthMessage::LoginProtocols { protocols, .. } = &message); + assert_let!( + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4108 { + protocols, + homeserver + }) = &message + ); + assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant)); + assert_eq!(homeserver.as_str(), HOMESERVER); + + let serialized = serde_json::to_value(&message).unwrap(); + assert_eq!(json, serialized); + } + + #[test] + fn test_protocols_serialization_msc_4388() { + const HOMESERVER: &str = "https://matrix-client.matrix.org/"; + + let json = json!({ + "type": "m.login.protocols", + "protocols": ["device_authorization_grant"], + "base_url": HOMESERVER, + + }); + + let message: QrAuthMessage = serde_json::from_value(json.clone()).unwrap(); + assert_let!( + QrAuthMessage::LoginProtocols(LoginProtocolsMessage::Msc4388 { protocols, base_url }) = + &message + ); assert!(protocols.contains(&LoginProtocolType::DeviceAuthorizationGrant)); + assert_eq!(base_url.as_str(), HOMESERVER); let serialized = serde_json::to_value(&message).unwrap(); assert_eq!(json, serialized); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 0533d6930d2..c41399ea157 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -319,6 +319,13 @@ impl EstablishedSecureChannel { } } + pub(super) fn is_using_msc_4388(&self) -> bool { + match &self.channel { + RendezvousChannel::Msc4108(_) => false, + RendezvousChannel::Msc4388(_) => true, + } + } + /// Get the [`CheckCode`] which can be used to, out of band, verify that /// both sides of the channel are indeed communicating with each other and /// not with a 3rd party. From a442a95f68cc85b68006b1e8d0dd329cc56043ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 19 Mar 2026 12:24:50 +0100 Subject: [PATCH 18/29] chore: Bump ruma --- .../src/authentication/oauth/mod.rs | 2 +- .../src/authentication/oauth/qrcode/mod.rs | 30 ++++++++++++++++++- .../oauth/qrcode/secure_channel/mod.rs | 4 ++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index ca92b69dfd6..915d6743444 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -384,7 +384,7 @@ impl OAuth { use ruma::api::client::rendezvous::discover_rendezvous; match self.client.send(discover_rendezvous::unstable::Request::new()).await { - Ok(_) => Ok(true), + Ok(response) => Ok(response.create_available), Err(e) => { if e.as_client_api_error().is_some_and(|err| { matches!(err.status_code, StatusCode::NOT_FOUND | StatusCode::FORBIDDEN) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index f60d3c7398d..8aea72e4f56 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -412,6 +412,7 @@ pub enum CheckCodeSenderError { #[cfg(test)] mod tests { use matrix_sdk_test::async_test; + use serde_json::json; use wiremock::{ Mock, ResponseTemplate, matchers::{method, path}, @@ -432,7 +433,9 @@ mod tests { .register_as_scoped( Mock::given(method("GET")) .and(path(URL)) - .respond_with(ResponseTemplate::new(200)) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "create_available": true, + }))) .expect(1), ) .await; @@ -446,6 +449,31 @@ mod tests { assert!(supported, "The rendezvous server should be supported"); } + { + let _discover_guard = server + .server() + .register_as_scoped( + Mock::given(method("GET")) + .and(path(URL)) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "create_available": false, + }))) + .expect(1), + ) + .await; + + let supported = client + .oauth() + .msc_4388_rendezvous_server_supported() + .await + .expect("We should be able to check if the rendezvous server is supported"); + + assert!( + !supported, + "The rendezvous server should not be supported, because create_available is false" + ); + } + { let _discover_guard = server .server() diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index c41399ea157..a0f2dcebd97 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -589,7 +589,9 @@ pub(super) mod test { .register_as_scoped( Mock::given(method("GET")) .and(path("/_matrix/client/unstable/io.element.msc4388/rendezvous")) - .respond_with(ResponseTemplate::new(200)), + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "create_available": true, + }))), ) .await; From 0054d355edce42560954c91e74ddb57d28185a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Fri, 20 Mar 2026 12:24:43 +0100 Subject: [PATCH 19/29] chore(qr-login): Add a note about an implementation mistake for MSC4108 qr login messages --- crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs index af2e2866e8d..10fb929ad15 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs @@ -101,6 +101,9 @@ pub enum LoginProtocolsMessage { /// The login protocols the existing device supports. protocols: Vec, /// The homeserver we're going to log in to. + /// + /// Note: this doesn't match the MSC which says that it is a server name + /// not a full URL homeserver: Url, }, } From b2f7b435c0ebbf1e8b5829130c8958676d76a135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 29 Apr 2026 11:23:24 +0200 Subject: [PATCH 20/29] refactor(qr-code): Make the checkcode sender a type alias over a generic sender --- bindings/matrix-sdk-ffi/src/qr_code.rs | 12 ++++---- .../src/authentication/oauth/qrcode/mod.rs | 30 +++++++++++-------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 3d58c836bc6..aa29890f25d 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -17,8 +17,8 @@ use std::sync::Arc; use matrix_sdk::authentication::oauth::{ OAuth, qrcode::{ - self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError, - DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress, + self, CheckCodeSender as SdkCheckCodeSender, DeviceCodeErrorResponseType, + GeneratedQrProgress, LoginFailureReason, QrProgress, SenderError, }, }; use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent}; @@ -391,11 +391,11 @@ impl From for HumanQrLoginError { } } -impl From for HumanQrLoginError { - fn from(value: CheckCodeSenderError) -> Self { +impl From for HumanQrLoginError { + fn from(value: SenderError) -> Self { match value { - CheckCodeSenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent, - CheckCodeSenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent, + SenderError::AlreadySent => HumanQrLoginError::CheckCodeAlreadySent, + SenderError::CannotSend => HumanQrLoginError::CheckCodeCannotBeSent, } } } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 8aea72e4f56..6923a40f36d 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -373,16 +373,9 @@ pub enum GeneratedQrProgress { /// Used to pass back the checkcode entered by the user to verify that the /// secure channel is indeed secure. -#[derive(Clone, Debug)] -pub struct CheckCodeSender { - inner: Arc>>>, -} +pub type CheckCodeSender = CloneableSender; impl CheckCodeSender { - pub(crate) fn new(tx: tokio::sync::oneshot::Sender) -> Self { - Self { inner: Arc::new(Mutex::new(Some(tx))) } - } - /// Send the check code. /// /// Calling this method more than once will result in an error. @@ -390,17 +383,30 @@ impl CheckCodeSender { /// # Arguments /// /// * `check_code` - The check code in digits representation. - pub async fn send(&self, check_code: u8) -> Result<(), CheckCodeSenderError> { + pub async fn send(&self, check_code: u8) -> Result<(), SenderError> { match self.inner.lock().await.take() { - Some(tx) => tx.send(check_code).map_err(|_| CheckCodeSenderError::CannotSend), - None => Err(CheckCodeSenderError::AlreadySent), + Some(tx) => tx.send(check_code).map_err(|_| SenderError::CannotSend), + None => Err(SenderError::AlreadySent), } } } +/// A oneshot sender we are able to clone so we can put it into a +/// [`SharedObservable`]. +#[derive(Clone, Debug)] +pub struct CloneableSender { + inner: Arc>>>, +} + +impl CloneableSender { + pub(crate) fn new(tx: tokio::sync::oneshot::Sender) -> Self { + Self { inner: Arc::new(Mutex::new(Some(tx))) } + } +} + /// Possible errors when calling [`CheckCodeSender::send`]. #[derive(Debug, thiserror::Error)] -pub enum CheckCodeSenderError { +pub enum SenderError { /// The check code has already been sent. #[error("check code already sent.")] AlreadySent, From 3a6c47b736cf65f8103f8ff9bafd07c12e1954e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 Apr 2026 16:32:27 +0200 Subject: [PATCH 21/29] Let the user confirm that they are done in the browser when doing qr code login granting --- bindings/matrix-sdk-ffi/src/qr_code.rs | 43 +++++++-- .../src/authentication/oauth/qrcode/grant.rs | 90 ++++++++++++++++--- .../authentication/oauth/qrcode/messages.rs | 3 + .../src/authentication/oauth/qrcode/mod.rs | 31 ++++++- 4 files changed, 145 insertions(+), 22 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index aa29890f25d..34aac4c4355 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -17,7 +17,8 @@ use std::sync::Arc; use matrix_sdk::authentication::oauth::{ OAuth, qrcode::{ - self, CheckCodeSender as SdkCheckCodeSender, DeviceCodeErrorResponseType, + self, CheckCodeSender as SdkCheckCodeSender, + ContinuationMessageSender as SdkContinuationMessageSender, DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress, SenderError, }, }; @@ -612,6 +613,7 @@ pub enum GrantQrLoginProgress { WaitingForAuth { /// A URI to open in a (secure) system browser to verify the new login. verification_uri: String, + continuation_sender: Arc, }, /// We are syncing secrets. SyncingSecrets, @@ -636,8 +638,11 @@ impl From> for GrantQrLoginProgress { check_code_string: format!("{check_code:02}"), } } - GrantLoginProgress::WaitingForAuth { verification_uri } => { - Self::WaitingForAuth { verification_uri: verification_uri.into() } + GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => { + Self::WaitingForAuth { + verification_uri: verification_uri.into(), + continuation_sender: Arc::new(continuation_sender.into()), + } } GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets, GrantLoginProgress::Done => Self::Done, @@ -664,6 +669,7 @@ pub enum GrantGeneratedQrLoginProgress { WaitingForAuth { /// A URI to open in a (secure) system browser to verify the new login. verification_uri: String, + continuation_message_sender: Arc, }, /// We are syncing secrets. SyncingSecrets, @@ -688,8 +694,11 @@ impl From> for GrantGeneratedQrL GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( inner, )) => Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) }, - GrantLoginProgress::WaitingForAuth { verification_uri } => { - Self::WaitingForAuth { verification_uri: verification_uri.into() } + GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => { + Self::WaitingForAuth { + verification_uri: verification_uri.into(), + continuation_message_sender: Arc::new(continuation_sender.into()), + } } GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets, GrantLoginProgress::Done => Self::Done, @@ -697,9 +706,9 @@ impl From> for GrantGeneratedQrL } } -#[derive(Debug, uniffi::Object)] /// Used to pass back the [`CheckCode`] entered by the user to verify that the /// secure channel is indeed secure. +#[derive(Debug, uniffi::Object)] pub struct CheckCodeSender { inner: SdkCheckCodeSender, } @@ -717,3 +726,25 @@ impl CheckCodeSender { self.inner.send(code).await.map_err(HumanQrLoginError::from) } } + +#[derive(Debug, Clone, uniffi::Object)] +pub struct ContinuationMessageSender { + inner: SdkContinuationMessageSender, +} + +#[matrix_sdk_ffi_macros::export] +impl ContinuationMessageSender { + pub async fn confirm(&self) -> Result<(), HumanQrLoginError> { + self.inner.confirm().await.map_err(HumanQrLoginError::from) + } + + pub async fn cancel(&self) -> Result<(), HumanQrLoginError> { + self.inner.cancel().await.map_err(HumanQrLoginError::from) + } +} + +impl From for ContinuationMessageSender { + fn from(value: SdkContinuationMessageSender) -> Self { + Self { inner: value } + } +} diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index 6653f3940e4..95298658bb3 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -36,8 +36,9 @@ use super::{ use crate::{ Client, authentication::oauth::qrcode::{ - CheckCodeSender, GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError, - QrProgress, SecureChannelError, messages::LoginProtocolsMessage, + CheckCodeSender, CloneableSender, ContinuationMessage, ContinuationMessageSender, + GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError, QrProgress, + SecureChannelError, messages::LoginProtocolsMessage, }, }; @@ -116,7 +117,32 @@ async fn finish_login_grant( .as_str(), ) .map_err(|e| QRCodeGrantLoginError::Unknown(e.to_string()))?; - state.set(GrantLoginProgress::WaitingForAuth { verification_uri }); + + let (sender, receiver) = tokio::sync::oneshot::channel(); + state.set(GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender: ContinuationMessageSender(CloneableSender::new(sender)), + }); + + // We wait for this device to finish up the login in the browser, only then do + // we continue. + match receiver.await { + Ok(ContinuationMessage::Confirm) => {} + // If the user sends an explicit cancellation or the sender is dropped without sending we + // tell the other side that we're unable to open the verification URI. + // + // Realistically the sender can't be dropped but let's just treat the cases the same instead + // of adding an unreachable. + Ok(ContinuationMessage::Cancel) | Err(_) => { + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::UnableToOpenVerificationUri, + homeserver: None, + }) + .await?; + return Ok(()); + } + } // We send the new device the m.login.protocol_accepted message to let it know // that the consent process is in progress. @@ -196,6 +222,10 @@ pub enum GrantLoginProgress { WaitingForAuth { /// A URI to open in a (secure) system browser to verify the new login. verification_uri: Url, + /// A sender to confirm that the login has been veriried in the system + /// browser or to cancel because the verification URI couldn't + /// be opened. + continuation_sender: ContinuationMessageSender, }, /// The new device has been granted access and this device is sending the /// secrets to it. @@ -935,7 +965,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -943,6 +976,7 @@ mod test { ) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } GrantLoginProgress::SyncingSecrets => { assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); @@ -1073,12 +1107,16 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. }) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } GrantLoginProgress::SyncingSecrets => { assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); @@ -1213,12 +1251,16 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. }) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } GrantLoginProgress::SyncingSecrets => { assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); @@ -1841,7 +1883,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -1849,6 +1894,7 @@ mod test { ) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -1966,12 +2012,16 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. }) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -2503,7 +2553,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -2511,6 +2564,7 @@ mod test { ) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -2633,12 +2687,16 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. }) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -2778,7 +2836,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -2786,6 +2847,7 @@ mod test { ) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -2909,12 +2971,16 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::WaitingForAuth { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel(QrProgress { .. }) ); assert_eq!(verification_uri.as_str(), verification_uri_complete); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs index 10fb929ad15..cc256729e30 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs @@ -166,6 +166,9 @@ pub enum LoginFailureReason { /// Sent by either new or existing device to indicate that the user has /// cancelled the login. UserCancelled, + /// Sent by existing device to indicate that it was unable to open the + /// verification_uri_complete (or verification_uri). + UnableToOpenVerificationUri, #[doc(hidden)] _Custom(PrivOwnedStr), } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 6923a40f36d..2c7e3f4d04e 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -384,10 +384,26 @@ impl CheckCodeSender { /// /// * `check_code` - The check code in digits representation. pub async fn send(&self, check_code: u8) -> Result<(), SenderError> { - match self.inner.lock().await.take() { - Some(tx) => tx.send(check_code).map_err(|_| SenderError::CannotSend), - None => Err(SenderError::AlreadySent), - } + self.send_impl(check_code).await + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) enum ContinuationMessage { + Confirm, + Cancel, +} + +#[derive(Clone, Debug)] +pub struct ContinuationMessageSender(CloneableSender); + +impl ContinuationMessageSender { + pub async fn confirm(&self) -> Result<(), SenderError> { + self.0.send_impl(ContinuationMessage::Confirm).await + } + + pub async fn cancel(&self) -> Result<(), SenderError> { + self.0.send_impl(ContinuationMessage::Cancel).await } } @@ -402,6 +418,13 @@ impl CloneableSender { pub(crate) fn new(tx: tokio::sync::oneshot::Sender) -> Self { Self { inner: Arc::new(Mutex::new(Some(tx))) } } + + async fn send_impl(&self, message: T) -> Result<(), SenderError> { + match self.inner.lock().await.take() { + Some(tx) => tx.send(message).map_err(|_| SenderError::CannotSend), + None => Err(SenderError::AlreadySent), + } + } } /// Possible errors when calling [`CheckCodeSender::send`]. From 0591a47d79f90c0af9bfdb0fd332acdf6f4fa96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 29 Apr 2026 15:54:41 +0200 Subject: [PATCH 22/29] Fix ffi compilation --- bindings/matrix-sdk-ffi/src/client.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index f03a036092a..a9a7ef5f00a 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -75,10 +75,16 @@ use matrix_sdk_ui::{ use mime::Mime; use oauth2::Scope; use ruma::{ - api::client::{ - alias::get_alias, - discovery::get_authorization_server_metadata::v1::{ - AccountManagementActionData, DeviceDeleteData, DeviceViewData, GrantType, + OwnedDeviceId, OwnedMxcUri, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName, + api::{ + client::{ + alias::get_alias, + discovery::get_authorization_server_metadata::v1::{ + AccountManagementActionData, DeviceDeleteData, DeviceViewData, GrantType, + }, + profile::{AvatarUrl, DisplayName}, + room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent}, + uiaa::{EmailUserIdentifier, UserIdentifier}, }, error::ErrorKind, }, From 94061ac65997ee5a1cfb752decbfe2ae45268a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 Apr 2026 16:32:27 +0200 Subject: [PATCH 23/29] feat(qr-code): Add yet another state to the login granting process This separates the step of opening the verification URI in a browser and closing the browser. --- bindings/matrix-sdk-ffi/src/qr_code.rs | 48 +++++--- .../src/authentication/oauth/qrcode/grant.rs | 103 ++++++++++++++---- 2 files changed, 117 insertions(+), 34 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 34aac4c4355..ecb9243643e 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -610,11 +610,14 @@ pub enum GrantQrLoginProgress { }, /// The secure channel has been confirmed using the [`CheckCode`] and this /// device is waiting for the authorization to complete. - WaitingForAuth { + OpeningVerificationUri { /// A URI to open in a (secure) system browser to verify the new login. verification_uri: String, continuation_sender: Arc, }, + WaitingForAuth { + continuation_sender: Arc, + }, /// We are syncing secrets. SyncingSecrets, /// The login has successfully finished. @@ -638,11 +641,15 @@ impl From> for GrantQrLoginProgress { check_code_string: format!("{check_code:02}"), } } - GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => { - Self::WaitingForAuth { - verification_uri: verification_uri.into(), - continuation_sender: Arc::new(continuation_sender.into()), - } + GrantLoginProgress::OpeningVerificationUri { + verification_uri, + continuation_sender, + } => Self::OpeningVerificationUri { + verification_uri: verification_uri.into(), + continuation_sender: Arc::new(continuation_sender.into()), + }, + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + Self::WaitingForAuth { continuation_sender: Arc::new(continuation_sender.into()) } } GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets, GrantLoginProgress::Done => Self::Done, @@ -659,17 +666,24 @@ pub enum GrantGeneratedQrLoginProgress { Starting, /// We have established the secure channel and now need to display the /// QR code so that the existing device can scan it. - QrReady { qr_code: Arc }, + QrReady { + qr_code: Arc, + }, /// The existing device has scanned the QR code and is displaying the /// checkcode. We now need to ask the user to enter the checkcode so that /// we can verify that the channel is indeed secure. - QrScanned { check_code_sender: Arc }, + QrScanned { + check_code_sender: Arc, + }, /// The secure channel has been confirmed using the [`CheckCode`] and this /// device is waiting for the authorization to complete. - WaitingForAuth { + OpeningVerificationUri { /// A URI to open in a (secure) system browser to verify the new login. verification_uri: String, - continuation_message_sender: Arc, + continuation_sender: Arc, + }, + WaitingForAuth { + continuation_sender: Arc, }, /// We are syncing secrets. SyncingSecrets, @@ -694,11 +708,15 @@ impl From> for GrantGeneratedQrL GrantLoginProgress::EstablishingSecureChannel(GeneratedQrProgress::QrScanned( inner, )) => Self::QrScanned { check_code_sender: Arc::new(CheckCodeSender { inner }) }, - GrantLoginProgress::WaitingForAuth { verification_uri, continuation_sender } => { - Self::WaitingForAuth { - verification_uri: verification_uri.into(), - continuation_message_sender: Arc::new(continuation_sender.into()), - } + GrantLoginProgress::OpeningVerificationUri { + verification_uri, + continuation_sender, + } => Self::OpeningVerificationUri { + verification_uri: verification_uri.into(), + continuation_sender: Arc::new(continuation_sender.into()), + }, + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + Self::WaitingForAuth { continuation_sender: Arc::new(continuation_sender.into()) } } GrantLoginProgress::SyncingSecrets => Self::SyncingSecrets, GrantLoginProgress::Done => Self::Done, diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index 95298658bb3..1991e328867 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs @@ -119,13 +119,13 @@ async fn finish_login_grant( .map_err(|e| QRCodeGrantLoginError::Unknown(e.to_string()))?; let (sender, receiver) = tokio::sync::oneshot::channel(); - state.set(GrantLoginProgress::WaitingForAuth { + state.set(GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender: ContinuationMessageSender(CloneableSender::new(sender)), }); - // We wait for this device to finish up the login in the browser, only then do - // we continue. + // We wait for this device to open the verification URI, this gives the device + // the chance to tell us that it couldn't have opened the verification URI. match receiver.await { Ok(ContinuationMessage::Confirm) => {} // If the user sends an explicit cancellation or the sender is dropped without sending we @@ -153,8 +153,28 @@ async fn finish_login_grant( // The new device displays the user code it received from the authorization // server and starts polling for an access token. In parallel, the user // consents to the new login in the browser on this device, while verifying - // the user code displayed on the other device. -- MSC4108 OAuth 2.0 login - // steps 5 & 6 + // the user code displayed on the other device. -- MSC4108 OAuth 2.0 login steps + // 5 & 6 + + let (sender, receiver) = tokio::sync::oneshot::channel(); + state.set(GrantLoginProgress::WaitingForAuth { + continuation_sender: ContinuationMessageSender(CloneableSender::new(sender)), + }); + + // We wait for this device to confirm that the authorization using the + // verification URI has succeeded. + match receiver.await { + Ok(ContinuationMessage::Confirm) => {} + Ok(ContinuationMessage::Cancel) | Err(_) => { + channel + .send_json(QrAuthMessage::LoginFailure { + reason: LoginFailureReason::UnableToOpenVerificationUri, + homeserver: None, + }) + .await?; + return Ok(()); + } + } // We wait for the new device to send us the m.login.success or m.login.failure // message @@ -218,13 +238,20 @@ pub enum GrantLoginProgress { /// and/or [`CheckCode`]. EstablishingSecureChannel(Q), /// The secure channel has been confirmed using the [`CheckCode`] and this - /// device is waiting for the authorization to complete. - WaitingForAuth { + /// device is waiting for verification URI to be opened. + OpeningVerificationUri { /// A URI to open in a (secure) system browser to verify the new login. verification_uri: Url, - /// A sender to confirm that the login has been veriried in the system - /// browser or to cancel because the verification URI couldn't - /// be opened. + /// A sender to confirm that the verification URI has been successfully + /// opened, or cancel if the URI could not have been opened. + continuation_sender: ContinuationMessageSender, + }, + /// The verification URI has been opened and the device is waiting for the + /// authorization to complete. + WaitingForAuth { + /// A sender to confirm that the authorization using the verification + /// URI has been completed. We can now wait on the other side to confirm + /// the receival of the access token. continuation_sender: ContinuationMessageSender, }, /// The new device has been granted access and this device is sending the @@ -965,7 +992,7 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -978,6 +1005,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } GrantLoginProgress::SyncingSecrets => { assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); } @@ -1107,7 +1138,7 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -1118,6 +1149,12 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } + GrantLoginProgress::SyncingSecrets => { assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); } @@ -1251,7 +1288,7 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -1262,6 +1299,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } GrantLoginProgress::SyncingSecrets => { assert_matches!(state, GrantLoginProgress::WaitingForAuth { .. }); } @@ -1883,7 +1924,7 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -1896,6 +1937,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } _ => { panic!("Alice should abort the process"); } @@ -2012,7 +2057,7 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -2023,6 +2068,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } _ => { panic!("Alice should abort the process"); } @@ -2553,7 +2602,7 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -2566,6 +2615,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } _ => { panic!("Alice should abort the process"); } @@ -2687,7 +2740,7 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -2698,6 +2751,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } _ => { panic!("Alice should abort the process"); } @@ -2836,7 +2893,7 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -2849,6 +2906,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } _ => { panic!("Alice should abort the process"); } @@ -2971,7 +3032,7 @@ mod test { .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { + GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender, } => { @@ -2982,6 +3043,10 @@ mod test { assert_eq!(verification_uri.as_str(), verification_uri_complete); continuation_sender.confirm().await.expect("should be able to confirm"); } + GrantLoginProgress::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); + } _ => { panic!("Alice should abort the process"); } From f5dce408d56cbd116c553b8487395e6ef25beac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 30 Apr 2026 10:53:10 +0200 Subject: [PATCH 24/29] doc(qr-login): Fix the examples now that we have more states for the login granting --- .../src/authentication/oauth/mod.rs | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 915d6743444..61251fbe4b0 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -1651,8 +1651,17 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { /// println!("Please enter the checkcode on your other device: {:?}", check_code); /// } - /// GrantLoginProgress::WaitingForAuth { verification_uri } => { - /// println!("Please open {verification_uri} to confirm the new login") + /// GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender } => { + /// println!("Please open {verification_uri} to confirm the new login"); + /// + /// // Once the client was able to open the verification URI we can let the + /// // process continue + /// continuation_sender.confirm().await?; + /// }, + /// GrantLoginProgress::WaitingForAuth { continuation_sender } => { + /// // Once the new login has been confirmed in the browser, we can let the + /// // client continue with the process. + /// continuation_sender.confirm().await?; /// }, /// GrantLoginProgress::Done => break, /// } @@ -1728,9 +1737,18 @@ impl<'a> GrantLoginWithQrCodeBuilder<'a> { /// let check_code = s.trim().parse::()?; /// checkcode_sender.send(check_code).await?; /// } - /// GrantLoginProgress::WaitingForAuth { verification_uri } => { - /// println!("Please open {verification_uri} to confirm the new login") + /// GrantLoginProgress::OpeningVerificationUri { verification_uri, continuation_sender } => { + /// println!("Please open {verification_uri} to confirm the new login"); + /// + /// // Once the client was able to open the verification URI we can let the + /// // process continue + /// continuation_sender.confirm().await?; /// }, + /// GrantLoginProgress::WaitingForAuth { continuation_sender } => { + /// // Once the new login has been confirmed in the browser, we can let the + /// // client continue with the process. + /// continuation_sender.confirm().await?; + /// } /// GrantLoginProgress::Done => break, /// } /// } From 4019d154cc1d46a5d9d76713f8228352fa4c6d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Thu, 30 Apr 2026 15:41:03 +0200 Subject: [PATCH 25/29] Don't depend on getrandom 3 twice --- crates/matrix-sdk-common/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index d0fd06949cf..34f3e5ebc16 100644 --- a/crates/matrix-sdk-common/Cargo.toml +++ b/crates/matrix-sdk-common/Cargo.toml @@ -67,7 +67,6 @@ wasm-bindgen-test.workspace = true [target.'cfg(target_family = "wasm")'.dev-dependencies] # Enable the JS feature for getrandom. getrandom = { workspace = true, default-features = false, features = ["wasm_js"] } -getrandom3 = { version = "0.3.4", package = "getrandom", default-features = false, features = ["wasm_js"] } js-sys.workspace = true [lints] From a0f1ef5962830789a2d8249db1ed21ebb3a148e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Mon, 4 May 2026 12:09:37 +0200 Subject: [PATCH 26/29] docs(qr-code): Document the continuation message sender --- bindings/matrix-sdk-ffi/src/qr_code.rs | 5 +++++ crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index ecb9243643e..009008134ac 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -745,6 +745,9 @@ impl CheckCodeSender { } } +/// Struct used to let the QR code granting logic know that it can continue with +/// the process since applications might suspend things while the verification +/// URI is open. #[derive(Debug, Clone, uniffi::Object)] pub struct ContinuationMessageSender { inner: SdkContinuationMessageSender, @@ -752,10 +755,12 @@ pub struct ContinuationMessageSender { #[matrix_sdk_ffi_macros::export] impl ContinuationMessageSender { + /// Confirm the continuation of the login granting process. pub async fn confirm(&self) -> Result<(), HumanQrLoginError> { self.inner.confirm().await.map_err(HumanQrLoginError::from) } + /// Cancel the the login granting process. pub async fn cancel(&self) -> Result<(), HumanQrLoginError> { self.inner.cancel().await.map_err(HumanQrLoginError::from) } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 2c7e3f4d04e..c11cd921a3d 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -388,20 +388,27 @@ impl CheckCodeSender { } } +/// The internal message of the [`ContinuationMessageSender`] to either continue +/// the login granting process or to cancel it. #[derive(Clone, Copy, Debug)] pub(crate) enum ContinuationMessage { Confirm, Cancel, } +/// Struct used to let the QR code granting logic know that it can continue with +/// the process since applications might suspend things while the verification +/// URI is open. #[derive(Clone, Debug)] pub struct ContinuationMessageSender(CloneableSender); impl ContinuationMessageSender { + /// Confirm the continuation of the login granting process. pub async fn confirm(&self) -> Result<(), SenderError> { self.0.send_impl(ContinuationMessage::Confirm).await } + /// Cancel the the login granting process. pub async fn cancel(&self) -> Result<(), SenderError> { self.0.send_impl(ContinuationMessage::Cancel).await } From 55a630613bcdc233b9b9452496e96b7714579360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 8 Apr 2026 16:32:27 +0200 Subject: [PATCH 27/29] fix(qr-login): Ensure some of the string are short enough to be put into the aad --- bindings/matrix-sdk-ffi/src/qr_code.rs | 12 +- .../src/types/qr_login/mod.rs | 30 ++++- .../src/types/qr_login/msc_4388.rs | 112 +++++++++++++++- .../src/authentication/oauth/qrcode/mod.rs | 29 ++++- .../oauth/qrcode/rendezvous_channel/mod.rs | 29 +++-- .../qrcode/rendezvous_channel/msc_4388.rs | 122 +++++++++++------- .../oauth/qrcode/secure_channel/mod.rs | 10 +- 7 files changed, 267 insertions(+), 77 deletions(-) diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 009008134ac..1edeb3df2c0 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -364,9 +364,9 @@ impl From for HumanQrLoginError { } QRCodeLoginError::SecureChannel(e) => match e { - SecureChannelError::MessageDecode(_) | SecureChannelError::RendezvousChannel(_) => { - HumanQrLoginError::Unknown - } + SecureChannelError::MessageDecode(_) + | SecureChannelError::RendezvousChannel(_) + | SecureChannelError::QrCodeCreationError(_) => HumanQrLoginError::Unknown, SecureChannelError::UnsupportedQrCodeType => { HumanQrLoginError::UnsupportedQrCodeType } @@ -470,9 +470,9 @@ impl From for HumanQrGrantLoginError { } QRCodeGrantLoginError::NotFound => Self::NotFound, QRCodeGrantLoginError::SecureChannel(e) => match e { - SecureChannelError::MessageDecode(_) | SecureChannelError::RendezvousChannel(_) => { - Self::Unknown(e.to_string()) - } + SecureChannelError::MessageDecode(_) + | SecureChannelError::RendezvousChannel(_) + | SecureChannelError::QrCodeCreationError(_) => Self::Unknown(e.to_string()), SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType, SecureChannelError::SecureChannelMessage { .. } | SecureChannelError::Decryption(_) diff --git a/crates/matrix-sdk-crypto/src/types/qr_login/mod.rs b/crates/matrix-sdk-crypto/src/types/qr_login/mod.rs index d56941916f2..3dfe09a345e 100644 --- a/crates/matrix-sdk-crypto/src/types/qr_login/mod.rs +++ b/crates/matrix-sdk-crypto/src/types/qr_login/mod.rs @@ -29,6 +29,8 @@ pub use msc_4108::Msc4108IntentData; use url::Url; use vodozemac::{Curve25519PublicKey, base64_decode, base64_encode}; +pub use crate::types::qr_login::msc_4388::{LimitedString, LimitedUrl, RendezvousId}; + /// Error type for the decoding of the [`QrCodeData`]. #[derive(Debug, Error)] #[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))] @@ -77,6 +79,20 @@ pub enum LoginQrCodeDecodeError { }, } +/// Error type for the creation of a new [`QrCodeData`] struct. +#[derive(Debug, Error)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))] +pub enum QrCodeCreationError { + /// The base URL of the homeserver needs to be at most [`u16::MAX`] bytes + /// long, otherwise it doesn't fit into the QR code. + #[error("The base URL of the homeserver is too long")] + TooLongBaseUrl, + /// The rendezvous ID of the channel needs to be at most [`u16::MAX`] bytes + /// long, otherwise it doesn't fit into the QR code. + #[error("The rendezvous ID is too long")] + TooLongRendezvousId, +} + /// Intent-specific data of the [`QrCodeData`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum QrCodeIntentData<'a> { @@ -98,7 +114,7 @@ pub enum QrCodeIntentData<'a> { Msc4388 { /// The ID of the rendezvous session, can be used to exchange messages /// with the other device. - rendezvous_id: &'a str, + rendezvous_id: &'a RendezvousId, /// The base URL of the homeserver that the device generating the QR is /// using. base_url: &'a Url, @@ -187,15 +203,19 @@ impl QrCodeData { rendezvous_id: String, base_url: Url, intent: QrCodeIntent, - ) -> Self { - Self { + ) -> Result { + let rendezvous_id = + RendezvousId::new(rendezvous_id).ok_or(QrCodeCreationError::TooLongRendezvousId)?; + let base_url = LimitedUrl::new(base_url).ok_or(QrCodeCreationError::TooLongBaseUrl)?; + + Ok(Self { inner: QrCodeDataInner::Msc4388(msc_4388::QrCodeData { intent: intent.into(), public_key, rendezvous_id, base_url, }), - } + }) } /// Attempt to decode a slice of bytes into a [`QrCodeData`] object. @@ -263,7 +283,7 @@ impl QrCodeData { }, QrCodeDataInner::Msc4388(qr_code_data) => QrCodeIntentData::Msc4388 { rendezvous_id: &qr_code_data.rendezvous_id, - base_url: &qr_code_data.base_url, + base_url: qr_code_data.base_url.as_url(), }, } } diff --git a/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs b/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs index dd46894d1b1..d32afaa428b 100644 --- a/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs +++ b/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs @@ -17,6 +17,7 @@ //! [MSC4388]: https://github.com/matrix-org/matrix-spec-proposals/pull/4388 use std::{ + fmt, io::{Cursor, Read}, str::{self}, }; @@ -65,6 +66,95 @@ impl TryFrom for QrCodeIntent { } } +/// A wrapper type for a [`Url`] which limits the length of the URL to +/// [`u16::MAX`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LimitedUrl(Url); + +impl LimitedUrl { + /// Create a new [`LimitedUrl`] from a [`Url`]. + /// + /// Returns `None` if the [`Url`] is too long. + pub fn new(s: Url) -> Option { + if s.as_str().len() <= u16::MAX as usize { Some(Self(s)) } else { None } + } + + /// Return the length of the URL. + /// + /// Is returned as an `u16` as it is guaranteed to be <= [`u16::MAX`]. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> u16 { + self.0.as_str().len() as u16 + } + + /// Get a reference to the underlying [`Url`]. + pub fn as_url(&self) -> &Url { + &self.0 + } + + /// Get a reference to the string representation of this URL. + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + /// Get a reference to the byte representation of the URL. + /// + /// This is a shorthand for `url.as_str().as_bytes()`. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_str().as_bytes() + } +} + +impl fmt::Display for LimitedUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// A wrapper type for a [`String`] which limits the length of the string to +/// [`u16::MAX`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LimitedString(String); + +impl fmt::Display for LimitedString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl LimitedString { + /// Create a new [`LimitedString`] from a [`String`]. + /// + /// Returns `None` if the [`String`] is too long. + pub fn new(s: String) -> Option { + if s.len() <= u16::MAX as usize { Some(Self(s)) } else { None } + } + + /// Return the length of the string. + /// + /// Is returned as an `u16` as it is guaranteed to be <= [`u16::MAX`]. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> u16 { + self.0.len() as u16 + } + + /// Get a reference to the string. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Get a reference to the byte representation of the string. + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +/// Type representing the rendezvous ID of a rendezvous session. +/// +/// Rendezvous IDs need to be put into the QR code for the QR code based login +/// and need to be at most [`u16::MAX`] bytes long to fit into the QR code. +pub type RendezvousId = LimitedString; + /// Data for the QR code login mechanism. /// /// The [`QrCodeData`] can be serialized and encoded as a QR code or it can be @@ -78,10 +168,10 @@ pub struct QrCodeData { pub public_key: Curve25519PublicKey, /// The ID of the rendezvous session, can be used to exchange messages with /// the other device. - pub rendezvous_id: String, + pub rendezvous_id: RendezvousId, /// The base URL of the homeserver that the device generating the QR is /// using. - pub base_url: Url, + pub base_url: LimitedUrl, } impl QrCodeData { @@ -135,6 +225,10 @@ impl QrCodeData { reader.read_exact(&mut rendezvous_id)?; let rendezvous_id = String::from_utf8(rendezvous_id).map_err(|e| e.utf8_error())?; + // The length here is guaranteed to be <= u16::MAX because that's the maximum + // amount of bytes we might have read. So we can skip the constructor here. + let rendezvous_id = LimitedString(rendezvous_id); + // 7. We read the two bytes for the length of the server base URL. let base_url_len = reader.read_u16::()?; @@ -142,6 +236,7 @@ impl QrCodeData { let mut base_url = vec![0u8; base_url_len.into()]; reader.read_exact(&mut base_url)?; let base_url = Url::parse(str::from_utf8(&base_url)?)?; + let base_url = LimitedUrl(base_url); Ok(Self { public_key, rendezvous_id, base_url, intent }) } else { @@ -154,10 +249,10 @@ impl QrCodeData { /// The list of bytes can be used by a QR code generator to create an image /// containing a QR code. pub fn to_bytes(&self) -> Vec { - let rendezvous_id_len = (self.rendezvous_id.as_str().len() as u16).to_be_bytes(); + let rendezvous_id_len = self.rendezvous_id.len().to_be_bytes(); // if path is / then don't include the trailing slash - let base_url = if self.base_url.path() == "/" { + let base_url = if self.base_url.as_url().path() == "/" { self.base_url.as_str().trim_end_matches('/') } else { self.base_url.as_str() @@ -237,7 +332,8 @@ mod test { ); assert_eq!( - "e8da6355-550b-4a32-a193-1619d9830668", data.rendezvous_id, + "e8da6355-550b-4a32-a193-1619d9830668", + data.rendezvous_id.as_str(), "The parsed rendezvous ID should match expected one", ); @@ -263,7 +359,8 @@ mod test { ); assert_eq!( - "e8da6355-550b-4a32-a193-1619d9830668", data.rendezvous_id, + "e8da6355-550b-4a32-a193-1619d9830668", + data.rendezvous_id.as_str(), "The parsed rendezvous URL should match expected one", ); @@ -305,7 +402,8 @@ mod test { ); assert_eq!( - "01HX9K00Q1H6KPD47EG4G1T3XG", data.rendezvous_id, + "01HX9K00Q1H6KPD47EG4G1T3XG", + data.rendezvous_id.as_str(), "The parsed rendezvous URL should match the expected one", ); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index c11cd921a3d..f76f485e3c1 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -27,7 +27,9 @@ use as_variant::as_variant; pub use matrix_sdk_base::crypto::types::qr_login::{ LoginQrCodeDecodeError, Msc4108IntentData, QrCodeData, QrCodeIntent, QrCodeIntentData, }; -use matrix_sdk_base::crypto::{SecretImportError, store::SecretsBundleExportError}; +use matrix_sdk_base::crypto::{ + SecretImportError, store::SecretsBundleExportError, types::qr_login::QrCodeCreationError, +}; pub use oauth2::{ ConfigurationError, DeviceCodeErrorResponse, DeviceCodeErrorResponseType, HttpClientError, RequestTokenError, StandardErrorResponse, @@ -262,16 +264,35 @@ pub enum MessageDecodeError { /// A received message has failed to be decoded. #[error(transparent)] Ecies(#[from] EciesMessageDecodeError), + /// A received message has failed to be decoded. #[error(transparent)] Hpke(#[from] HpkeMessageDecodeError), + /// A message we received over the secure channel was not a valid UTF-8 /// encoded string. #[error(transparent)] Utf8(#[from] std::str::Utf8Error), + /// A message couldn't be deserialized from JSON. #[error(transparent)] Json(#[from] serde_json::Error), + + /// The sequence token of the rendezvous channel needs to be at most + /// [`u16::MAX`] bytes long, otherwise it can't be encoded as additional + /// authenticated data. + #[error("The base URL of the homeserver is too long")] + TooLongSequenceToken, + + /// The base URL of the homeserver needs to be at most [`u16::MAX`] bytes + /// long, otherwise it can't be encoded as additional authenticated data. + #[error("The base URL of the homeserver is too long")] + TooLongBaseUrl, + + /// The rendezvous ID of the channel needs to be at most [`u16::MAX`] bytes + /// long, otherwise it can't be encoded as additional authenticated data. + #[error("The rendezvous ID is too long")] + TooLongRendezvousId, } /// Error type for decryption failures of the secure channel. @@ -334,9 +355,13 @@ pub enum SecureChannelError { )] CannotReceiveCheckCode, - #[error("The QR code specifies an unsupported protocol version")] /// The QR code specifies an unsupported protocol version. + #[error("The QR code specifies an unsupported protocol version")] UnsupportedQrCodeType, + + /// The QR code couldn't have been created. + #[error(transparent)] + QrCodeCreationError(#[from] QrCodeCreationError), } /// Metadata to be used with [`LoginProgress::EstablishingSecureChannel`] diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs index e9640aa9270..ac5f93ea66f 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use matrix_sdk_base::crypto::types::qr_login::{LimitedUrl, RendezvousId}; use tracing::instrument; use url::Url; @@ -39,7 +40,7 @@ pub(super) struct InboundChannelCreationResult { #[derive(Debug, PartialEq, Eq)] pub(super) enum RendezvousInfo<'a> { Msc4108 { rendezvous_url: &'a Url }, - Msc4388 { rendezvous_id: &'a str }, + Msc4388 { rendezvous_id: &'a RendezvousId }, } pub(super) enum RendezvousChannel { @@ -57,9 +58,11 @@ impl RendezvousChannel { client: HttpClient, rendezvous_server: &Url, msc_4388: bool, - ) -> Result { + ) -> Result { if msc_4388 { - Ok(Self::Msc4388(msc_4388::Channel::create_outbound(client, rendezvous_server).await?)) + let rendezvous_server = LimitedUrl::new(rendezvous_server.clone()) + .ok_or(MessageDecodeError::TooLongBaseUrl)?; + Ok(Self::Msc4388(msc_4388::Channel::create_outbound(client, &rendezvous_server).await?)) } else { Ok(Self::Msc4108(msc_4108::Channel::create_outbound(client, rendezvous_server).await?)) } @@ -86,10 +89,13 @@ impl RendezvousChannel { pub(super) async fn create_inbound_msc4388( client: HttpClient, base_url: &Url, - rendezvous_id: &str, - ) -> Result { + rendezvous_id: &RendezvousId, + ) -> Result { + let base_url = + LimitedUrl::new(base_url.clone()).ok_or(MessageDecodeError::TooLongBaseUrl)?; + let msc_4388::InboundChannelCreationResult { channel, initial_message } = - msc_4388::Channel::create_inbound(client, base_url, rendezvous_id).await?; + msc_4388::Channel::create_inbound(client, &base_url, rendezvous_id).await?; Ok(InboundChannelCreationResult { channel: Self::Msc4388(channel), @@ -115,9 +121,9 @@ impl RendezvousChannel { /// /// The message must be of the `text/plain` content type. #[instrument(skip_all)] - pub(super) async fn send(&mut self, message: String) -> Result<(), HttpError> { + pub(super) async fn send(&mut self, message: String) -> Result<(), SecureChannelError> { match self { - RendezvousChannel::Msc4108(channel) => channel.send(message.into_bytes()).await, + RendezvousChannel::Msc4108(channel) => Ok(channel.send(message.into_bytes()).await?), RendezvousChannel::Msc4388(channel) => channel.send(message).await, } } @@ -153,10 +159,17 @@ impl RendezvousChannel { RendezvousChannel::Msc4388(channel) => { let msc_4388::Channel { base_url, rendezvous_id, sequence_token, .. } = channel; + let base_url_len: u16 = base_url.len(); + let rendezvous_id_len: u16 = rendezvous_id.len(); + let sequence_token_len: u16 = sequence_token.len(); + Some( [ + &base_url_len.to_be_bytes(), base_url.as_str().as_bytes(), + &rendezvous_id_len.to_be_bytes(), rendezvous_id.as_bytes(), + &sequence_token_len.to_be_bytes(), sequence_token.as_bytes(), ] .concat(), diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs index 91448e069e0..b03616b5ab3 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs @@ -15,7 +15,10 @@ use std::{borrow::Cow, time::Duration}; use http::StatusCode; -use matrix_sdk_base::sleep; +use matrix_sdk_base::{ + crypto::types::qr_login::{LimitedString, LimitedUrl, RendezvousId}, + sleep, +}; use ruma::api::{ EndpointError as _, SupportedVersions, client::rendezvous::{ @@ -26,7 +29,11 @@ use ruma::api::{ use tracing::{debug, instrument, trace}; use url::Url; -use crate::{HttpError, RumaApiError, http_client::HttpClient}; +use crate::{ + HttpError, RumaApiError, + authentication::oauth::qrcode::{MessageDecodeError, SecureChannelError}, + http_client::HttpClient, +}; #[cfg(test)] const POLL_TIMEOUT: Duration = Duration::from_millis(10); @@ -45,6 +52,12 @@ pub(super) struct InboundChannelCreationResult { pub initial_message: String, } +/// Type representing the rendezvous ID of a rendezvous session. +/// +/// The sequence token will be put into the additional authenticated data and +/// need to be at most [`u16::MAX`] bytes long for it to fit into it. +type SequenceToken = LimitedString; + struct RendezvousMessage { pub status_code: StatusCode, pub data: String, @@ -52,11 +65,11 @@ struct RendezvousMessage { pub(crate) struct Channel { client: HttpClient, - pub(super) base_url: Url, + pub(super) base_url: LimitedUrl, /// The ID of the rendezvous session we're using to exchange messages /// through the channel. - pub(super) rendezvous_id: String, - pub(super) sequence_token: String, + pub(super) rendezvous_id: RendezvousId, + pub(super) sequence_token: SequenceToken, } fn response_to_error(status: StatusCode, data: String) -> HttpError { @@ -80,8 +93,8 @@ impl Channel { /// through the channel. pub(super) async fn create_outbound( client: HttpClient, - base_url: &Url, - ) -> Result { + base_url: &LimitedUrl, + ) -> Result { use std::borrow::Cow; let request = create_rendezvous_session::unstable_msc4388::Request::new("".to_owned()); @@ -101,8 +114,10 @@ impl Channel { ) .await?; - let rendezvous_id = response.id; - let sequence_token = response.sequence_token; + let rendezvous_id = + RendezvousId::new(response.id).ok_or(MessageDecodeError::TooLongRendezvousId)?; + let sequence_token = SequenceToken::new(response.sequence_token) + .ok_or(MessageDecodeError::TooLongRendezvousId)?; Ok(Self { client, base_url: base_url.to_owned(), rendezvous_id, sequence_token }) } @@ -113,14 +128,15 @@ impl Channel { /// message from the rendezvous session on the given [`rendezvous_url`]. pub(super) async fn create_inbound( client: HttpClient, - base_url: &Url, - rendezvous_id: &str, - ) -> Result { + base_url: &LimitedUrl, + rendezvous_id: &RendezvousId, + ) -> Result { // Receive the initial message, which should be empty. But we need the ETAG to // fully establish the rendezvous channel. - let response = Self::receive_message_impl(&client, base_url, rendezvous_id).await?; - - let sequence_token = response.sequence_token.clone(); + let response = + Self::receive_message_impl(&client, base_url.as_url(), rendezvous_id).await?; + let sequence_token = SequenceToken::new(response.sequence_token) + .ok_or(MessageDecodeError::TooLongSequenceToken)?; let initial_message = RendezvousMessage { status_code: StatusCode::OK, data: response.data }; @@ -137,17 +153,17 @@ impl Channel { /// Get the ID of the rendezvous session we're using to exchange messages /// through the channel. - pub(super) fn rendezvous_id(&self) -> &str { + pub(super) fn rendezvous_id(&self) -> &RendezvousId { &self.rendezvous_id } /// Send the given `message` through the [`RendezvousChannel`] to the other /// device. #[instrument(skip_all)] - pub(super) async fn send(&mut self, message: String) -> Result<(), HttpError> { + pub(super) async fn send(&mut self, message: String) -> Result<(), SecureChannelError> { let request = update_rendezvous_session::unstable::Request::new( - self.rendezvous_id.clone(), - self.sequence_token.clone(), + self.rendezvous_id.to_string(), + self.sequence_token.to_string(), message, ); @@ -170,7 +186,8 @@ impl Channel { // We successfully send out a message, get the sequence_token and update our // internal copy of the sequence_token. - self.sequence_token = response.sequence_token; + self.sequence_token = SequenceToken::new(response.sequence_token) + .ok_or(MessageDecodeError::TooLongSequenceToken)?; Ok(()) } @@ -183,7 +200,7 @@ impl Channel { /// /// This method will wait in a loop for the channel to give us a new /// message. - pub(super) async fn receive(&mut self) -> Result { + pub(super) async fn receive(&mut self) -> Result { loop { let message = self.receive_single_message().await?; @@ -202,7 +219,7 @@ impl Channel { } else { let error = response_to_error(message.status_code, message.data); - return Err(error); + return Err(error.into()); } } } @@ -210,9 +227,10 @@ impl Channel { async fn receive_message_impl( client: &HttpClient, base_url: &Url, - rendezvous_id: &str, + rendezvous_id: &RendezvousId, ) -> Result { - let request = get_rendezvous_session::unstable::Request::new(rendezvous_id.to_owned()); + let request = + get_rendezvous_session::unstable::Request::new(rendezvous_id.as_str().to_owned()); client .send( request, @@ -228,13 +246,17 @@ impl Channel { .await } - async fn receive_single_message(&mut self) -> Result { + async fn receive_single_message(&mut self) -> Result { let response = - Self::receive_message_impl(&self.client, &self.base_url, &self.rendezvous_id).await?; + Self::receive_message_impl(&self.client, self.base_url.as_url(), &self.rendezvous_id) + .await?; + + let new_sequence_token = SequenceToken::new(response.sequence_token) + .ok_or(MessageDecodeError::TooLongSequenceToken)?; // since the rendezvous API has changed it doesn't make sense to use the http // status code at this layer - if response.sequence_token == self.sequence_token { + if self.sequence_token == new_sequence_token { return Ok(RendezvousMessage { status_code: StatusCode::NOT_MODIFIED, data: response.data, @@ -242,7 +264,7 @@ impl Channel { } // We received a new sequence_token, put it into the copy of our sequence_token. - self.sequence_token = response.sequence_token; + self.sequence_token = new_sequence_token; let message = RendezvousMessage { status_code: StatusCode::OK, data: response.data }; @@ -267,12 +289,12 @@ mod test { async fn mock_rendezvous_create( server: &MockServer, - rendezvous_id: &str, + rendezvous_id: &RendezvousId, ) -> Result { server .register(Mock::given(method("POST")).and(path(BASE_PATH)).respond_with( ResponseTemplate::new(200).set_body_json(json!({ - "id": rendezvous_id, + "id": rendezvous_id.as_str(), "sequence_token": "1", "expires_in_ms": 10_000, })), @@ -285,11 +307,13 @@ mod test { #[async_test] async fn test_creation() { let server = MockServer::start().await; - let base_url = - Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"); - let rendezvous_id = "abcdEFG12345"; + let base_url = LimitedUrl::new( + Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"), + ) + .unwrap(); + let rendezvous_id = RendezvousId::new("abcdEFG12345".to_owned()).unwrap(); - let rendezvous_path = mock_rendezvous_create(&server, rendezvous_id) + let rendezvous_path = mock_rendezvous_create(&server, &rendezvous_id) .await .expect("We should be able to create a rendezvous"); @@ -301,12 +325,13 @@ mod test { assert_eq!( alice.rendezvous_id(), - rendezvous_id, + &rendezvous_id, "Alice should have configured the rendezvous ID correctly." ); assert_eq!( - alice.sequence_token, "1", + alice.sequence_token.as_str(), + "1", "Alice should have remembered the sequence_token the server gave us." ); @@ -345,7 +370,7 @@ mod test { let client = HttpClient::new(reqwest::Client::new(), RequestConfig::short_retry()); let InboundChannelCreationResult { channel: bob, initial_message: _ } = - Channel::create_inbound(client, &base_url, rendezvous_id).await.expect( + Channel::create_inbound(client, &base_url, &rendezvous_id).await.expect( "We should be able to create a rendezvous channel from a received message", ); @@ -355,7 +380,8 @@ mod test { }; assert_eq!( - bob.sequence_token, "2", + bob.sequence_token.as_str(), + "2", "Bob should have remembered the sequence_token the server gave us." ); @@ -422,9 +448,12 @@ mod test { #[async_test] async fn test_retry_mechanism() { let server = MockServer::start().await; - let base_url = - Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"); - let rendezvous_path = mock_rendezvous_create(&server, "abcdEFG12345") + let rendezvous_id = RendezvousId::new("abcdEFG12345".to_owned()).unwrap(); + let base_url = LimitedUrl::new( + Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"), + ) + .unwrap(); + let rendezvous_path = mock_rendezvous_create(&server, &rendezvous_id) .await .expect("We should be able to create a rendezvous"); @@ -439,7 +468,7 @@ mod test { Mock::given(method("GET")) .and(path(rendezvous_path.clone())) .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "sequence_token": alice.sequence_token, + "sequence_token": alice.sequence_token.as_str(), "data": "old data", "expires_in_ms": 10_000, }))) @@ -472,9 +501,12 @@ mod test { #[async_test] async fn test_receive_error() { let server = MockServer::start().await; - let url = - Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"); - let rendezvous_path = mock_rendezvous_create(&server, "abcdEFG12345") + let rendezvous_id = RendezvousId::new("abcdEFG12345".to_owned()).unwrap(); + let url = LimitedUrl::new( + Url::parse(&server.uri()).expect("We should be able to parse the example homeserver"), + ) + .unwrap(); + let rendezvous_path = mock_rendezvous_create(&server, &rendezvous_id) .await .expect("We should be able to create a rendezvous"); diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index a0f2dcebd97..1d96bc97805 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -75,10 +75,11 @@ impl SecureChannel { let qr_code_data = QrCodeData::new_msc4388( crypto_channel.public_key(), - rendezvous_id.to_owned(), + // TODO: Avoid the double conversion here? + rendezvous_id.as_str().to_owned(), homeserver_url.clone(), QrCodeIntent::Login, - ); + )?; (crypto_channel, qr_code_data) } @@ -109,10 +110,11 @@ impl SecureChannel { RendezvousInfo::Msc4388 { rendezvous_id, .. } => { channel.qr_code_data = QrCodeData::new_msc4388( channel.crypto_channel.public_key(), - rendezvous_id.to_owned(), + // TODO: Avoid the double conversion here? + rendezvous_id.as_str().to_owned(), homeserver_url.clone(), QrCodeIntent::Reciprocate, - ); + )?; } } From d8cd83f4af42480bd6a5a163a20cf2b216a56a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Tue, 5 May 2026 13:44:04 +0200 Subject: [PATCH 28/29] Fix some clippy issues --- crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs | 2 +- .../src/authentication/oauth/qrcode/secure_channel/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index f76f485e3c1..ee37e14787c 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs @@ -440,7 +440,7 @@ impl ContinuationMessageSender { } /// A oneshot sender we are able to clone so we can put it into a -/// [`SharedObservable`]. +/// observable. #[derive(Clone, Debug)] pub struct CloneableSender { inner: Arc>>>, diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs index 1d96bc97805..0f8547d3073 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/mod.rs @@ -357,7 +357,7 @@ impl EstablishedSecureChannel { let aad = self.channel.additional_authenticated_data().unwrap_or_default(); let message = self.crypto_channel.seal(message, &aad); - Ok(self.channel.send(message).await?) + self.channel.send(message).await } async fn receive(&mut self) -> Result { From 0ae11bd234c2affff640db8bda94ccb306afb6c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Wed, 3 Jun 2026 18:57:16 +0200 Subject: [PATCH 29/29] feat: Limit the rendezvous ID and sequence token to u8::MAX bytes --- .../src/types/qr_login/msc_4388.rs | 53 ++++++++++--------- .../oauth/qrcode/rendezvous_channel/mod.rs | 4 +- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs b/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs index d32afaa428b..76e4e8f1b0a 100644 --- a/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs +++ b/crates/matrix-sdk-crypto/src/types/qr_login/msc_4388.rs @@ -112,7 +112,7 @@ impl fmt::Display for LimitedUrl { } /// A wrapper type for a [`String`] which limits the length of the string to -/// [`u16::MAX`]. +/// [`u8::MAX`]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct LimitedString(String); @@ -127,15 +127,15 @@ impl LimitedString { /// /// Returns `None` if the [`String`] is too long. pub fn new(s: String) -> Option { - if s.len() <= u16::MAX as usize { Some(Self(s)) } else { None } + if s.len() <= u8::MAX as usize { Some(Self(s)) } else { None } } /// Return the length of the string. /// - /// Is returned as an `u16` as it is guaranteed to be <= [`u16::MAX`]. + /// Is returned as an `u8` as it is guaranteed to be <= [`u8::MAX`]. #[allow(clippy::len_without_is_empty)] - pub fn len(&self) -> u16 { - self.0.len() as u16 + pub fn len(&self) -> u8 { + self.0.len() as u8 } /// Get a reference to the string. @@ -152,7 +152,7 @@ impl LimitedString { /// Type representing the rendezvous ID of a rendezvous session. /// /// Rendezvous IDs need to be put into the QR code for the QR code based login -/// and need to be at most [`u16::MAX`] bytes long to fit into the QR code. +/// and need to be at most [`u8::MAX`] bytes long to fit into the QR code. pub type RendezvousId = LimitedString; /// Data for the QR code login mechanism. @@ -184,7 +184,7 @@ impl QrCodeData { // 2. One byte type, 0x03 is for the format specified in MSC4388. // 3. One byte intent, either 0x00 or 0x01. // 4. 32 bytes for the ephemeral Curve25519 key. - // 5. Two bytes for the length of the rendezvous ID, a u16 in big-endian + // 5. One byte for the length of the rendezvous ID, a u16 in big-endian // encoding. // 6. The UTF-8 encoded string containing the rendezvous ID. // 7. Two bytes for the length of the server base URL, a u16 in big-endian @@ -218,14 +218,15 @@ impl QrCodeData { reader.read_exact(&mut public_key)?; let public_key = Curve25519PublicKey::from_bytes(public_key); - // 5. We read two bytes for the length of the rendezvous ID. - let rendezvous_id_len = reader.read_u16::()?; + // 5. We read the single byte for the length of the rendezvous ID. + let rendezvous_id_len = reader.read_u8()?; + // 6. We read the rendezvous ID itself. let mut rendezvous_id = vec![0u8; rendezvous_id_len.into()]; reader.read_exact(&mut rendezvous_id)?; let rendezvous_id = String::from_utf8(rendezvous_id).map_err(|e| e.utf8_error())?; - // The length here is guaranteed to be <= u16::MAX because that's the maximum + // The length here is guaranteed to be <= u8::MAX because that's the maximum // amount of bytes we might have read. So we can skip the constructor here. let rendezvous_id = LimitedString(rendezvous_id); @@ -249,7 +250,9 @@ impl QrCodeData { /// The list of bytes can be used by a QR code generator to create an image /// containing a QR code. pub fn to_bytes(&self) -> Vec { - let rendezvous_id_len = self.rendezvous_id.len().to_be_bytes(); + let rendezvous_id_len = self.rendezvous_id.len(); + + dbg!(rendezvous_id_len, &self.rendezvous_id); // if path is / then don't include the trailing slash let base_url = if self.base_url.as_url().path() == "/" { @@ -264,7 +267,7 @@ impl QrCodeData { &[TYPE], &[self.intent.clone() as u8], self.public_key.as_bytes().as_slice(), - &rendezvous_id_len, + &[rendezvous_id_len], self.rendezvous_id.as_bytes(), &base_url_len, base_url.as_bytes(), @@ -286,12 +289,12 @@ mod test { 0x49, 0x4F, 0x5F, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x5F, 0x4D, 0x53, 0x43, 0x34, 0x33, 0x38, 0x38, 0x03, 0x01, 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, 0x24, 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, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x6D, 0x61, 0x74, 0x72, 0x69, - 0x78, 0x2D, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x2E, 0x6D, 0x61, 0x74, 0x72, 0x69, 0x78, - 0x2E, 0x6F, 0x72, 0x67, + 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x24, 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, + 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x6D, 0x61, 0x74, 0x72, 0x69, 0x78, + 0x2D, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x2E, 0x6D, 0x61, 0x74, 0x72, 0x69, 0x78, 0x2E, + 0x6F, 0x72, 0x67, ]; // Test vector for the QR code data, copied from the MSC, with the intent set to @@ -300,16 +303,16 @@ mod test { 0x49, 0x4F, 0x5F, 0x45, 0x4C, 0x45, 0x4D, 0x45, 0x4E, 0x54, 0x5F, 0x4D, 0x53, 0x43, 0x34, 0x33, 0x38, 0x38, 0x03, 0x00, 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, 0x24, 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, 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x6D, 0x61, 0x74, 0x72, 0x69, - 0x78, 0x2D, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x2E, 0x6D, 0x61, 0x74, 0x72, 0x69, 0x78, - 0x2E, 0x6F, 0x72, 0x67, + 0xe5, 0x14, 0x37, 0x02, 0x48, 0xed, 0x6b, 0x24, 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, + 0x20, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3A, 0x2F, 0x2F, 0x6D, 0x61, 0x74, 0x72, 0x69, 0x78, + 0x2D, 0x63, 0x6C, 0x69, 0x65, 0x6E, 0x74, 0x2E, 0x6D, 0x61, 0x74, 0x72, 0x69, 0x78, 0x2E, + 0x6F, 0x72, 0x67, ]; // Test vector for the QR code data in base64 format, self-generated. - const QR_CODE_DATA_BASE64: &str = "SU9fRUxFTUVOVF9NU0M0Mzg4AwG0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PAAaMDFIWDlLMDBRMUg2S1BENDdFRzRHMVQzWEcAJGh0dHBzOi8vc3luYXBzZS1vaWRjLmxhYi5lbGVtZW50LmRldg"; + const QR_CODE_DATA_BASE64: &str = "SU9fRUxFTUVOVF9NU0M0Mzg4AwG0yzZ1QVpQ1jlnoxWX3d5jrWRFfELxjS2gN7pz9y+3PBowMUhYOUswMFExSDZLUEQ0N0VHNEcxVDNYRwAkaHR0cHM6Ly9zeW5hcHNlLW9pZGMubGFiLmVsZW1lbnQuZGV2"; #[test] fn parse_qr_data() { diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs index ac5f93ea66f..ddb479f2fe2 100644 --- a/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/mod.rs @@ -160,8 +160,8 @@ impl RendezvousChannel { let msc_4388::Channel { base_url, rendezvous_id, sequence_token, .. } = channel; let base_url_len: u16 = base_url.len(); - let rendezvous_id_len: u16 = rendezvous_id.len(); - let sequence_token_len: u16 = sequence_token.len(); + let rendezvous_id_len = rendezvous_id.len(); + let sequence_token_len = sequence_token.len(); Some( [