matrix-org / matrix-org/matrix-rust-sdk
`OlmMachine` constantly tries to upload keys when restoring session
- Dominant language
- Rust
- Stars
- 2.3k
- Forks
- 500
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 106
Description
### Background
Here is how the code works:
1. The `Client` [continually](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk/src/encryption/mod.rs#L644-L677) asks [`OlmMachine.outgoing_requests()`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk-crypto/src/machine/mod.rs#L509-L545) for requests to send (including `OlmMachine.keys_for_upload(...)`) and translates those `AnyOutgoingRequest` to actual outgoing requests
1. When it gets a response, it should be calling `OlmMachine.mark_request_as_sent(...)` which is happening for [`Client.keys_upload(...)`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk/src/encryption/mod.rs#L526-L551)
1. This trickles down as [`OlmMachine.mark_request_as_sent(...)`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk-crypto/src/machine/mod.rs#L589-L591) -> [`OlmMachine.receive_keys_upload_response(...)`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk-crypto/src/machine/mod.rs#L730-L745) -> [`Account.receive_keys_upload_response(...)`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk-crypto/src/olm/account.rs#L1245-L1262) -> which calls `Account.mark_as_shared()`, `Account.mark_keys_as_published()`, `Account.update_key_counts(...)`
1. Then the next time the `OlmMachine` asks for the `OlmMachine.keys_for_upload(...)` request, it sees that the keys are already shared and returns `None` so we stop uploading more keys.
For new sessions, things work properly and `Client.keys_upload(...)` gets a `200` response and everything gets called as expected.
**Problem:**
But for existing sessions where we `restore_session(...)`, `Client.keys_upload(...)` gets a `400` (keys already exists) error response from the homeserver because it's already uploaded those one-time-keys before (OTK) and [`mark_request_as_sent(...)`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/crates/matrix-sdk/src/encryption/mod.rs#L548) never gets called and the keys are never marked as shared. So then we try the exact same thing on the next cycle of outgoing requests and get the same result every time. To be clear, we try to upload `device_keys`, `one_time_keys`, and `fallback_keys` continuously because nothing is marked as shared.
### Synapse logs:
Shows off the `400` error response from `/_matrix/client/v3/keys/upload`: `One time key signed_curve25519:AAAAAAAAAA0 already exists`
```
2025-06-09 15:12:07,526 - synapse.rest.client.keys - 146 - INFO - POST-14 - asdf /keys/upload: user_id=@test-user-18-device-list-update2:my.synapse.linux.server, device_id=EYDNWNNLLA
2025-06-09 15:12:07,526 - synapse.handlers.e2e_keys - 913 - INFO - POST-14 - Updating device_keys for device 'EYDNWNNLLA' for user @test-user-18-device-list-update2:my.synapse.linux.server at 1749499927526
2025-06-09 15:12:07,531 - synapse.handlers.e2e_keys - 950 - INFO - POST-14- - Adding one_time_keys dict_keys(['signed_curve25519:AAAAAAAAAA0', 'signed_curve25519:AAAAAAAAAA4', 'signed_curve25519:AAAAAAAAAA8', 'signed_curve25519:AAAAAAAAAAA', 'signed_curve25519:AAAAAAAAAAE', 'signed_curve25519:AAAAAAAAAAI', 'signed_curve25519:AAAAAAAAAAM', 'signed_curve25519:AAAAAAAAAAQ', 'signed_curve25519:AAAAAAAAAAU', 'signed_curve25519:AAAAAAAAAAY', 'signed_curve25519:AAAAAAAAAAc', 'signed_curve25519:AAAAAAAAAAg', 'signed_curve25519:AAAAAAAAAAk', 'signed_curve25519:AAAAAAAAAAo', 'signed_curve25519:AAAAAAAAAAs', 'signed_curve25519:AAAAAAAAAAw', 'signed_curve25519:AAAAAAAAAB0', 'signed_curve25519:AAAAAAAAAB4', 'signed_curve25519:AAAAAAAAAB8', 'signed_curve25519:AAAAAAAAABA', 'signed_curve25519:AAAAAAAAABE', 'signed_curve25519:AAAAAAAAABI', 'signed_curve25519:AAAAAAAAABM', 'signed_curve25519:AAAAAAAAABQ', 'signed_curve25519:AAAAAAAAABU', 'signed_curve25519:AAAAAAAAABY', 'signed_curve25519:AAAAAAAAABc', 'signed_curve25519:AAAAAAAAABg', 'signed_curve25519:AAAAAAAAABk', 'signed_curve25519:AAAAAAAAABo', 'signed_curve25519:AAAAAAAAABs', 'signed_curve25519:AAAAAAAAABw', 'signed_curve25519:AAAAAAAAAC0', 'signed_curve25519:AAAAAAAAAC4', 'signed_curve25519:AAAAAAAAAC8', 'signed_curve25519:AAAAAAAAACA', 'signed_curve25519:AAAAAAAAACE', 'signed_curve25519:AAAAAAAAACI', 'signed_curve25519:AAAAAAAAACM', 'signed_curve25519:AAAAAAAAACQ', 'signed_curve25519:AAAAAAAAACU', 'signed_curve25519:AAAAAAAAACY', 'signed_curve25519:AAAAAAAAACc', 'signed_curve25519:AAAAAAAAACg', 'signed_curve25519:AAAAAAAAACk', 'signed_curve25519:AAAAAAAAACo', 'signed_curve25519:AAAAAAAAACs', 'signed_curve25519:AAAAAAAAACw', 'signed_curve25519:AAAAAAAAADA', 'signed_curve25519:AAAAAAAAADE']) for device 'EYDNWNNLLA' for user '@test-user-18-device-list-update2:my.synapse.linux.server' at 1749499927526
2025-06-09 15:12:07,536 - synapse.http.server - 130 - INFO - POST-14 - SynapseError: 400 - One time key signed_curve25519:AAAAAAAAAA0 already exists. Old key: {"key":"EGcRB4WrbDaRXEpHTBLmh0oygnGuWv82dzUL+uFNTTk","signatures":{"@test-user-18-device-list-update2:my.synapse.linux.server":{"ed25519:EYDNWNNLLA":"RxxsfpZpbtMdlC4UUrmTpCSHVvOXCDxd/nnkF1uRG9LAzWDciEv2WR+IGGKpTWwTmdx9LdmVU6JTNmDwYQrPDA"}}}; new key: {'key': '9lY+QpfmqmNv5+8jfb3qbgLyEhyV7SlUtp7VNm0Nc0I', 'signatures': {'@test-user-18-device-list-update2:my.synapse.linux.server': {'ed25519:EYDNWNNLLA': 'rpY9uVdqMN9GUPS50eWDidUq9foCQFvR4eB/8GSEb/qSxVHrKv1JEdsao6M0EArh/xrkx0wUmnDEB2aKgMfrBw'}}}
2025-06-09 15:12:07,537 - synapse.access.http.8008 - 508 - INFO - POST-14 - 127.0.0.1 - 8008 - {@test-user-18-device-list-update2:my.synapse.linux.server} Processed request: 0.011sec/0.000sec (0.001sec, 0.000sec) (0.000sec/0.008sec/4) 610B 400 "POST /_matrix/client/v3/keys/upload HTTP/1.1" "-" [0 dbevts]
```
### Reproduction case
The example just uses the `matrix-sdk` to create a client, `matrix_client.restore_session(...)` if available (which means the first-run will just login/register the user), and setup a `/sync` loop, and keep it running until you press `Enter`.
main.rs
```rust
use std::time::Duration;
use anyhow::Context;
use matrix_sdk::reqwest;
use matrix_sdk::{LoopCtrl, config::SyncSettings, ruma::api::client::uiaa};
use serde::Deserialize;
use tokio_util::sync::CancellationToken;
use url::Url;
mod register;
/// Configuration for a given homeserver
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Homeserver {
/// Homeserver name. Must be unique.
///
/// Users on this homeserver should have MXID's like `@user:server_name`
pub server_name: String,
/// URL to access the client API.
pub client_url: Url,
/// Method to use to register new users
pub user_registration: register::UserRegistration,
/// Password to use for all users.
pub user_password: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let use_existing_sessions = true;
let homeserver = Homeserver {
server_name: "my.synapse.server".to_string(),
client_url: Url::parse("http://localhost:8008/").context("Invalid client URL")?,
user_registration: register::UserRegistration::RegistrationSharedSecret(
"xxx".to_string(),
),
user_password: "secret_password".to_string(),
};
let user_id = format!(
"@test-user-matrix-rust-sdk-key-upload1:{}",
homeserver.server_name
);
let http_client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.context("Failed to build HTTP client")?;
let matrix_client = matrix_sdk::Client::builder()
.http_client(http_client.clone())
.homeserver_url(&homeserver.client_url)
.build()
.await
.context("Couldn't create matrix client")?;
let session_path_string = format!("sessions/{}/{}.json", homeserver.server_name, user_id);
let session_path = std::path::Path::new(&session_path_string);
let (new_user, new_session) =
match (use_existing_sessions, std::fs::read_to_string(session_path)) {
(true, Ok(serialized_session)) => {
// This is added to avoid type inference issues: `match the size for
// values of type `str` cannot be known at compilation time`
let serialized_session: String = serialized_session;
// Try restoring from a previously persisted session first
let session: matrix_sdk::authentication::matrix::MatrixSession =
serde_json::from_str(&serialized_session)?;
matrix_client
.restore_session(session)
.await
.context("Failed restoring session for user")?;
println!("Restored user session");
(false, false)
}
_ => {
println!("Registering user");
// Register a user or login if the user already exists.
let new_user = register::login_or_register_user(
http_client,
&matrix_client,
homeserver.user_registration.clone(),
user_id,
homeserver.user_password.clone(),
)
.await
.context("Failed registering user")?;
(new_user, true)
}
};
// Ensure the user is logged in at this point
let user_id = matrix_client
.user_id()
.context("Client is not logged in")?
.to_owned();
// Persist the session for future runs
if let Some(session) = matrix_client.session() {
match session {
// We should get a `MatrixSession` because we're using the
// "native Matrix authentication API".
matrix_sdk::authentication::AuthSession::Matrix(matrix_session) => {
let serialized_session = serde_json::to_string(&matrix_session)?;
if let Some(session_path_directory) = session_path.parent() {
std::fs::create_dir_all(session_path_directory)?;
}
std::fs::write(session_path, serialized_session)?;
}
matrix_sdk::authentication::AuthSession::OAuth(_oauth_session) => {
return Err(anyhow::anyhow!(
"Unable to persist OAuth session (not supported and unexpected)"
));
}
// This enum is non-exhaustive so we have to handle the `_` case
// but it's not expected.
_ => {
return Err(anyhow::anyhow!(
"Unable to persist unknown session type (not supported): {:?}",
session
));
}
}
} else {
return Err(anyhow::anyhow!(
"Client should be logged in by this point but has no session"
));
}
let cancelation_token = CancellationToken::new();
// We need to bootstrap cross-signing for any new login session because each new
// login creates a new device.
if new_session {
println!("Bootstrapping cross signing");
if let Err(e) = matrix_client
.encryption()
.bootstrap_cross_signing_if_needed(None)
.await
{
if let Some(response) = e.as_uiaa_response() {
let mut password = uiaa::Password::new(
user_id.clone().into(),
homeserver.user_password.clone().to_owned(),
);
password.session.clone_from(&response.session);
matrix_client
.encryption()
.bootstrap_cross_signing_if_needed(Some(uiaa::AuthData::Password(password)))
.await
.context("Couldn't bootstrap cross signing")?;
} else {
return Err(e).context("Couldn't bootstrap cross signing");
}
}
}
// Spawn the sync loop in the background with a cancellation token
//
// We care about syncing to mimic the real-world use case of a client listening
// for new messages.
println!("Starting sync loop");
let sync_handle = tokio::spawn({
let cancelation_token = cancelation_token.child_token();
let client = matrix_client.clone();
async move {
client
.sync_with_callback(SyncSettings::default(), |_| {
if cancelation_token.is_cancelled() {
std::future::ready(LoopCtrl::Break)
} else {
std::future::ready(LoopCtrl::Continue)
}
})
.await?;
anyhow::Ok(())
}
});
println!("Press Enter to continue...");
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.expect("Failed to read line");
// Stop all the agents
cancelation_token.cancel();
// Wait for the sync loop to finish
if let Err(err) = sync_handle.await {
println!("Something went wrong while waiting for the sync loop: {err}");
}
Ok(())
}
```
register.rs
```rust
use anyhow::{Context, ensure};
use hmac::{Hmac, Mac};
use http::{StatusCode, header};
use matrix_sdk::reqwest;
use matrix_sdk::ruma::api::client::{
account::{check_registration_token_validity, register::v3::Request as RegistrationRequest},
uiaa,
};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
/// Method to use to register new users
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum UserRegistration {
/// Synapse registration token
///
/// Created via the Synapse admin API: `POST /_synapse/admin/v1/registration_tokens/new`.
/// See https://github.com/element-hq/synapse/blob/develop/docs/usage/administration/admin_api/registration_tokens.md#create-token
#[serde(rename = "admin_registration_token")]
AdminRegistrationToken(String),
/// Shared-Secret Registration
///
/// As configured in the Synapse homeserver configuration `registration_shared_secret`
#[serde(rename = "registration_shared_secret")]
RegistrationSharedSecret(String),
}
/// `GET /_synapse/admin/v1/register`
#[derive(Deserialize, Debug)]
pub struct SharedSecretRegistrationNonceResponse {
pub nonce: String,
}
#[allow(dead_code)]
pub enum UserType {
/// Normal user
Normal,
/// Special designation in Synapse (see their docs)
Bot,
/// Special designation in Synapse (see their docs)
Support,
}
/// Information necessary for a `POST /_synapse/admin/v1/register` request
pub struct SharedSecretRegistrationRequest {
/// The `registgration_shared_secret` configured in Synapse
pub registration_shared_secret: String,
pub nonce: String,
pub username: String,
pub displayname: String,
pub password: String,
pub admin: bool,
pub user_type: UserType,
}
impl SharedSecretRegistrationRequest {
/// Serialize the request body to a JSON string
fn to_request_body(&self) -> anyhow::Result {
Ok(SharedSecretRegistrationRequestBody {
nonce: self.nonce.clone(),
username: self.username.clone(),
displayname: self.displayname.clone(),
password: self.password.clone(),
admin: self.admin,
mac: self.generate_mac_digest_for_registration()?,
})
}
/// Used for Shared-Secret Registration (`POST /_synapse/admin/v1/register`)
///
/// Args:
fn generate_mac_digest_for_registration(&self) -> anyhow::Result {
let mut mac = Hmac::::new_from_slice(self.registration_shared_secret.as_bytes())?;
mac.update(self.nonce.as_bytes());
mac.update(b"\0");
mac.update(self.username.as_bytes());
mac.update(b"\0");
mac.update(self.password.as_bytes());
mac.update(b"\0");
mac.update(if self.admin { b"admin" } else { b"notadmin" });
match self.user_type {
UserType::Normal => (),
UserType::Bot => {
mac.update(b"\0");
mac.update("bot".as_bytes());
}
UserType::Support => {
mac.update(b"\0");
mac.update("support".as_bytes());
}
};
let result = mac.finalize();
let code_bytes = result.into_bytes();
Ok(hex::encode(code_bytes))
}
}
/// Serialized request body for a `POST /_synapse/admin/v1/register` request
#[derive(Serialize, Deserialize, Debug)]
pub struct SharedSecretRegistrationRequestBody {
pub nonce: String,
pub username: String,
pub displayname: String,
pub password: String,
pub admin: bool,
pub mac: String,
}
/// `POST /_synapse/admin/v1/register`
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct SharedSecretRegistrationResponse {
pub access_token: String,
pub user_id: String,
pub home_server: String,
pub device_id: String,
}
/// Register a user or login if the user already exists.
///
/// Returns whether the user was newly registered or not.
pub async fn login_or_register_user(
http_client: reqwest::Client,
matrix_client: &matrix_sdk::Client,
user_registration_method: UserRegistration,
user_id: String,
password: String,
) -> anyhow::Result {
let user_id = matrix_sdk::ruma::UserId::parse(user_id)?;
let username = user_id.localpart().to_owned();
let server_name = user_id.server_name().to_owned();
// Sanity check that the matrix_client is for the correct server when possible.
if let Some(matrix_client_server_name) = matrix_client.server() {
ensure!(
server_name == matrix_client_server_name.to_string(),
"Server name in user_id ({}) doesn't match `matrix_client` server name ({})",
user_id,
server_name,
);
}
let new_user = match user_registration_method {
UserRegistration::AdminRegistrationToken(admin_registration_token) => {
let request = check_registration_token_validity::v1::Request::new(
admin_registration_token.clone(),
);
let is_registration_token_valid = matrix_client.send(request).await?;
println!(
"Registration token valid? {}",
is_registration_token_valid.valid,
);
if !is_registration_token_valid.valid {
return Err(anyhow::anyhow!(
"`registration_token` for homeserver ({:?}) is invalid",
server_name
));
}
let mut request: RegistrationRequest = RegistrationRequest::new();
request.username = Some(username.clone());
request.password = Some(password.to_owned());
request.auth = Some(uiaa::AuthData::Password(uiaa::Password::new(
uiaa::UserIdentifier::UserIdOrLocalpart(username.clone()),
password.clone(),
)));
//matrix_client.send(Requ)
match matrix_client.matrix_auth().register(request).await {
Ok(_response) => true,
Err(err) => {
if let Some(uiaa_info) = err.as_uiaa_response() {
let session = uiaa_info.session.clone();
println!("Failed registering with session {:?}: {:?}", session, err,);
let mut request = RegistrationRequest::new();
request.username = Some(username.clone());
request.password = Some(password.clone().to_owned());
let mut reg_token =
uiaa::RegistrationToken::new(admin_registration_token.clone());
reg_token.session = session.clone();
request.auth = Some(uiaa::AuthData::RegistrationToken(reg_token));
match matrix_client
.matrix_auth()
.register(request)
.await
.context("Failed to register user")
{
Ok(_response) => true,
Err(_err) => {
let mut request = RegistrationRequest::new();
request.username = Some(username.clone());
request.password = Some(password.clone().to_owned());
let mut password_register = uiaa::Dummy::new();
password_register.session = session;
request.auth = Some(uiaa::AuthData::Dummy(password_register));
match matrix_client
.matrix_auth()
.register(request)
.await
.context("Failed to register user")
{
Ok(_response) => true,
Err(_err) => {
println!(
"Failed registering user, attempting to login as existing user.",
);
matrix_client
.matrix_auth()
.login_username(username, &password.clone())
.await
.context("Failed logging in user")?;
false
}
}
}
}
} else {
println!(
"Failed getting session for user registration, attempting to login as existing user.",
);
matrix_client
.matrix_auth()
.login_username(username, &password.clone())
.await
.context("Failed logging in user")?;
false
}
}
}
}
UserRegistration::RegistrationSharedSecret(registration_shared_secret) => {
let admin_register_url = matrix_client
.homeserver()
.join("/_synapse/admin/v1/register")?;
let nonce_response = http_client
.get(admin_register_url.clone())
.send()
.await?
.error_for_status()?
.json::()
.await?;
let serialized_registration_request_body = serde_json::to_string(
&SharedSecretRegistrationRequest {
registration_shared_secret,
nonce: nonce_response.nonce,
username: username.clone(),
displayname: username.clone(),
password: password.to_owned(),
admin: false,
user_type: UserType::Normal,
}
.to_request_body()?,
)?;
let registration_response = http_client
.post(admin_register_url.clone())
.header(header::CONTENT_TYPE, "application/json")
.body(serialized_registration_request_body.clone())
.send()
.await?;
let status = registration_response.status();
match status {
StatusCode::OK => {
let registration_response: SharedSecretRegistrationResponse =
registration_response.json().await?;
// Sanity check
ensure!(
registration_response.home_server == server_name,
"`home_server` ({}) in response doesn't match our expected server_name ({})",
registration_response.home_server,
server_name,
);
ensure!(
registration_response.user_id == user_id,
"`user_id` in response ({}) doesn't match expected user ID ({})",
registration_response.user_id,
user_id,
);
// Login the user for the `matrix_client`
matrix_client
.matrix_auth()
.login_username(username, &password.clone())
.await
.context("Failed logging in user")?;
// New user
true
}
StatusCode::BAD_REQUEST => {
let error_response: matrix_sdk::ruma::api::client::error::StandardErrorBody =
registration_response.json().await?;
match error_response.kind {
matrix_sdk::ruma::api::client::error::ErrorKind::UserInUse => {
// Login the user for the `matrix_client`
println!("User already exists, logging in.");
matrix_client
.matrix_auth()
.login_username(username, &password.clone())
.await
.context("Failed logging in user")?;
// Not a new user
false
}
_ => {
return Err(anyhow::anyhow!(
"Expected registration (POST {} with {}) to respond successfully (or `{}`) but encountered {} -> {:?}",
admin_register_url,
// Note: This does log senstive information but it is useful
// for debugging. We're assuming test users aren't important
// and/or in a test environment.
serialized_registration_request_body,
matrix_sdk::ruma::api::client::error::ErrorCode::UserInUse,
StatusCode::BAD_REQUEST,
error_response,
));
}
}
}
status_code => {
return Err(anyhow::anyhow!(
"Expected registration (POST {} with {}) to respond successfully (or `{}`) but encountered {} -> {}",
admin_register_url,
// Note: This does log senstive information but it is useful
// for debugging. We're assuming test users aren't important
// and/or in a test environment.
serialized_registration_request_body,
matrix_sdk::ruma::api::client::error::ErrorCode::UserInUse,
status_code,
registration_response
.text()
.await
.unwrap_or("".to_string()),
));
}
}
}
};
Ok(new_user)
}
```
Cargo.toml
```toml
[package]
name = "matrix-sdk-keys-upload"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.86"
hex = "0.4.3"
hmac = "0.12.1"
http = "1.3.1"
matrix-sdk = { version = "0.11.0", features = ["anyhow"] }
# matrix-sdk = { path = "/home/eric/Documents/github/element/matrix-rust-sdk/crates/matrix-sdk/", features = [
# "anyhow",
# ] }
reqwest = { version = "0.12.15", default-features = false, features = ["json"] }
serde = { version = "1.0.202", features = ["derive", "rc"] }
serde_json = "1.0.140"
sha1 = "0.10.6"
tokio = { version = "1.37.0", features = ["full"] }
tokio-util = "0.7.11"
url = "2.5.4"
```
### Workaround
Currently, it seems like the `matrix-sdk` assumes you're defining a client `store_config`. Things appear to work once you start using one but the Rust SDK should gracefully handle key conflicts without this.
```rust
let matrix_client = matrix_sdk::Client::builder()
.http_client(http_client.clone())
.homeserver_url(&homeserver.client_url)
// Works when a client store is defined (`indexeddb_store()` and `store_config()` also available)
.sqlite_store(client_session_store_path, None)
.build()
.await
.context("Couldn't create matrix client")?;
```
See [`examples/persist_session/src/main.rs`](https://github.com/matrix-org/matrix-rust-sdk/blob/216e878231eb230255cb4796d45e5bc0faa0d388/examples/persist_session/src/main.rs#L83-L105) as a full example.
### Definition of done
Tasks that have spawned from the discussion below:
- [ ] Update doc comments for `Client::restore_session()` to point out the need to also setup persistent storage on the client
- [ ] Update *Restoring a client* section in the `encryption.md` docs on to more explicitly point out the need to setup persistent storage on the client
- [ ] Detect when the persistent storage isn't configured (i.e. no keys in the crypto storage) and someone uses `Client::restore_session()`; make sure error is thrown when the `e2e-encryption` feature is enabled.
- [ ] (needs buy-in): Detect key conflict on `/_matrix/client/v3/keys/upload` scenarios and back-off on trying to upload keys (this is a last resort as usually we should be able to detect a mismatch between the device ID from user session/`CryptoStore` and throw an error then instead)
- [ ] (needs buy-in): Better API (still being discussed)
Contributor guide
Research direction
Trace the restore_session and key-upload flow through crates/matrix-sdk/src/encryption/mod.rs, crates/matrix-sdk-crypto/src/machine/mod.rs, and crates/matrix-sdk-crypto/src/olm/account.rs. Start with the provided main.rs reproduction and verify that an already-restored session no longer repeatedly submits device, one-time, and fallback keys after the homeserver returns a 400 response.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100