diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f375c1c..075cee1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 0.5.4 + +- Fix: Recover broken sessions more reliably + +## 0.5.3 + +- Hotfix: Fixed stuck Signal sessions +- Improve: Gracefully upgrade all sessions to PQXDH + ## 0.5.2 - Hotfix: Fixed an infinite loop involving delivery receipts diff --git a/ios/build/.last_build_id b/ios/build/.last_build_id deleted file mode 100644 index 7769d20e..00000000 --- a/ios/build/.last_build_id +++ /dev/null @@ -1 +0,0 @@ -ce49e7d90cd902197f9a9cbc84219d23 \ No newline at end of file diff --git a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart index d4df77c8..c702ab33 100644 --- a/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart +++ b/lib/src/visual/views/camera/camera_preview_components/main_camera_controller.dart @@ -136,8 +136,10 @@ class MainCameraController { try { await cameraControllerTemp?.stopImageStream(); // ignore: empty_catches - } catch (e) {} - await cameraControllerTemp?.dispose(); + } catch (e) { + } finally { + await cameraControllerTemp?.dispose(); + } }, ); initCameraStarted = false; diff --git a/pubspec.yaml b/pubspec.yaml index 6d19d0d8..f30bef85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.6.0+173 +version: 0.6.0+175 environment: sdk: ^3.11.0 diff --git a/rust/src/api/runtime/client.rs b/rust/src/api/runtime/client.rs index 6a939cf8..331b8092 100644 --- a/rust/src/api/runtime/client.rs +++ b/rust/src/api/runtime/client.rs @@ -420,8 +420,25 @@ impl ApiClient { break; } // Tungstenite message - if let stream_tungstenite::tokio_tungstenite::tungstenite::Message::Binary(bytes) = &*msg { - self_clone.handle_incoming(&connection, bytes).await; + use stream_tungstenite::tokio_tungstenite::tungstenite::Message; + match &*msg { + Message::Binary(bytes) => { + self_clone.handle_incoming(&connection, bytes).await; + } + // The server's close code tells why it dropped the + // connection, e.g. 1013 (outbound queue full) or 4003 + // (ack timeouts). The disconnect event does not carry it. + Message::Close(frame) => match frame { + Some(frame) => tracing::info!( + "websocket closed by the server with code {}: {}", + u16::from(frame.code), + frame.reason + ), + None => tracing::info!( + "websocket closed by the server without a code" + ), + }, + _ => {} } } Err(_) => break, diff --git a/rust/src/signal/engine.rs b/rust/src/signal/engine.rs index 46e5739d..12bd57bf 100644 --- a/rust/src/signal/engine.rs +++ b/rust/src/signal/engine.rs @@ -972,6 +972,88 @@ mod tests { assert!(!bob.reset_session("carol", 1, false).await.unwrap()); } + /// The user-initiated reset: the record is gone, nothing encrypts for the + /// peer until a fresh bundle builds a new session, and that one works. + #[tokio::test] + async fn hard_reset_deletes_and_allows_rebuilding_a_contact_session() { + let (alice, _alice_dir) = create_test_engine("alice").await; + let (bob, _bob_dir) = create_test_engine("bob").await; + + alice + .process_prekey_bundle("bob".to_string(), 1, bob.generate_bundle().await.unwrap()) + .await + .unwrap(); + + assert!(alice.reset_session("bob", 1, true).await.unwrap()); + assert!(!alice.reset_session("bob", 1, true).await.unwrap()); + assert!(alice + .encrypt_message("bob".to_string(), 1, b"missing".to_vec()) + .await + .is_err()); + + alice + .process_prekey_bundle("bob".to_string(), 1, bob.generate_bundle().await.unwrap()) + .await + .unwrap(); + + assert_eq!(ping(&alice, &bob, "rebuilt").await.unwrap(), b"rebuilt"); + } + + /// A message decrypted twice is a duplicate, for prekey and whisper + /// messages alike, and never reported as an unusable session. + #[tokio::test] + async fn decrypting_a_message_twice_reports_a_duplicate() { + let (alice, _alice_dir) = create_test_engine("alice").await; + let (bob, _bob_dir) = create_test_engine("bob").await; + + alice + .process_prekey_bundle("bob".to_string(), 1, bob.generate_bundle().await.unwrap()) + .await + .unwrap(); + + let prekey_message = alice + .encrypt_message("bob".to_string(), 1, b"first".to_vec()) + .await + .unwrap(); + assert_eq!( + prekey_message.last(), + Some(&(CiphertextMessageType::PreKey as u8)) + ); + bob.decrypt_message("alice".to_string(), 1, prekey_message.clone()) + .await + .unwrap(); + let error = bob + .decrypt_message("alice".to_string(), 1, prekey_message) + .await + .unwrap_err(); + assert!( + matches!(error, TwonlyError::SignalDuplicateMessage(_)), + "{error}" + ); + + // After Bob's reply Alice no longer needs a PreKey message. + ping(&bob, &alice, "reply").await.unwrap(); + let whisper_message = alice + .encrypt_message("bob".to_string(), 1, b"second".to_vec()) + .await + .unwrap(); + assert_eq!( + whisper_message.last(), + Some(&(CiphertextMessageType::Whisper as u8)) + ); + bob.decrypt_message("alice".to_string(), 1, whisper_message.clone()) + .await + .unwrap(); + let error = bob + .decrypt_message("alice".to_string(), 1, whisper_message) + .await + .unwrap_err(); + assert!( + matches!(error, TwonlyError::SignalDuplicateMessage(_)), + "{error}" + ); + } + #[tokio::test] async fn test_twonly_api_100_messages() -> std::result::Result<(), Box> { use crate::database::signal::Database;