Skip to main content

Client

Struct Client 

Source
pub struct Client<P: CryptoProvider = DefaultProvider> { /* private fields */ }
Expand description

A connected client: an identity, a directory connection, an authenticated relay connection, and the peers it has open sessions with.

Generic over the crypto provider, defaulting to open-tacenta’s provider so that every existing caller keeps working by writing Client and nothing else.

The default is the point, and so is the parameter. Before this the client held libsignal’s concrete Party, so a feature flag swapped the whole binary and the two providers could not both be live. Decision 0056 steps 4 and 5 assume otherwise: flipping the default for new sessions while existing ones continue, and draining them, both need two providers running at once. That was not possible and nothing said so.

Implementations§

Source§

impl<P: CryptoProvider> Client<P>

Source

pub async fn connect(config: &Config) -> Result<Self>

Generate a fresh identity, publish it to the directory, and authenticate to the relay — leaving a client ready to send and receive. This is first-run enrolment: the identity is new every call, so re-running it for an address that is already bound to a different key is refused by trust-on-first-use. To keep an identity across restarts, save export_identity and reconnect with connect_with_identity.

Source

pub async fn connect_tls( config: &Config, server_name: &str, tls: &ClientTls, ) -> Result<Self>

Like connect, but over TLS to a server presenting server_name (e.g. the hosted tacenta.com). tls decides which certificate to trust — ClientTls::web_pki for a public CA.

Source

pub async fn connect_with_identity( config: &Config, identity: &[u8], ) -> Result<Self>

Reconnect under a previously saved identity (from export_identity): the client presents the same identity key, so the directory refreshes the existing binding rather than rejecting a new key for the address, and peers see no safety-number change. The session and prekey store starts empty — prekeys are re-published and peer sessions re-establish on next use.

Source

pub async fn connect_with_state(config: &Config, state: &[u8]) -> Result<Self>

Reconnect under a full saved state (from export_state): the same identity and the live ratchet sessions, so open conversations resume mid-ratchet — a message a peer sent while this device was down still decrypts. The stronger sibling of connect_with_identity, which keeps only the identity and re-establishes sessions from scratch.

Source

pub async fn connect_with_state_tls( config: &Config, server_name: &str, tls: &ClientTls, state: &[u8], ) -> Result<Self>

Like connect_with_state, but over TLS to a server presenting server_name.

Source

pub async fn connect_with_state_sealed( config: &Config, state: &[u8], store: Arc<dyn SecureStore + Send + Sync>, ) -> Result<Self>

Reconnect under a sealed full state (from export_state_sealed) — decision 0078’s anchor B, the rollback-resistant restore path. Attaches store (per-send commits resume), refuses a forged state, and catches a state older than the latest send via the counter — closing EX-03 against a file-rewriter given a rollback-resistant store.

Two things happen that the unsealed connect_with_state does not do. First, the state’s authenticator is verified under a key from store; a state a file-rewriter forged (old sessions, high generation) has no matching authenticator and is refused here rather than resumed, which is the AR-1 gap closed. Second, because the generation that survives that check is authenticated, presenting it to the directory (checkpoint) is a real freshness control: a genuine older sealed state — a legitimate backup restore, or an attacker replaying an untouched old file — authenticates but is caught as a rollback, and its sessions are discarded (decision 3).

A directory that is unreachable does not fail the connect: the client resumes optimistically and the witness runs on the next reachable checkpoint (decision 3a).

Source

pub async fn connect_with_state_sealed_tls( config: &Config, server_name: &str, tls: &ClientTls, state: &[u8], store: Arc<dyn SecureStore + Send + Sync>, ) -> Result<Self>

Like connect_with_state_sealed, but over TLS to a server presenting server_name.

Source

pub fn address(&self) -> &DeviceAddr

This client’s own address.

Source

pub fn export_identity(&self) -> Vec<u8>

Serialize this client’s identity secret so it can be persisted and reused across restarts via connect_with_identity. The bytes carry a private key — store them as a secret, at rest as carefully as any other key material.

Source

pub async fn export_state(&self) -> Result<Vec<u8>>

Serialize this client’s full resumable state — its identity, its live ratchet sessions with every peer it has an open conversation with, and its published prekey store — so a process restart resumes conversations mid-ratchet instead of re-establishing them, and can still decrypt a first-contact message a new peer sent while this device was down. Re-establishing loses any message a peer sent while this device was down; persisting the sessions keeps live conversations decryptable, and persisting the prekeys keeps first contact decryptable — a message a new peer sent to a published one-time prekey is encrypted to that prekey, and its private half lives only in the prekey store. Restore with connect_with_state or sign_in_with_state.

The bytes carry the identity private key, session secrets and prekey private halves — store them as a secret, encrypted at rest. Supersedes export_identity, which keeps only the identity.

Source

pub async fn export_state_sealed(&self) -> Result<Vec<u8>>

Export a sealed full state — decision 0078’s anchor B, the rollback-resistant path: against an attacker who can rewrite the state file it refuses a forged generation and, via the per-send store counter, any state older than the latest send — closing EX-03 against a file-rewriter given a rollback-resistant store.

The bytes carry the same identity, sessions and prekeys as export_state, but the persisted-state generation is bound inside an authenticator keyed by the attached SecureStore rather than appended in plaintext, and the store’s rollback counter is bound alongside it. An attacker who rewrites the file cannot forge either without the secure-storage key. Restore with connect_with_state_sealed or sign_in_with_state_sealed.

Requires a store attached (attach_secure_store, or a sealed restore, which attaches it). The counter it binds is the current value — send and receive advance it, so it already reflects this state’s ratchet position; export does not bump it. That is what makes a restore of a state older than the latest send caught, not just an older export (the per-send window ER-1 flagged).

The seal authenticates but does not encrypt: the bytes carry secrets and must be stored encrypted at rest. What the seal adds is freshness — the property EX-03 is about — not confidentiality.

Source

pub async fn checkpoint(&mut self) -> Result<DirResponse>

Present this client’s persisted-state generation to the directory (decision 0078), and act on a rollback.

The directory advances its per-device anchor and reports whether the state is current (Fresh) or older than one it has already witnessed (RolledBack). On a rollback the sessions are discarded and the identity kept (decision 3): the ratchets an attacker rewound are not resumed, so they re-establish fresh on the next message and the attacker gets a client that has forgotten the chain keys it wanted replayed.

Opt-in, and detection-only. This is 0078’s anchor A. It is not invoked automatically on any shipping path, because the generation it presents is unauthenticated in the state file and a file-rewriter forges it (internal audit AR-1) — so calling it defends only against a legitimate backup restore or a naive whole-file replay, not against the attacker EX-03 is about, and it exposes the anchor-poisoning DoS of AR-2. A deployment that wants that limited detection calls it after connecting, after activity, and on a schedule; the real control waits for anchor B (an authenticated generation under a secure-storage key). Returns the directory’s verdict; a network error is surfaced, the rollback is not an error.

Source

pub fn restore_outcome(&self) -> RestoreOutcome

What the constructor found: whether restored sessions are in use or were discarded as a rollback. Read it after any sign-in or connect; a rollback the directory catches later (checkpoint) moves it too.

Source

pub fn attach_secure_store( &mut self, store: Arc<dyn SecureStore + Send + Sync>, ) -> Result<()>

Turn on per-send rollback protection (decision 0078, closing the ER-1 per-send window): hold store for the client’s life so every send and every ratchet-advancing receive commits the freshness counter to it, and a sealed export binds that counter’s current value.

Call this once, before sending, on a freshly connected or sign_ined client; a sealed restore (connect_with_state_sealed) attaches the store itself. Attaching the same store the platform holds for export_state_sealed is what makes the counter monotone across restarts.

One store per client: a second attach is refused as Error::InvalidArgument, since re-rooting the counter mid-life would let a state older than the last send pass. The store’s current counter is read on attach and every later bump must exceed it, so a store reset underneath the client fails the next send.

Source

pub fn has_open_session(&self, peer: &DeviceAddr) -> bool

Whether this client currently holds an open session with peer — a conversation it can resume without a fresh handshake. Goes to false for every peer after a rollback checkpoint discards the sessions.

Source

pub async fn reconnect(&mut self) -> Result<()>

Re-establish the directory and relay connections, re-authenticating with this client’s identity. Sessions live in memory and survive, so conversations continue where they left off. send and receive call this themselves (with bounded backoff) when they find the connection gone; it is public for a caller that wants to reconnect eagerly.

Source

pub async fn send(&mut self, to: &DeviceAddr, message: &[u8]) -> Result<()>

Encrypt and send message to to, opening a session (via a directory lookup of the recipient’s bundle) on first contact.

If the connection is found dead, a bounded reconnect runs and the same ciphertext is retried once — the retry re-sends, it does not re-encrypt, so the ratchet advances exactly once per call. In the rare case where the first attempt landed before the connection died, the duplicate is dropped by the recipient (a replayed ciphertext cannot decrypt twice).

Source

pub async fn receive(&mut self) -> Result<Vec<Received>>

Wait for the next non-empty batch of mail, decrypting and acknowledging every pending message and returning each with its sender. Because the relay attributes messages, this works even for a first-contact message from a peer this client has never talked to.

The relay is polled before waiting, so everything queued while this client was offline is drained immediately — a reconnecting client gets its backlog without waiting for the next push. If the connection is found dead, a bounded reconnect (immediate, then 1s/2s/4s/8s) runs before giving up.

A message that cannot be decrypted — a replayed ciphertext, a corrupt envelope — is acknowledged past and dropped rather than returned: retrying it can never succeed, and refusing to advance would wedge the queue behind one poison message forever.

Source

pub fn mail(&self) -> MailSignal

The signal that says mail may be waiting: pinged on every push from the relay, across reconnects, and when a connection ends. A caller that shares this client behind a lock waits on this outside the lock and then calls drain, so another task can send meanwhile; receive is that loop for a caller holding the client itself. The signal keeps one permit, so a push that lands between a poll and the wait is not missed.

Source

pub fn inbound(&mut self) -> impl Stream<Item = Result<Received>> + '_

Inbound messages as a stream, one at a time in the order the relay delivered them: receive flattened, for a caller that wants each message as it arrives rather than batches. The stream borrows the client while it is polled, so a task that also sends keeps the client behind a lock and does what the FFI and browser heads do: wait on mail outside the lock and drain inside it. An error is yielded once and ends the stream; calling again starts another. The stream is not Unpin: pin it (std::pin::pin!) before calling next.

Source

pub async fn drain(&mut self) -> Result<Vec<Received>>

A single non-blocking poll: whatever is queued for this device right now, or an empty batch if nothing is pending. Unlike receive it never waits for the server’s push — the caller drives the cadence.

This exists for a loop that must also do something else between checks for mail (read the keyboard, redraw a UI): receive would park in its wait and starve that work, and cancelling receive on a timer is unsafe because the poll it wraps is a socket round trip that must not be torn mid-response. drain returns promptly either way, so the caller decides when to poll again.

Source

pub async fn rotate(&mut self) -> Result<()>

Re-key this client’s identity: generate a fresh identity, rotate the directory binding to it — authorized by the current key (decision record 0024) — and adopt it.

Rotation invalidates existing peer sessions: the new identity has an empty store, so the next send to each peer opens a fresh session, and a peer who verified the old key sees a safety-number change. The live relay connection, authenticated under the old key when the client connected, stays valid for its lifetime.

Source

pub async fn delivered_watermark( &mut self, devices: &[DeviceAddr], ) -> Result<u64>

The delivered-to-all watermark across devices: how many messages the relay has confirmed delivered to every one of them (decision record 0026) — the minimum of their per-device delivered counts. Every address must belong to this client’s user; the relay refuses a query that spans another user’s devices.

Source

pub async fn find(&mut self, username: &str) -> Result<Option<Contact>>

Resolve username within this client’s own tenant to a Contact, or None if no such user is registered. The username is combined with the client’s tenant — read from its own handle "<tenant>/<user>" — so it finds users in the same tenant; exact resolution only, no enumeration. Errs if this client is not operating under a tenant handle (the pre-account path). Resolves the primary device (device 1); multi-device resolution is later work.

Source

pub async fn sign_up( accounts: SocketAddr, api_key: &str, username: &str, password: &str, ) -> Result<()>

Sign up a new user under a tenant. A control-plane action: it creates the account but does not connect — call sign_in afterwards for a connected client. api_key scopes it to the tenant.

Source

pub async fn sign_up_tls( accounts: SocketAddr, server_name: &str, tls: &ClientTls, api_key: &str, username: &str, password: &str, ) -> Result<()>

Like sign_up, but over TLS to a server presenting server_name (the hosted path).

Source

pub async fn sign_in(config: &AccountConfig) -> Result<Self>

Sign in and provision a fresh device identity, returning a client that sends and receives under the account’s handle (e.g. acme/alice).

Save export_identity and reconnect with sign_in_with_identity on later runs, so the device keeps the same key: re-provisioning a new key for an already-bound handle is refused by trust-on-first-use.

Source

pub async fn sign_in_tls( config: &AccountConfig, server_name: &str, tls: &ClientTls, ) -> Result<Self>

Like sign_in, but over TLS to a server presenting server_name (the hosted path). tls decides the trust — ClientTls::web_pki for a public CA such as Let’s Encrypt.

Source

pub async fn sign_in_with_identity( config: &AccountConfig, identity: &[u8], ) -> Result<Self>

Sign in and provision under a saved device identity (from export_identity), keeping the same bound key across restarts.

Source

pub async fn sign_in_with_state( config: &AccountConfig, state: &[u8], ) -> Result<Self>

Sign in under a full saved state (from export_state): the same device identity and its live ratchet sessions, so conversations resume mid-ratchet across a process restart. The account-path sibling of connect_with_state.

Source

pub async fn sign_in_with_state_tls( config: &AccountConfig, server_name: &str, tls: &ClientTls, state: &[u8], ) -> Result<Self>

Like sign_in_with_state, but over TLS to a server presenting server_name (the hosted path).

Source

pub async fn sign_in_with_state_sealed( config: &AccountConfig, state: &[u8], store: Arc<dyn SecureStore + Send + Sync>, ) -> Result<Self>

Sign in under a sealed full state (from export_state_sealed) — the account-path sibling of connect_with_state_sealed, and the rollback-resistant sign-in path (decision 0078, anchor B). Attaches store; a forged state is refused, any state older than the latest send is caught by the store counter, and the generation is witnessed to the directory — closing EX-03 against a file-rewriter given a rollback-resistant store.

Source

pub async fn sign_in_with_state_sealed_tls( config: &AccountConfig, server_name: &str, tls: &ClientTls, state: &[u8], store: Arc<dyn SecureStore + Send + Sync>, ) -> Result<Self>

Like sign_in_with_state_sealed, but over TLS to a server presenting server_name (the hosted path).

Auto Trait Implementations§

§

impl<P = OpenParty> !Freeze for Client<P>

§

impl<P = OpenParty> !RefUnwindSafe for Client<P>

§

impl<P = OpenParty> !UnwindSafe for Client<P>

§

impl<P> Send for Client<P>
where P: Send,

§

impl<P> Sync for Client<P>
where P: Sync,

§

impl<P> Unpin for Client<P>
where P: Unpin,

§

impl<P> UnsafeUnpin for Client<P>
where P: UnsafeUnpin,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V