Skip to content
Open
446 changes: 328 additions & 118 deletions Cargo.lock

Large diffs are not rendered by default.

408 changes: 408 additions & 0 deletions src/legacy_session.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod group_types;
#[cfg(feature = "image")]
pub mod image_utils;
pub mod key_helper;
pub mod legacy_session;
pub mod logger;
pub mod noise_session;
pub mod protocol_address;
Expand Down
47 changes: 42 additions & 5 deletions src/session_cipher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
protocol_address::ProtocolAddress,
storage_adapter::{JsStorageAdapter, SignalStorage},
};
use wacore_libsignal::protocol::{self as libsignal, SessionStore, UsePQRatchet};
use wacore_libsignal::protocol::{self as libsignal, PreKeyStore, SessionStore, UsePQRatchet};

#[inline]
fn bytes_to_uint8array(bytes: &[u8]) -> Uint8Array {
Expand Down Expand Up @@ -78,12 +78,23 @@ impl SessionCipher {
JsValue::from_str(&msg)
})?;

// Snapshot before decrypt advances the chain (no-op for a fresh session).
let inner = prekey_message.message();
let skip_snapshot = self
.storage_adapter
.snapshot_skip(
&self.remote_address.0,
inner.sender_ratchet_key(),
inner.counter(),
)
.await;

let mut session_store = self.storage_adapter.clone();
let mut identity_store = session_store.clone();
let mut prekey_store = session_store.clone();
let signed_prekey_store = session_store.clone();

let plaintext = libsignal::message_decrypt_prekey(
let result = libsignal::message_decrypt_prekey(
&prekey_message,
&self.remote_address.0,
&mut session_store,
Expand All @@ -99,7 +110,18 @@ impl SessionCipher {
JsValue::from_str(&msg)
})?;

Ok(bytes_to_uint8array(&plaintext))
// v0.6 reports the consumed prekey instead of deleting it. Best-effort:
// the message is already decrypted, so a removal failure must not drop the
// delivered plaintext (a redelivered pkmsg reuses the promoted session).
if let Some(prekey_id) = result.consumed_prekey_id {
let _ = prekey_store.remove_pre_key(prekey_id).await;
}

self.storage_adapter
.commit_skipped(&self.remote_address.0, skip_snapshot)
.await;

Ok(bytes_to_uint8array(&result.plaintext))
}

#[wasm_bindgen(js_name = decryptWhisperMessage)]
Expand All @@ -115,10 +137,20 @@ impl SessionCipher {
JsValue::from_str(&msg)
})?;

// Snapshot before decrypt advances the chain (seeds committed post-auth).
let skip_snapshot = self
.storage_adapter
.snapshot_skip(
&self.remote_address.0,
signal_message.sender_ratchet_key(),
signal_message.counter(),
)
.await;

let mut session_store = self.storage_adapter.clone();
let mut identity_store = session_store.clone();

let plaintext = libsignal::message_decrypt_signal(
let result = libsignal::message_decrypt_signal(
&signal_message,
&self.remote_address.0,
&mut session_store,
Expand All @@ -131,7 +163,12 @@ impl SessionCipher {
JsValue::from_str(&msg)
})?;

Ok(bytes_to_uint8array(&plaintext))
// MAC checked out → commit the skipped seeds (see commit_skipped).
self.storage_adapter
.commit_skipped(&self.remote_address.0, skip_snapshot)
.await;

Ok(bytes_to_uint8array(&result.plaintext))
}

#[wasm_bindgen(js_name = hasOpenSession)]
Expand Down
40 changes: 39 additions & 1 deletion src/session_record.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
use js_sys::{Array, Reflect, Uint8Array};
use js_sys::{Array, Object, Reflect, Uint8Array};
use wacore_libsignal::protocol::SessionRecord as CoreSessionRecord;
use wasm_bindgen::prelude::*;

const INVALID_INPUT_ERROR: &str = "SessionRecord.deserialize: Invalid input type. Expected Uint8Array, Array, or Buffer-like object.";
const SESSIONS_KEY: &str = "_sessions";
const DATA_KEY: &str = "data";

#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Object, typescript_type = "{ baseKey: Uint8Array; registrationId: number }")]
pub type SessionInfo;
}

#[wasm_bindgen(js_name = SessionRecord)]
pub struct SessionRecord {
pub(crate) serialized_data: Vec<u8>,
Expand Down Expand Up @@ -57,6 +63,38 @@ impl SessionRecord {
.map(|record| record.session_state().is_some())
.unwrap_or(false)
}

// The X3DH base key indexes the session and is shared by both peers, so it's a
// stable per-session id; remote registration id identifies the peer device.
// libsignal-node exposed both via getOpenSession().indexInfo.baseKey/registrationId,
// which Baileys' retry protections read. `undefined` when there's no open session.
#[wasm_bindgen(js_name = sessionInfo)]
pub fn session_info(&self) -> Result<Option<SessionInfo>, JsValue> {
let record = CoreSessionRecord::deserialize(&self.serialized_data)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
if record.session_state().is_none() {
return Ok(None);
}
let base_key = record
.alice_base_key()
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let registration_id = record
.remote_registration_id()
.map_err(|e| JsValue::from_str(&e.to_string()))?;

let obj = Object::new();
Reflect::set(
&obj,
&JsValue::from_str("baseKey"),
&Uint8Array::from(base_key),
)?;
Reflect::set(
&obj,
&JsValue::from_str("registrationId"),
&JsValue::from(registration_id),
)?;
Ok(Some(obj.unchecked_into()))
}
}

fn create_empty_session_record() -> Result<SessionRecord, JsValue> {
Expand Down
Loading