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..1517ae4effb 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 } @@ -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 } @@ -119,7 +120,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/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 020b2409b0e..a9a7ef5f00a 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -80,7 +80,7 @@ use ruma::{ client::{ alias::get_alias, discovery::get_authorization_server_metadata::v1::{ - AccountManagementActionData, DeviceDeleteData, DeviceViewData, + AccountManagementActionData, DeviceDeleteData, DeviceViewData, GrantType, }, profile::{AvatarUrl, DisplayName}, room::create_room::{RoomPowerLevelsContentOverride, v3::CreationContent}, @@ -1976,10 +1976,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. diff --git a/bindings/matrix-sdk-ffi/src/qr_code.rs b/bindings/matrix-sdk-ffi/src/qr_code.rs index 96ffc4cef10..1edeb3df2c0 100644 --- a/bindings/matrix-sdk-ffi/src/qr_code.rs +++ b/bindings/matrix-sdk-ffi/src/qr_code.rs @@ -17,8 +17,9 @@ use std::sync::Arc; use matrix_sdk::authentication::oauth::{ OAuth, qrcode::{ - self, CheckCodeSender as SdkCheckCodeSender, CheckCodeSenderError, - DeviceCodeErrorResponseType, GeneratedQrProgress, LoginFailureReason, QrProgress, + self, CheckCodeSender as SdkCheckCodeSender, + ContinuationMessageSender as SdkContinuationMessageSender, DeviceCodeErrorResponseType, + GeneratedQrProgress, LoginFailureReason, QrProgress, SenderError, }, }; use matrix_sdk_base::crypto::types::qr_login::{self, QrCodeIntent}; @@ -120,7 +121,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 +216,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(); @@ -361,15 +364,14 @@ impl From for HumanQrLoginError { } QRCodeLoginError::SecureChannel(e) => match e { - SecureChannelError::Utf8(_) - | SecureChannelError::MessageDecode(_) - | SecureChannelError::Json(_) - | SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown, + SecureChannelError::MessageDecode(_) + | SecureChannelError::RendezvousChannel(_) + | SecureChannelError::QrCodeCreationError(_) => HumanQrLoginError::Unknown, SecureChannelError::UnsupportedQrCodeType => { HumanQrLoginError::UnsupportedQrCodeType } SecureChannelError::SecureChannelMessage { .. } - | SecureChannelError::Ecies(_) + | SecureChannelError::Decryption(_) | SecureChannelError::InvalidCheckCode | SecureChannelError::CannotReceiveCheckCode => { HumanQrLoginError::ConnectionInsecure @@ -390,11 +392,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, } } } @@ -468,13 +470,12 @@ 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(_) + | SecureChannelError::QrCodeCreationError(_) => 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, @@ -531,8 +532,6 @@ impl From> for QrLoginProgress { match value { LoginProgress::Starting => Self::Starting, LoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let check_code = check_code.to_digit(); - Self::EstablishingSecureChannel { check_code, check_code_string: format!("{check_code:02}"), @@ -611,9 +610,13 @@ 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, @@ -633,15 +636,20 @@ impl From> for GrantQrLoginProgress { match value { GrantLoginProgress::Starting => Self::Starting, GrantLoginProgress::EstablishingSecureChannel(QrProgress { check_code }) => { - let check_code = check_code.to_digit(); - Self::EstablishingSecureChannel { check_code, check_code_string: format!("{check_code:02}"), } } - GrantLoginProgress::WaitingForAuth { verification_uri } => { - Self::WaitingForAuth { verification_uri: verification_uri.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, @@ -658,16 +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_sender: Arc, + }, + WaitingForAuth { + continuation_sender: Arc, }, /// We are syncing secrets. SyncingSecrets, @@ -692,8 +708,15 @@ 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::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, @@ -701,9 +724,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, } @@ -721,3 +744,30 @@ impl CheckCodeSender { self.inner.send(code).await.map_err(HumanQrLoginError::from) } } + +/// 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, +} + +#[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) + } +} + +impl From for ContinuationMessageSender { + fn from(value: SdkContinuationMessageSender) -> Self { + Self { inner: value } + } +} diff --git a/crates/matrix-sdk-common/Cargo.toml b/crates/matrix-sdk-common/Cargo.toml index 92500766ada..34f3e5ebc16 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 @@ -66,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] 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-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..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 @@ -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 +/// [`u8::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() <= u8::MAX as usize { Some(Self(s)) } else { None } + } + + /// Return the length of the string. + /// + /// Is returned as an `u8` as it is guaranteed to be <= [`u8::MAX`]. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> u8 { + self.0.len() as u8 + } + + /// 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 [`u8::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 { @@ -94,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 @@ -128,13 +218,18 @@ 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 <= 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); + // 7. We read the two bytes for the length of the server base URL. let base_url_len = reader.read_u16::()?; @@ -142,6 +237,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 +250,12 @@ 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(); + + dbg!(rendezvous_id_len, &self.rendezvous_id); // 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() @@ -169,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(), @@ -191,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 @@ -205,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() { @@ -237,7 +335,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 +362,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 +405,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-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/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index 14eff34730b..61251fbe4b0 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -370,6 +370,33 @@ 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 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(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) + }) { + Ok(false) + } else { + Err(e) + } + } + } + } + /// Log in this device using a QR code. /// /// # Arguments @@ -1432,8 +1459,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}") @@ -1625,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, /// } @@ -1702,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, /// } /// } diff --git a/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/grant.rs index b2aced751a2..1991e328867 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. @@ -36,8 +36,9 @@ use super::{ use crate::{ Client, authentication::oauth::qrcode::{ - CheckCodeSender, GeneratedQrProgress, LoginFailureReason, QRCodeGrantLoginError, - QrProgress, SecureChannelError, + 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::OpeningVerificationUri { + verification_uri, + continuation_sender: ContinuationMessageSender(CloneableSender::new(sender)), + }); + + // 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 + // 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. @@ -127,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 @@ -192,10 +238,21 @@ 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 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 /// secrets to it. @@ -269,7 +326,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 })); @@ -280,9 +337,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?; @@ -307,6 +371,7 @@ pub struct GrantLoginWithGeneratedQrCode<'a> { client: &'a Client, device_creation_timeout: Duration, state: SharedObservable>, + msc_4388_support: bool, } impl<'a> GrantLoginWithGeneratedQrCode<'a> { @@ -314,7 +379,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 +400,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> { @@ -338,20 +416,33 @@ 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 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, 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 @@ -363,7 +454,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)?; @@ -380,11 +471,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 }) @@ -397,7 +488,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}; @@ -408,7 +499,7 @@ mod test { use super::*; use crate::{ authentication::oauth::qrcode::{ - LoginFailureReason, QrAuthMessage, + LoginFailureReason, MessageDecodeError, QrAuthMessage, messages::{AuthorizationGrant, LoginProtocolType}, secure_channel::{EstablishedSecureChannel, test::MockedRendezvousServer}, }, @@ -453,9 +544,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()) - .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 => { @@ -611,6 +700,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"); @@ -626,9 +720,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); @@ -772,11 +884,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 { @@ -821,10 +933,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"); @@ -875,7 +992,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -883,6 +1003,11 @@ 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 { .. }); @@ -917,10 +1042,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 { @@ -951,7 +1086,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, msc_4388) .await .expect("Bob should be able to create a secure channel."); let qr_code_data = channel.qr_code_data().clone(); @@ -1000,16 +1135,26 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + 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::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 { .. }); } @@ -1043,11 +1188,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 { @@ -1080,7 +1236,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(); @@ -1129,15 +1285,23 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + 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::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 { .. }); @@ -1177,7 +1341,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); @@ -1303,7 +1468,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); @@ -1325,7 +1491,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(); @@ -1369,7 +1535,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); break; } @@ -1414,7 +1580,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 { @@ -1544,7 +1711,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 { @@ -1575,7 +1743,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(); @@ -1619,7 +1787,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } _ => { @@ -1659,7 +1827,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 { @@ -1755,7 +1924,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -1763,6 +1935,11 @@ 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"); @@ -1799,7 +1976,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 { @@ -1830,7 +2008,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(); @@ -1876,15 +2054,23 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + 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::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -1921,9 +2107,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; @@ -1996,9 +2186,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; @@ -2019,7 +2213,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(); @@ -2077,7 +2271,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); @@ -2200,7 +2395,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); @@ -2222,7 +2418,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(); @@ -2266,7 +2462,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); break; } @@ -2308,7 +2504,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); @@ -2405,7 +2602,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -2413,6 +2613,11 @@ 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"); @@ -2453,7 +2658,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); @@ -2485,7 +2691,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(); @@ -2531,15 +2737,23 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + 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::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -2581,7 +2795,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); @@ -2678,7 +2893,10 @@ mod test { .await .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + verification_uri, + continuation_sender, + } => { assert_matches!( state, GrantLoginProgress::EstablishingSecureChannel( @@ -2686,6 +2904,11 @@ 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"); @@ -2727,7 +2950,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); @@ -2759,7 +2983,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(); @@ -2805,15 +3029,23 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); } - GrantLoginProgress::WaitingForAuth { verification_uri } => { + GrantLoginProgress::OpeningVerificationUri { + 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::WaitingForAuth { continuation_sender } => { + assert_matches!(state, GrantLoginProgress::OpeningVerificationUri { .. }); + continuation_sender.confirm().await.expect("should be able to confirm"); } _ => { panic!("Alice should abort the process"); @@ -2856,7 +3088,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); @@ -2968,7 +3201,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"); @@ -2979,7 +3214,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); @@ -3001,7 +3237,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(); @@ -3045,7 +3281,7 @@ mod test { checkcode_tx .take() .expect("The checkcode should only be forwarded once") - .send(check_code.to_digit()) + .send(*check_code) .expect("Alice should be able to forward the checkcode"); break; } @@ -3076,7 +3312,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/login.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/login.rs index 7a118ec0ae2..a8b80f5db62 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. @@ -40,7 +40,10 @@ use crate::{ Client, authentication::oauth::{ ClientRegistrationData, OAuth, OAuthError, - qrcode::{CheckCodeSender, GeneratedQrProgress, LoginProtocolType, QrProgress}, + qrcode::{ + CheckCodeSender, GeneratedQrProgress, LoginProtocolType, QrProgress, + messages::LoginProtocolsMessage, + }, }, }; @@ -301,6 +304,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."); @@ -354,6 +359,7 @@ pub struct LoginWithGeneratedQrCode<'a> { client: &'a Client, registration_data: Option<&'a ClientRegistrationData>, state: SharedObservable>, + msc_4388_support: bool, } impl LoginWithGeneratedQrCode<'_> { @@ -366,6 +372,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> { @@ -376,6 +390,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."); @@ -388,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 { @@ -405,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?; @@ -436,25 +476,33 @@ 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( &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()).await?; + let secure_channel = + 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, ))); @@ -469,7 +517,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), ))); @@ -495,12 +543,11 @@ mod test { use matrix_sdk_common::executor::spawn; use matrix_sdk_test::async_test; use serde_json::json; - use vodozemac::ecies::CheckCode; use super::*; use crate::{ authentication::oauth::qrcode::{ - messages::LoginProtocolType, + messages::{LoginProtocolType, LoginProtocolsMessage}, secure_channel::{SecureChannel, test::MockedRendezvousServer}, }, config::RequestConfig, @@ -540,7 +587,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"); @@ -548,9 +595,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()) - .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() @@ -586,11 +632,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(); @@ -611,16 +657,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) + 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) @@ -668,6 +726,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, @@ -688,7 +756,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(); let check_code_sender = cctx_receiver.await.expect("Alice should receive the CheckCodeSender"); @@ -699,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) @@ -751,11 +826,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(); @@ -791,18 +866,33 @@ 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"); - - assert_matches!( - secure_channel.qr_code_data().intent_data(), - QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } - ); + let secure_channel = + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, msc_4388) + .await + .expect("Bob should be able to create a secure channel"); + + 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 { @@ -856,10 +946,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(); @@ -899,18 +999,33 @@ 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"); - - assert_matches!( - secure_channel.qr_code_data().intent_data(), - QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } - ); + let secure_channel = + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, msc_4388) + .await + .expect("Bob should be able to create a secure channel"); + + 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 { @@ -963,9 +1078,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 { @@ -973,7 +1099,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(); @@ -1015,16 +1142,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) + 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) @@ -1069,6 +1210,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 { @@ -1076,7 +1218,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(); @@ -1134,18 +1277,33 @@ 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"); - - assert_matches!( - secure_channel.qr_code_data().intent_data(), - QrCodeIntentData::Msc4108 { data: Msc4108IntentData::Login, .. } - ); + let secure_channel = + SecureChannel::login(bob.inner.http_client.clone(), &homeserver_url, msc_4388) + .await + .expect("Bob should be able to create a secure channel"); + + 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 { @@ -1186,9 +1344,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!( @@ -1199,9 +1357,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!( @@ -1212,8 +1383,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!( @@ -1224,9 +1405,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!( @@ -1237,8 +1431,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!( @@ -1249,9 +1453,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!( @@ -1262,37 +1476,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; @@ -1301,41 +1557,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(); @@ -1352,7 +1659,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/messages.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/messages.rs index a65aa789d2c..cc256729e30 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,31 @@ 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. + /// + /// Note: this doesn't match the MSC which says that it is a server name + /// not a full URL + homeserver: Url, + }, +} + impl QrAuthMessage { /// Create a new [`QrAuthMessage::LoginProtocol`] message with the /// [`LoginProtocolType::DeviceAuthorizationGrant`] protocol type. @@ -146,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), } @@ -170,17 +193,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/mod.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/mod.rs index 7857eb22c69..ee37e14787c 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, @@ -37,8 +39,10 @@ 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}; +pub use vodozemac::{ + ecies::{Error as EciesError, MessageDecodeError as EciesMessageDecodeError}, + hpke::{Error as HpkeError, MessageDecodeError as HpkeMessageDecodeError}, +}; mod grant; mod login; @@ -253,26 +257,66 @@ impl DeviceAuthorizationOAuthError { } } -/// Error type for failures in when receiving or sending messages over the -/// secure channel. +/// Error type which describes failures when messages which are received over +/// the secure channel fail to be decoded. #[derive(Debug, Error)] -pub enum SecureChannelError { +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 has failed to be decrypted. + /// 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. +#[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), +} - /// A received message has failed to be decoded. +/// Error type for failures in when receiving or sending messages over the +/// secure channel. +#[derive(Debug, Error)] +pub enum SecureChannelError { + /// A message has failed to be decrypted. #[error(transparent)] - MessageDecode(#[from] MessageDecodeError), + Decryption(#[from] DecryptionError), - /// A message couldn't be deserialized from JSON. + /// A received message has failed to be decoded. #[error(transparent)] - Json(#[from] serde_json::Error), + MessageDecode(#[from] MessageDecodeError), /// The secure channel failed to be established because it received an /// unexpected message. @@ -311,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`] @@ -321,12 +369,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 @@ -335,7 +383,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 { @@ -343,41 +391,77 @@ 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), } /// 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 checkcode. + /// Send the check code. /// /// Calling this method more than once will result in an error. /// /// # 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> { + self.send_impl(check_code).await + } +} + +/// 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 + } +} + +/// A oneshot sender we are able to clone so we can put it into a +/// observable. +#[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))) } + } + + async fn send_impl(&self, message: T) -> 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(message).map_err(|_| SenderError::CannotSend), + None => Err(SenderError::AlreadySent), } } } /// 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, @@ -385,3 +469,134 @@ pub enum CheckCodeSenderError { #[error("check code cannot be sent.")] CannotSend, } + +#[cfg(test)] +mod tests { + use matrix_sdk_test::async_test; + use serde_json::json; + 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).set_body_json(json!({ + "create_available": true, + }))) + .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(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() + .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 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" + ); + } + + { + 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"); + } + } +} 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..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 @@ -12,14 +12,18 @@ // 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; use crate::{ - HttpError, authentication::oauth::qrcode::SecureChannelError, http_client::HttpClient, + HttpError, + authentication::oauth::qrcode::{MessageDecodeError, SecureChannelError}, + http_client::HttpClient, }; mod msc_4108; +mod msc_4388; /// The result of the [`RendezvousChannel::create_inbound()`] method. pub(super) struct InboundChannelCreationResult { @@ -36,10 +40,12 @@ pub(super) struct InboundChannelCreationResult { #[derive(Debug, PartialEq, Eq)] pub(super) enum RendezvousInfo<'a> { Msc4108 { rendezvous_url: &'a Url }, + Msc4388 { rendezvous_id: &'a RendezvousId }, } pub(super) enum RendezvousChannel { Msc4108(msc_4108::Channel), + Msc4388(msc_4388::Channel), } impl RendezvousChannel { @@ -51,8 +57,15 @@ impl RendezvousChannel { pub(super) async fn create_outbound( client: HttpClient, rendezvous_server: &Url, - ) -> Result { - Ok(Self::Msc4108(msc_4108::Channel::create_outbound(client, rendezvous_server).await?)) + msc_4388: bool, + ) -> Result { + if msc_4388 { + 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?)) + } } /// Create a new inbound [`RendezvousChannel`]. @@ -69,6 +82,27 @@ 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: &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?; + + 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<'_> { @@ -76,6 +110,9 @@ impl RendezvousChannel { RendezvousChannel::Msc4108(channel) => { RendezvousInfo::Msc4108 { rendezvous_url: channel.rendezvous_url() } } + RendezvousChannel::Msc4388(channel) => { + RendezvousInfo::Msc4388 { rendezvous_id: channel.rendezvous_id() } + } } } @@ -84,9 +121,10 @@ 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, } } @@ -99,10 +137,44 @@ 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?, - }; + 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?), + } + } - Ok(String::from_utf8(message).map_err(|e| e.utf8_error())?) + /// 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; + + let base_url_len: u16 = base_url.len(); + let rendezvous_id_len = rendezvous_id.len(); + let sequence_token_len = 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 new file mode 100644 index 00000000000..b03616b5ab3 --- /dev/null +++ b/crates/matrix-sdk/src/authentication/oauth/qrcode/rendezvous_channel/msc_4388.rs @@ -0,0 +1,554 @@ +// 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::{ + crypto::types::qr_login::{LimitedString, LimitedUrl, RendezvousId}, + 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, + authentication::oauth::qrcode::{MessageDecodeError, SecureChannelError}, + 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, +} + +/// 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, +} + +pub(crate) struct Channel { + client: HttpClient, + pub(super) base_url: LimitedUrl, + /// The ID of the rendezvous session we're using to exchange messages + /// through the channel. + pub(super) rendezvous_id: RendezvousId, + pub(super) sequence_token: SequenceToken, +} + +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: &LimitedUrl, + ) -> 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 = + 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 }) + } + + /// 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: &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.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 }; + + 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) -> &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<(), SecureChannelError> { + let request = update_rendezvous_session::unstable::Request::new( + self.rendezvous_id.to_string(), + self.sequence_token.to_string(), + 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 = SequenceToken::new(response.sequence_token) + .ok_or(MessageDecodeError::TooLongSequenceToken)?; + + 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.into()); + } + } + } + + async fn receive_message_impl( + client: &HttpClient, + base_url: &Url, + rendezvous_id: &RendezvousId, + ) -> Result { + let request = + get_rendezvous_session::unstable::Request::new(rendezvous_id.as_str().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.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 self.sequence_token == new_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 = new_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: &RendezvousId, + ) -> Result { + server + .register(Mock::given(method("POST")).and(path(BASE_PATH)).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "id": rendezvous_id.as_str(), + "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 = 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) + .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.as_str(), + "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).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.as_str(), + "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 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"); + + 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.as_str(), + "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 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"); + + 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/crypto_channel.rs b/crates/matrix-sdk/src/authentication/oauth/qrcode/secure_channel/crypto_channel.rs index ba8c32d2c3f..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,14 +30,20 @@ use vodozemac::{ Curve25519PublicKey, - ecies::{CheckCode, Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message}, + ecies::{Ecies, EstablishedEcies, InboundCreationResult, InitialMessage, Message}, + hpke::{ + self, DigitMode, 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 { Ecies(Ecies), + Hpke(HpkeRecipientChannel), } impl CryptoChannel { @@ -46,10 +52,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(), } } @@ -57,11 +69,21 @@ impl CryptoChannel { pub(super) fn establish_inbound_channel( self, message: &str, + aad: &[u8], ) -> 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).map_err(MessageDecodeError::from)?; + Ok(CryptoChannelCreationResult::Hpke( + hpke.establish_channel(&message, aad).map_err(DecryptionError::from)?, + )) } } } @@ -69,6 +91,7 @@ impl CryptoChannel { pub(super) enum CryptoChannelCreationResult { Ecies(InboundCreationResult), + Hpke(RecipientCreationResult), } impl CryptoChannelCreationResult { @@ -78,6 +101,7 @@ impl CryptoChannelCreationResult { CryptoChannelCreationResult::Ecies(inbound_creation_result) => { &inbound_creation_result.message } + CryptoChannelCreationResult::Hpke(result) => &result.message, } } } @@ -88,35 +112,49 @@ impl CryptoChannelCreationResult { /// cryptographic messages. pub(super) enum EstablishedCryptoChannel { Ecies(EstablishedEcies), + Hpke(EstablishedHpkeChannel), } 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().to_digit(DigitMode::NoLeadingZero) + } } } /// 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)? + 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).map_err(MessageDecodeError::from)?; + channel.open(&message, aad).map_err(DecryptionError::from)? } }; - 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 29c0fa025f7..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 @@ -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. @@ -19,15 +19,23 @@ use matrix_sdk_base::crypto::types::qr_login::{ use serde::{Serialize, de::DeserializeOwned}; use tracing::{instrument, trace}; use url::Url; -use vodozemac::ecies::{ - CheckCode, Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult, +use vodozemac::{ + ecies::{Ecies, EstablishedEcies, InboundCreationResult, OutboundCreationResult}, + hpke::{ + BidirectionalCreationResult, HpkeSenderChannel, InitialResponse, RecipientCreationResult, + SenderCreationResult, UnidirectionalSenderChannel, + }, }; 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"; @@ -44,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 } => { @@ -58,6 +68,19 @@ impl SecureChannel { intent_data, ); + (crypto_channel, qr_code_data) + } + RendezvousInfo::Msc4388 { rendezvous_id, .. } => { + let crypto_channel = CryptoChannel::new_hpke(); + + let qr_code_data = QrCodeData::new_msc4388( + crypto_channel.public_key(), + // TODO: Avoid the double conversion here? + rendezvous_id.as_str().to_owned(), + homeserver_url.clone(), + QrCodeIntent::Login, + )?; + (crypto_channel, qr_code_data) } }; @@ -69,8 +92,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 } => { @@ -83,6 +107,15 @@ impl SecureChannel { mode_data, ); } + RendezvousInfo::Msc4388 { rendezvous_id, .. } => { + channel.qr_code_data = QrCodeData::new_msc4388( + channel.crypto_channel.public_key(), + // TODO: Avoid the double conversion here? + rendezvous_id.as_str().to_owned(), + homeserver_url.clone(), + QrCodeIntent::Reciprocate, + )?; + } } Ok(channel) @@ -96,10 +129,11 @@ 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())?; + let message = std::str::from_utf8(result.plaintext()).map_err(MessageDecodeError::from)?; trace!("Received the initial secure channel message"); @@ -116,6 +150,17 @@ impl SecureChannel { secure_channel.send(LOGIN_OK_MESSAGE).await?; 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(), &aad); + + self.channel.send(message.encode()).await?; + + let crypto_channel = EstablishedCryptoChannel::Hpke(channel); + + EstablishedSecureChannel { channel: self.channel, crypto_channel } + } }; Ok(AlmostEstablishedSecureChannel { secure_channel }) @@ -140,7 +185,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() { Ok(self.secure_channel) } else { Err(Error::InvalidCheckCode) @@ -163,6 +208,7 @@ impl EstablishedSecureChannel { ) -> Result { enum ChannelType { Ecies(EstablishedEcies), + Hpke(UnidirectionalSenderChannel), } if qr_code_data.intent() == expected_mode { @@ -172,20 +218,6 @@ impl EstablishedSecureChannel { let client = HttpClient::new(client, RequestConfig::short_retry()); - // Let's establish an outbound ECIES 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`. - let (crypto_channel, encoded_message) = { - let ecies = Ecies::new(); - - let OutboundCreationResult { ecies, message } = ecies.establish_outbound_channel( - qr_code_data.public_key(), - LOGIN_INITIATE_MESSAGE.as_bytes(), - )?; - (ChannelType::Ecies(ecies), 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 @@ -196,9 +228,42 @@ 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 + } + }; + + // 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`. + 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 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(), + &aad, + ) + .map_err(DecryptionError::from)?; + (ChannelType::Hpke(channel), message.encode()) + } }; trace!( @@ -213,15 +278,37 @@ 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 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, &aad) + .map_err(DecryptionError::from)?; + 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 + // 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."); @@ -234,10 +321,17 @@ 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. - pub(super) fn check_code(&self) -> &CheckCode { + pub(super) fn check_code(&self) -> u8 { self.crypto_channel.check_code() } @@ -246,7 +340,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 } @@ -256,18 +350,23 @@ 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> { - let message = self.crypto_channel.seal(message); + let aad = self.channel.additional_authenticated_data().unwrap_or_default(); - Ok(self.channel.send(message).await?) + let message = self.crypto_channel.seal(message, &aad); + 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) } } @@ -285,8 +384,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 wiremock::{ Mock, MockGuard, MockServer, ResponseTemplate, @@ -307,10 +408,28 @@ pub(super) mod test { post_guard: MockGuard, put_guard: MockGuard, get_guard: MockGuard, + discover_guard: Option, } 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)); @@ -425,18 +544,154 @@ pub(super) mod test { get_guard, homeserver_url, rendezvous_url, + discover_guard: None, + } + } + + 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 discover_guard = server + .register_as_scoped( + Mock::given(method("GET")) + .and(path("/_matrix/client/unstable/io.element.msc4388/rendezvous")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "create_available": true, + }))), + ) + .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, + discover_guard: Some(discover_guard), } } } - #[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) + let alice = SecureChannel::reciprocate(client, &rendezvous_server.homeserver_url, msc_4388) .await .expect("Alice should be able to create a secure channel."); @@ -465,9 +720,19 @@ 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()) .expect("Alice should be able to confirm the established secure channel."); 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; + } } 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}")