Merge branch 'hotfix' into dev

This commit is contained in:
otsmr 2026-09-25 12:11:03 +02:00
commit 856a67edaa
6 changed files with 115 additions and 6 deletions

View file

@ -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

View file

@ -1 +0,0 @@
ce49e7d90cd902197f9a9cbc84219d23

View file

@ -136,8 +136,10 @@ class MainCameraController {
try {
await cameraControllerTemp?.stopImageStream();
// ignore: empty_catches
} catch (e) {}
} catch (e) {
} finally {
await cameraControllerTemp?.dispose();
}
},
);
initCameraStarted = false;

View file

@ -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

View file

@ -420,9 +420,26 @@ impl ApiClient {
break;
}
// Tungstenite message
if let stream_tungstenite::tokio_tungstenite::tungstenite::Message::Binary(bytes) = &*msg {
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,
}

View file

@ -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<dyn std::error::Error>> {
use crate::database::signal::Database;