ExWapp.Signal.SessionStore (ExWapp v0.1.2)

Copy Markdown View Source

Manages Signal sessions for multiple recipients.

Sessions are stored per (user, device) pair and persisted to the main store.

Uses ExWapp.Signal.Adapter to switch between Signal implementations.

Summary

Functions

Attaches to an existing ETS-backed session store table.

Copies a device session and its pending pre-key state to a PN/LID alias.

Removes the pending outbound pre-key wrapper for one device session.

Removes a session for a recipient JID.

Deletes cached sessions for the target JID and any PN<->LID aliases derived from the current mapping store. Matching is done on the stripped base JID so all device variants are cleared together.

Gets the pending outbound pre-key wrapper for a device session.

Gets a session for a recipient JID.

Checks if we have a session for a recipient.

Lists all JIDs with active sessions.

Creates a new empty session store.

Creates a lightweight ETS-backed session store reference.

Stores the pending outbound pre-key wrapper for one device session.

Stores a session for a recipient JID.

Replaces the backend contents with the provided snapshot and returns the store.

Returns the last-touch timestamp for a stored session, in wall-clock milliseconds.

Returns a plain snapshot map for persistence/debugging.

Returns true when none of the candidate JIDs has a recent session touch timestamp.

Returns true when the given value is a valid session-store backend.

Updates a session after encryption/decryption.

Types

pending_prekey()

@type pending_prekey() :: %{
  :base_key => binary(),
  :signed_prekey_id => non_neg_integer(),
  optional(:prekey_id) => non_neg_integer() | nil
}

t()

@type t() ::
  %{
    sessions: %{required(String.t()) => map()},
    pending_prekeys: %{required(String.t()) => map()},
    session_touched_at_ms: %{required(String.t()) => integer()}
  }
  | ExWapp.Signal.SessionStore.Ref.t()

Functions

attach(table)

@spec attach(term()) :: t()

Attaches to an existing ETS-backed session store table.

copy_session(store, source_jid, alias_jid)

@spec copy_session(t(), String.t(), String.t()) :: t()

Copies a device session and its pending pre-key state to a PN/LID alias.

decrypt_message(store, jid, ciphertext, is_prekey_message, local_identity, local_prekeys)

@spec decrypt_message(t(), String.t(), binary(), boolean(), map(), map()) ::
  {:ok, binary(), t()} | {:error, term()}

Decrypts a message from a sender.

delete_pending_prekey(store, jid)

@spec delete_pending_prekey(t(), String.t()) :: t()

Removes the pending outbound pre-key wrapper for one device session.

delete_session(store, jid)

@spec delete_session(t(), String.t()) :: t()

Removes a session for a recipient JID.

delete_sessions_for_user(store, jid, lid_mapping \\ %{})

@spec delete_sessions_for_user(t(), String.t(), map()) :: t()

Deletes cached sessions for the target JID and any PN<->LID aliases derived from the current mapping store. Matching is done on the stripped base JID so all device variants are cleared together.

encrypt_message(store, jid, plaintext, local_identity, local_registration_id, remote_bundle)

@spec encrypt_message(t(), String.t(), binary(), map(), non_neg_integer(), map()) ::
  {:ok, binary(), :prekey | :whisper, t()} | {:error, term()}

Encrypts a message to a recipient.

Creates a session if needed using the provided prekey bundle. Returns either a PreKeySignalMessage (new session) or regular SignalMessage.

evict_one_way_sessions(store, min_age_ms, now_ms \\ System.system_time(:millisecond))

@spec evict_one_way_sessions(t(), non_neg_integer(), integer()) :: {t(), [String.t()]}

Deletes sessions this device has only ever sent on.

A session with traffic in one direction and nothing coming back is the shape a desync leaves behind: the peer dropped its half and re-initiated, this device never managed to adopt the new one, and every send since has been encrypted against a ratchet the peer abandoned. The server answers those with <ack error="463"> forever, because nothing in the send path ever concludes that the session itself is the problem.

It is also the shape of a perfectly healthy session to someone who has not replied yet, so age is what tells them apart — hence min_age_ms, and hence calling this at connect rather than mid-conversation. An unstamped session is treated as old: timestamps are written on every store, so their absence means the session outlived the process that made it.

Returns the store and the JIDs that were dropped. The next send to those peers fetches a fresh bundle and negotiates from scratch.

get_or_create_session(store, jid, local_identity, remote_bundle, local_registration_id)

@spec get_or_create_session(t(), String.t(), map(), map(), non_neg_integer()) ::
  {:ok, map(), binary() | nil, t()} | {:error, term()}

Creates or gets a session for a recipient.

If no session exists, uses the prekey bundle to create one.

get_or_create_session(store, jid, local_identity, remote_bundle, local_registration_id, opts)

@spec get_or_create_session(
  t(),
  String.t(),
  map(),
  map(),
  non_neg_integer(),
  keyword()
) ::
  {:ok, map(), binary() | nil, t()} | {:error, term()}

Creates or gets a session for a recipient with options.

Options

  • :force - force creation from the supplied bundle even if a session exists.

get_pending_prekey(store, jid)

@spec get_pending_prekey(t(), String.t()) :: {:ok, pending_prekey()} | :not_found

Gets the pending outbound pre-key wrapper for a device session.

A pending wrapper remains active until that same device proves the Signal session by sending a message that decrypts successfully.

get_session(store, jid)

@spec get_session(t(), String.t()) :: {:ok, map()} | :not_found

Gets a session for a recipient JID.

has_session?(store, jid)

@spec has_session?(t(), String.t()) :: boolean()

Checks if we have a session for a recipient.

list_sessions(store)

@spec list_sessions(t()) :: [String.t()]

Lists all JIDs with active sessions.

new()

@spec new() :: t()

Creates a new empty session store.

new_ets(initial \\ nil)

@spec new_ets(t() | nil) :: t()

Creates a lightweight ETS-backed session store reference.

put_pending_prekey(store, jid, pending_prekey)

@spec put_pending_prekey(t(), String.t(), pending_prekey()) :: t()

Stores the pending outbound pre-key wrapper for one device session.

put_session(store, jid, session)

@spec put_session(t(), String.t(), map()) :: t()

Stores a session for a recipient JID.

replace(store, snapshot)

@spec replace(t(), t() | nil) :: t()

Replaces the backend contents with the provided snapshot and returns the store.

session_touched_at(store, jid)

@spec session_touched_at(t(), String.t()) :: integer() | nil

Returns the last-touch timestamp for a stored session, in wall-clock milliseconds.

snapshot(store)

@spec snapshot(t() | nil) :: map() | nil

Returns a plain snapshot map for persistence/debugging.

stale_or_missing?(store, candidate_jids, max_age_ms, now_ms \\ System.system_time(:millisecond))

@spec stale_or_missing?(t(), [String.t()], pos_integer(), integer()) :: boolean()

Returns true when none of the candidate JIDs has a recent session touch timestamp.

store?(arg1)

@spec store?(term()) :: boolean()

Returns true when the given value is a valid session-store backend.

update_session(store, jid, session)

@spec update_session(t(), String.t(), map()) :: t()

Updates a session after encryption/decryption.