Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9e98b51
chore: Bump vodozemac
poljar Dec 5, 2025
bcf07cc
feat(qr-login): Support HPKE for the cryptographic channel
poljar Dec 5, 2025
058ca49
feat(qr-login): Properly handle HPKE errors in the secure channel
poljar Jan 16, 2026
5ff4384
refactor(qr-login): Move some more error variants into the MessageDec…
poljar Jan 16, 2026
2dc39af
MSC4388 support for the rendezvous channel
poljar Jan 23, 2026
7da8718
feat(oauth): Add support to MSC4388 for the QR login
poljar Feb 6, 2026
20c27cb
test(oauth): Add some more tests for the MSC4388 QR login variant
poljar Feb 6, 2026
a7fafb7
feat(qr-login): Include some additional authenticated data in the HPK…
poljar Feb 23, 2026
c814c35
feat(qr-login): Simplify the check code interface
poljar Feb 27, 2026
c001d5a
feat(qr-login): Add a method to check if the server supports MSC4388 …
poljar Mar 17, 2026
6628810
feat(qr-login): Use the old QR login variant if the homeserver does n…
poljar Mar 17, 2026
472ce0e
feat(ffi): Check for QR login support using the new discovery endpoin…
poljar Mar 17, 2026
7644743
feat(ffi): Request the MSC4388 variant of the QR code login by default
poljar Mar 17, 2026
72ab02b
chore: Enable the wasm_js feature for yet another getrandom version
poljar Mar 18, 2026
9979251
chore: Bump vodozemac
poljar Mar 18, 2026
b6ab4ae
fix(qrlogin): Return false upon a 403 response when we check for MSC4…
poljar Mar 18, 2026
5c00e6f
fix(qr-login): Support the new variant of the m.login.protocols message
poljar Mar 19, 2026
aa5baf9
chore: Bump ruma
poljar Mar 19, 2026
cb359ee
chore(qr-login): Add a note about an implementation mistake for MSC41…
poljar Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,079 changes: 842 additions & 237 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ eyeball-im-util = { version = "0.10.0", default-features = false }
futures-core = { version = "0.3.31", default-features = false, features = ["std"] }
futures-executor = { version = "0.3.31", default-features = false, features = ["std"] }
futures-util = { version = "0.3.31", default-features = false, features = ["std"] }
getrandom = { version = "0.3.4", default-features = false }
getrandom = { version = "0.4.1", default-features = false }
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 }
Expand All @@ -72,7 +72,7 @@ regex = { version = "1.12.2", default-features = false }
reqwest = { version = "0.13.1", default-features = false }
ring = { version = "0.17.14", default-features = false }
rmp-serde = { version = "1.3.0", default-features = false }
ruma = { git = "https://github.com/ruma/ruma", rev = "03ca65151f40304e044f89d8e9e18b68cb9e4f29", features = [
ruma = { git = "https://github.com/ruma/ruma", rev = "7f461cb0115aeeb98c2167363759ef99d1a565ee", features = [
"client-api-c",
"compat-unset-avatar",
"compat-upload-signatures",
Expand All @@ -94,6 +94,7 @@ ruma = { git = "https://github.com/ruma/ruma", rev = "03ca65151f40304e044f89d8e9
"unstable-msc4306",
"unstable-msc4308",
"unstable-msc4310",
"unstable-msc4388",
] }
rustls = { version = "0.23.37", default-features = false, features = ["ring"] }
rustls-pki-types = { version = "1.14.0", default-features = false }
Expand Down Expand Up @@ -121,7 +122,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.9.0", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
vodozemac = { git = "https://github.com/matrix-org/vodozemac/", rev = "7de70ace", default-features = false, features = ["libolm-compat", "insecure-pk-encryption"] }
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 }
Expand Down
34 changes: 30 additions & 4 deletions bindings/matrix-sdk-ffi/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ use ruma::{
api::client::{
alias::get_alias,
discovery::get_authorization_server_metadata::v1::{
AccountManagementActionData, DeviceDeleteData, DeviceViewData,
AccountManagementActionData, DeviceDeleteData, DeviceViewData, GrantType,
},
error::ErrorKind,
profile::{AvatarUrl, DisplayName},
Expand Down Expand Up @@ -1975,10 +1975,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<bool, ClientError> {
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.
Expand Down
28 changes: 12 additions & 16 deletions bindings/matrix-sdk-ffi/src/qr_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ impl LoginWithQrCodeHandler {
.registration_data()
.map_err(|_| HumanQrLoginError::OidcMetadataInvalid)?;

let login = self.oauth.login_with_qr_code(Some(&registration_data)).generate();
let mut login = self.oauth.login_with_qr_code(Some(&registration_data)).generate();
login.with_msc4388_support();

let mut progress = login.subscribe_to_progress();

Expand Down Expand Up @@ -214,7 +215,8 @@ impl GrantLoginWithQrCodeHandler {
self: Arc<Self>,
progress_listener: Box<dyn GrantGeneratedQrLoginProgressListener>,
) -> 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();

Expand Down Expand Up @@ -361,15 +363,14 @@ impl From<qrcode::QRCodeLoginError> for HumanQrLoginError {
}

QRCodeLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => HumanQrLoginError::Unknown,
SecureChannelError::MessageDecode(_) | SecureChannelError::RendezvousChannel(_) => {
HumanQrLoginError::Unknown
}
SecureChannelError::UnsupportedQrCodeType => {
HumanQrLoginError::UnsupportedQrCodeType
}
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::Decryption(_)
| SecureChannelError::InvalidCheckCode
| SecureChannelError::CannotReceiveCheckCode => {
HumanQrLoginError::ConnectionInsecure
Expand Down Expand Up @@ -468,13 +469,12 @@ impl From<qrcode::QRCodeGrantLoginError> for HumanQrGrantLoginError {
}
QRCodeGrantLoginError::NotFound => Self::NotFound,
QRCodeGrantLoginError::SecureChannel(e) => match e {
SecureChannelError::Utf8(_)
| SecureChannelError::MessageDecode(_)
| SecureChannelError::Json(_)
| SecureChannelError::RendezvousChannel(_) => Self::Unknown(e.to_string()),
SecureChannelError::MessageDecode(_) | SecureChannelError::RendezvousChannel(_) => {
Self::Unknown(e.to_string())
}
SecureChannelError::UnsupportedQrCodeType => Self::UnsupportedQrCodeType,
SecureChannelError::SecureChannelMessage { .. }
| SecureChannelError::Ecies(_)
| SecureChannelError::Decryption(_)
| SecureChannelError::InvalidCheckCode
| SecureChannelError::CannotReceiveCheckCode => Self::ConnectionInsecure,
SecureChannelError::InvalidIntent => Self::OtherDeviceAlreadySignedIn,
Expand Down Expand Up @@ -531,8 +531,6 @@ impl From<qrcode::LoginProgress<QrProgress>> 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}"),
Expand Down Expand Up @@ -633,8 +631,6 @@ impl From<qrcode::GrantLoginProgress<QrProgress>> 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}"),
Expand Down
1 change: 1 addition & 0 deletions crates/matrix-sdk-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-crypto/src/olm/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ mod tests {
let one_time_key = *bob.one_time_keys().values().next().unwrap();
let sender_key = bob.identity_keys().curve25519;
let mut alice_session = alice.create_outbound_session_helper(
SessionConfig::default(),
SessionConfig::version_2(),
sender_key,
one_time_key,
false,
Expand Down
2 changes: 1 addition & 1 deletion crates/matrix-sdk-qrcode/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 28 additions & 2 deletions crates/matrix-sdk/src/authentication/oauth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,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<bool, HttpError> {
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
Expand Down Expand Up @@ -1381,8 +1408,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}")
Expand Down
Loading
Loading