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>
impl<P: CryptoProvider> Client<P>
Sourcepub async fn connect(config: &Config) -> Result<Self>
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.
Sourcepub async fn connect_tls(
config: &Config,
server_name: &str,
tls: &ClientTls,
) -> Result<Self>
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.
Sourcepub async fn connect_with_identity(
config: &Config,
identity: &[u8],
) -> Result<Self>
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.
Sourcepub async fn connect_with_state(config: &Config, state: &[u8]) -> Result<Self>
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.
Sourcepub async fn connect_with_state_tls(
config: &Config,
server_name: &str,
tls: &ClientTls,
state: &[u8],
) -> Result<Self>
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.
Sourcepub async fn connect_with_state_sealed(
config: &Config,
state: &[u8],
store: Arc<dyn SecureStore + Send + Sync>,
) -> Result<Self>
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).
Sourcepub async fn connect_with_state_sealed_tls(
config: &Config,
server_name: &str,
tls: &ClientTls,
state: &[u8],
store: Arc<dyn SecureStore + Send + Sync>,
) -> Result<Self>
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.
Sourcepub fn address(&self) -> &DeviceAddr
pub fn address(&self) -> &DeviceAddr
This client’s own address.
Sourcepub fn export_identity(&self) -> Vec<u8> ⓘ
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.
Sourcepub async fn export_state(&self) -> Result<Vec<u8>>
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.
Sourcepub async fn export_state_sealed(&self) -> Result<Vec<u8>>
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.
Sourcepub async fn checkpoint(&mut self) -> Result<DirResponse>
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.
Sourcepub fn restore_outcome(&self) -> RestoreOutcome
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.
Sourcepub fn attach_secure_store(
&mut self,
store: Arc<dyn SecureStore + Send + Sync>,
) -> Result<()>
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.
Sourcepub fn has_open_session(&self, peer: &DeviceAddr) -> bool
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.
Sourcepub async fn reconnect(&mut self) -> Result<()>
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.
Sourcepub async fn send(&mut self, to: &DeviceAddr, message: &[u8]) -> Result<()>
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).
Sourcepub async fn receive(&mut self) -> Result<Vec<Received>>
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.
Sourcepub fn mail(&self) -> MailSignal
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.
Sourcepub fn inbound(&mut self) -> impl Stream<Item = Result<Received>> + '_
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.
Sourcepub async fn drain(&mut self) -> Result<Vec<Received>>
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.
Sourcepub async fn rotate(&mut self) -> Result<()>
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.
Sourcepub async fn delivered_watermark(
&mut self,
devices: &[DeviceAddr],
) -> Result<u64>
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.
Sourcepub async fn find(&mut self, username: &str) -> Result<Option<Contact>>
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.
Sourcepub async fn sign_up(
accounts: SocketAddr,
api_key: &str,
username: &str,
password: &str,
) -> Result<()>
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.
Sourcepub async fn sign_up_tls(
accounts: SocketAddr,
server_name: &str,
tls: &ClientTls,
api_key: &str,
username: &str,
password: &str,
) -> Result<()>
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).
Sourcepub async fn sign_in(config: &AccountConfig) -> Result<Self>
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.
Sourcepub async fn sign_in_tls(
config: &AccountConfig,
server_name: &str,
tls: &ClientTls,
) -> Result<Self>
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.
Sourcepub async fn sign_in_with_identity(
config: &AccountConfig,
identity: &[u8],
) -> Result<Self>
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.
Sourcepub async fn sign_in_with_state(
config: &AccountConfig,
state: &[u8],
) -> Result<Self>
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.
Sourcepub async fn sign_in_with_state_tls(
config: &AccountConfig,
server_name: &str,
tls: &ClientTls,
state: &[u8],
) -> Result<Self>
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).
Sourcepub async fn sign_in_with_state_sealed(
config: &AccountConfig,
state: &[u8],
store: Arc<dyn SecureStore + Send + Sync>,
) -> Result<Self>
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.
Sourcepub 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>
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).