Compare commits

..

5 Commits

Author SHA1 Message Date
Ginger 7bf32f6b09 chore: News fragment 2026-07-13 09:38:39 -04:00
Ginger 484392b700 fix: Allow unstable and stable device id query together 2026-07-13 09:37:46 -04:00
Ginger ce494eb0c0 fix: Properly sync newly created rooms 2026-07-13 08:50:53 -04:00
Ginger 1f5e178c3f fix: Properly handle appservice device creation 2026-07-13 08:50:48 -04:00
Renovate Bot ddd050d402 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v43.260.2 2026-07-13 05:03:24 +00:00
14 changed files with 92 additions and 84 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:43.252.1@sha256:121eb04ef758537019fb58f587aa53c99e5fda4e703993ce2ca01bd4b3926bd4
image: ghcr.io/renovatebot/renovate:43.260.2@sha256:60d830108d9ff6408d11a55d5f110ee0976bed78a6b82b08211e3ddb72f72952
options: --tmpfs /tmp:exec
steps:
- name: Checkout
+1
View File
@@ -0,0 +1 @@
Appservices are now properly able to create devices for E2EE.
+1
View File
@@ -0,0 +1 @@
Fixed newly created rooms failing to sync properly in clients using legacy sync.
+1
View File
@@ -0,0 +1 @@
Appservices may now specify both the unstable and stable `device_id` query parameters in a request. The stable parameter will take priority. Contributed by @ginger.
+1 -1
View File
@@ -26,7 +26,7 @@
};
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use super::DEVICE_ID_LENGTH;
use crate::{Ruma, router::ClientIdentity};
pub(crate) mod register;
+8 -6
View File
@@ -20,9 +20,12 @@
assign,
};
use serde_json::value::RawValue;
use service::{mailer::messages, users::HashedPassword};
use service::{
mailer::messages,
users::{DeviceToken, HashedPassword},
};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use super::DEVICE_ID_LENGTH;
use crate::Ruma;
/// # `POST /_matrix/client/v3/register`
@@ -119,7 +122,7 @@ pub(crate) async fn register_route(
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device
let new_token = utils::random_string(TOKEN_LENGTH);
let new_token = DeviceToken::new_random();
// Create device for this account
services
@@ -127,8 +130,7 @@ pub(crate) async fn register_route(
.create_device(
&user_id,
&device_id,
&new_token,
None,
Some(new_token.clone()),
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
@@ -142,7 +144,7 @@ pub(crate) async fn register_route(
debug_info!(%user_id, ?device, "New account created via legacy registration");
Ok(assign!(register::v3::Response::new(user_id), {
access_token: token,
access_token: token.map(DeviceToken::into_token),
device_id: device,
refresh_token: None,
expires_in: None,
+1 -8
View File
@@ -89,14 +89,7 @@ pub(crate) async fn update_device_route(
services
.users
.create_device(
sender_user,
&device_id,
&appservice.registration.as_token,
None,
None,
Some(client.to_string()),
)
.create_device(sender_user, &device_id, None, None, Some(client.to_string()))
.await?;
return Ok(update_device::v3::Response::new());
+8 -8
View File
@@ -29,8 +29,9 @@
},
assign,
};
use service::users::DeviceToken;
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use super::DEVICE_ID_LENGTH;
use crate::Ruma;
/// # `GET /_matrix/client/v3/login`
@@ -196,8 +197,8 @@ pub(crate) async fn login_route(
.clone()
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate a new token for the device (ensuring no collisions)
let token = services.users.generate_unique_token().await;
// Generate a new token for the device
let token = DeviceToken::new_random();
// Determine if device_id was provided and exists in the db for this user
let device_exists = if body.device_id.is_some() {
@@ -213,7 +214,7 @@ pub(crate) async fn login_route(
if device_exists {
services
.users
.set_token(&user_id, &device_id, &token, None)
.set_token(&user_id, &device_id, token.clone())
.await?;
} else {
services
@@ -221,8 +222,7 @@ pub(crate) async fn login_route(
.create_device(
&user_id,
&device_id,
&token,
None,
Some(token.clone()),
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
@@ -241,7 +241,7 @@ pub(crate) async fn login_route(
info!("{user_id} logged in");
#[allow(deprecated)]
Ok(assign!(login::v3::Response::new(user_id, token, device_id), {
Ok(assign!(login::v3::Response::new(user_id, token.into_token(), device_id), {
well_known: client_discovery_info,
expires_in: None,
home_server: Some(services.config.server_name.clone()),
@@ -273,7 +273,7 @@ pub(crate) async fn login_token_route(
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
.await?;
let login_token = utils::random_string(TOKEN_LENGTH);
let login_token = DeviceToken::new_random().into_token();
let expires_in = services.users.create_login_token(sender_user, &login_token);
Ok(get_login_token::v1::Response::new(
+2 -3
View File
@@ -15,10 +15,9 @@
#[derive(Deserialize)]
pub(crate) struct AuthQueryParams {
pub(super) user_id: Option<String>,
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
#[serde(alias = "org.matrix.msc3202.device_id")]
pub(super) device_id: Option<String>,
#[serde(rename = "org.matrix.msc3202.device_id")]
pub(super) legacy_device_id: Option<String>,
}
/// Extractor for Ruma request structs
+22 -18
View File
@@ -219,25 +219,29 @@ async fn verify<B: AsRef<[u8]> + Sync>(
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
// The device_id can be provided via `device_id` or
// `org.matrix.msc3202.device_id` query parameter.
let sender_device =
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
// Verify the device exists for this user
if services
.users
.get_device_metadata(&sender_user, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as \
this device."
)));
}
let sender_device = if let Some(device_id) = query
.device_id
.or(query.legacy_device_id)
.as_deref()
.map(Into::into)
{
// Verify the device exists for this user
if services
.users
.get_device_metadata(&sender_user, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as this \
device."
)));
}
Some(device_id.to_owned())
} else {
None
};
Some(device_id.to_owned())
} else {
None
};
Ok(ClientIdentity::Appservice {
sender_user,
+7 -9
View File
@@ -25,7 +25,7 @@
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
},
},
users,
users::{self, DeviceToken},
};
pub mod client_metadata;
@@ -343,7 +343,7 @@ async fn create_session(
client_name: Option<String>,
client_id: String,
) -> Result<TokenResponse, OAuthError> {
let access_token = Self::generate_token();
let access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let refresh_token = Self::generate_token();
let device_id = requested_scopes
@@ -376,8 +376,7 @@ async fn create_session(
.create_device(
&authorizing_user,
device_id,
&access_token,
Some(Self::ACCESS_TOKEN_MAX_AGE),
Some(access_token.clone()),
client_name,
None,
)
@@ -413,7 +412,7 @@ async fn create_session(
);
Ok(TokenResponse {
access_token,
access_token: access_token.into_token(),
token_type: TokenType::Bearer,
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
scope: requested_scopes.iter().join(" "),
@@ -449,7 +448,7 @@ async fn refresh_session(
assert_eq!(&client_id, &session_info.client_id, "session info client id mismatch");
let new_access_token = Self::generate_token();
let new_access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let new_refresh_token = Self::generate_token();
let scope = session_info.scopes.iter().join(" ");
session_info
@@ -461,8 +460,7 @@ async fn refresh_session(
.set_token(
&refresh_token_info.user_id,
&refresh_token_info.device_id,
&new_access_token,
Some(Self::ACCESS_TOKEN_MAX_AGE),
new_access_token.clone(),
)
.await
.expect("should be able to set token");
@@ -479,7 +477,7 @@ async fn refresh_session(
.raw_put(&new_refresh_token, Json(refresh_token_info));
Ok(TokenResponse {
access_token: new_access_token,
access_token: new_access_token.into_token(),
token_type: TokenType::Bearer,
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
scope,
-1
View File
@@ -41,7 +41,6 @@ pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) ->
self.create_device(
user_id,
&request.device_id,
"",
None,
request.initial_device_display_name.clone(),
None,
+38 -29
View File
@@ -11,20 +11,44 @@
use futures::{Stream, StreamExt};
use ruma::{
DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
api::client::device::Device, events::AnyToDeviceEvent, serde::Raw, uint,
api::client::device::Device, assign, events::AnyToDeviceEvent, serde::Raw, uint,
};
use serde_json::json;
use crate::users::increment;
#[must_use]
#[derive(Clone)]
pub struct DeviceToken {
token: String,
max_age: Option<Duration>,
}
impl DeviceToken {
const DEVICE_ID_LENGTH: usize = 10;
pub fn new(token: String) -> Self { Self { token, max_age: None } }
pub fn new_random() -> Self { Self::new(utils::random_string(Self::DEVICE_ID_LENGTH)) }
pub fn with_max_age(self, max_age: Duration) -> Self {
assign!(self, { max_age: Some(max_age) })
}
#[must_use]
pub fn into_token(self) -> String { self.token }
}
impl super::Service {
/// Adds a new device to a user.
///
/// If no `token` is provided, the device will not be able to be logged
/// into.
pub async fn create_device(
&self,
user_id: &UserId,
device_id: &DeviceId,
token: &str,
token_max_age: Option<Duration>,
token: Option<DeviceToken>,
initial_device_display_name: Option<String>,
client_ip: Option<String>,
) -> Result<()> {
@@ -38,8 +62,12 @@ pub async fn create_device(
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
self.db.userdeviceid_metadata.put(key, Json(device));
self.set_token(user_id, device_id, token, token_max_age)
.await
if let Some(token) = token {
self.set_token(user_id, device_id, token).await?;
}
Ok(())
}
/// Removes a device from a user.
@@ -97,31 +125,12 @@ pub async fn get_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result<
self.db.userdeviceid_token.qry(&key).await.deserialized()
}
/// Generate a unique access token that doesn't collide with existing tokens
pub async fn generate_unique_token(&self) -> String {
loop {
let token = utils::random_string(32);
// Check for collision with existing appservice and user tokens
let (appservice, usr) = tokio::join!(
self.services.appservice.find_from_token(&token),
self.db.token_userdeviceid.get(&token)
);
if appservice.is_ok() || usr.is_ok() {
continue;
}
return token;
}
}
/// Replaces the access token of one device.
pub async fn set_token(
&self,
user_id: &UserId,
device_id: &DeviceId,
token: &str,
token_max_age: Option<Duration>,
DeviceToken { token, max_age }: DeviceToken,
) -> Result<()> {
let key = (user_id, device_id);
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
@@ -136,7 +145,7 @@ pub async fn set_token(
if self
.services
.appservice
.find_from_token(token)
.find_from_token(&token)
.await
.is_ok()
{
@@ -153,10 +162,10 @@ pub async fn set_token(
}
// Assign token to user device combination
self.db.userdeviceid_token.put_raw(key, token);
self.db.token_userdeviceid.raw_put(token, key);
self.db.userdeviceid_token.put_raw(key, &token);
self.db.token_userdeviceid.raw_put(&token, key);
if let Some(max_age) = token_max_age {
if let Some(max_age) = max_age {
let expires = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("system time should not be before the epoch")
+1
View File
@@ -14,6 +14,7 @@
utils::{self},
};
use database::Map;
pub use device::DeviceToken;
pub use profile::ProfileFieldChange;
use ruma::{UserId, api::error::ErrorKind, encryption::CrossSigningKey, serde::Raw};
use serde::{Deserialize, Serialize};