Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/fix-backup-import-partial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix restoring from key backup failing entirely when a single backed-up key could not be read; the rest of the keys are now restored.
5 changes: 5 additions & 0 deletions .changeset/fix-push-anr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix "Sable isn't responding" appearing repeatedly on Android when notifications arrived while the app was in the background.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,12 @@ windows = { version = "0.62", features = [
tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] }

[target.'cfg(any(windows, target_os = "linux"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "d06bc72dae599174ffa6d57c72c3a0d652346507" }
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "628f99df7a7634eed423388392ad995f13e2d08c" }

# default-features = false drops notify-rust so macOS uses the native
# UNUserNotificationCenter backend (needs a signed .app to deliver).
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "d06bc72dae599174ffa6d57c72c3a0d652346507", default-features = false }
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "628f99df7a7634eed423388392ad995f13e2d08c", default-features = false }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = { version = "2", optional = true }
Expand All @@ -138,7 +138,7 @@ libloading = "0.9"
zbus = "5"

[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "d06bc72dae599174ffa6d57c72c3a0d652346507", features = [
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "628f99df7a7634eed423388392ad995f13e2d08c", features = [
"push-notifications",
] }
tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" }
Expand Down
101 changes: 90 additions & 11 deletions src-tauri/src/matrix_crypto/backup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ struct BackedUpSession {
key: Value,
}

fn exportable_keys(sessions: Vec<BackedUpSession>) -> (Vec<ExportedRoomKey>, usize) {
let mut exported = Vec::with_capacity(sessions.len());
let mut skipped = 0usize;

for session in sessions {
let Ok(room) = RoomId::parse(&session.room_id) else {
skipped += 1;
continue;
};
let Ok(key) = serde_json::from_value::<BackedUpRoomKey>(session.key) else {
skipped += 1;
continue;
};
exported.push(ExportedRoomKey::from_backed_up_room_key(
room,
session.session_id,
key,
));
}

(exported, skipped)
}

fn import_result(result: RoomKeyImportResult) -> Value {
json!({
"importedCount": result.imported_count,
Expand Down Expand Up @@ -152,17 +175,9 @@ async fn handle(machine: &OlmMachine, method: &str, args: &Value) -> Result<Opti
serde_json::from_str(&str_arg(args, method, "keys")?)
.map_err(|e| format!("importBackedUpRoomKeys: bad keys: {e}"))?;

let mut exported = Vec::with_capacity(sessions.len());
for session in sessions {
let room = RoomId::parse(&session.room_id)
.map_err(|e| format!("importBackedUpRoomKeys: bad room id: {e}"))?;
let key: BackedUpRoomKey = serde_json::from_value(session.key)
.map_err(|e| format!("importBackedUpRoomKeys: bad key for room {room}: {e}"))?;
exported.push(ExportedRoomKey::from_backed_up_room_key(
room,
session.session_id,
key,
));
let (exported, skipped) = exportable_keys(sessions);
if skipped > 0 {
log::warn!("importBackedUpRoomKeys: skipped {skipped} unreadable backed-up keys");
}

let result = machine
Expand Down Expand Up @@ -254,4 +269,68 @@ mod tests {
assert_eq!(parsed[0].room_id, "!room:example.org");
assert_eq!(parsed[0].session_id, "session-1");
}

const VALID_CURVE_KEY: &str = "KyHFkVuB9MFbEkiCw+idNHKbiM8r3cWpNNPdyHkFeHY";

fn valid_session_key() -> String {
use matrix_sdk_crypto::vodozemac::{base64_encode, Ed25519SecretKey};

let signing_key = Ed25519SecretKey::new().public_key();
let mut bytes = Vec::with_capacity(165);
bytes.push(1u8);
bytes.extend_from_slice(&0u32.to_be_bytes());
bytes.extend_from_slice(&[0u8; 128]);
bytes.extend_from_slice(signing_key.as_bytes());
base64_encode(bytes)
}

fn session(room_id: &str, sender_key: &str, session_key: &str) -> BackedUpSession {
serde_json::from_value(json!({
"room_id": room_id,
"session_id": "session-1",
"algorithm": "m.megolm.v1.aes-sha2",
"sender_key": sender_key,
"session_key": session_key,
"sender_claimed_keys": {},
"forwarding_curve25519_key_chain": [],
}))
.unwrap()
}

/// MSC3700 deprecated `sender_key`, so backups legitimately carry blank ones.
/// These used to abort the whole import with an error, costing the user every
/// other key in the batch; they must be counted and stepped over instead.
#[test]
fn a_readable_session_survives_a_bad_neighbour() {
let key = valid_session_key();
let sessions = vec![
session("!room:example.org", "", &key),
session("!room:example.org", VALID_CURVE_KEY, &key),
];

let (exported, skipped) = exportable_keys(sessions);

assert_eq!(skipped, 1);
assert_eq!(exported.len(), 1);
}

#[test]
fn a_malformed_room_id_is_skipped_too() {
let key = valid_session_key();
let (exported, skipped) =
exportable_keys(vec![session("not-a-room-id", VALID_CURVE_KEY, &key)]);

assert!(exported.is_empty());
assert_eq!(skipped, 1);
}

#[test]
fn a_wholly_readable_batch_skips_nothing() {
let key = valid_session_key();
let (exported, skipped) =
exportable_keys(vec![session("!room:example.org", VALID_CURVE_KEY, &key)]);

assert_eq!(exported.len(), 1);
assert_eq!(skipped, 0);
}
}
Loading