SecureMessageClient

class SecureMessageClient(val localAddress: DeviceAddress, storage: ClientStorage, protocol: ProtocolEngine, transport: SecureMessageTransport, preKeyConfiguration: PreKeyConfiguration = PreKeyConfiguration(), clock: Clock = Clock.System)

Sends and receives messages for localAddress. Envelope payloads are CiphertextMessageCodec-encoded ciphertext messages; their plaintext is a SecurePayloadCodec reliability frame.

Messages are reliable across collisions, lost envelopes and lost acknowledgements (docs/message-reliability.md). send gives each application message a LogicalMessageId and keeps it pending, plaintext included, until the recipient acknowledges it. retryPendingMessages encrypts pending messages again, under the current session and with the same logical ID. Both peers must use this frame (milestone 8); raw plaintext from older peers is rejected.

Received messages are delivered at least once, until the application finalizes them (docs/application-delivery.md, docs/message-discard.md). decrypt stores a new message as pending and returns it as ReceiveResult.Delivery; pendingReceivedMessages pages through every pending message again, also after a restart. Once the application has applied a message durably, it calls commitReceivedMessage; for a message it can never apply, it calls discardReceivedMessage. Either records a terminal tombstone and only then sends the encrypted acknowledgement. An acknowledgement therefore means that the receiving application durably finalized the message and will not have it redelivered; the sender cannot tell a commit from a discard. This is not exactly-once processing: an application that crashes between its own commit and commitReceivedMessage sees the message again, so it should apply messages idempotently, keyed by sender and logical ID. Finalized IDs are kept for duplicate suppression until pruneProcessedMessages removes them.

The client owns its local protocol state in storage: identity key, signed prekeys, one-time prekeys and sessions. Call initialize once per start before anything else; it creates missing state and keeps existing state. Other operations never create an identity on their own. Before the first initialize on a storage they throw SecureMessageClientException.NotInitialized. Storage is read on every operation; the client caches nothing.

Every operation stores the new session state in the same ClientStorage.transaction as the crypto work. A failed encrypt or decrypt leaves the stored state as it was. Network calls happen outside transactions.

initialize and rotateSignedPreKey change local state only. Call publishPreKeys afterwards to upload the public material.

The server only accepts device-scoped requests from the registered device (docs/server-authentication.md). initialize creates the device authentication key, a signing key separate from the messaging identity; registerDevice registers its public half with the server, explicitly and once. publishPreKeys and receive then sign their requests with it; the application never handles signatures. Fetching bundles and sending envelopes need no authentication.

Signed prekeys expire (docs/signed-prekey-lifecycle.md). A replaced signed prekey stays usable for new sessions for PreKeyConfiguration.signedPreKeyGracePeriod; after that, initiations that name it fail with SecureMessageClientException.ExpiredSignedPreKey and initialize deletes it. Established sessions are not affected. initialize also rotates the current signed prekey once it is PreKeyConfiguration.signedPreKeyRotationAge old, so long-running applications call initialize periodically, followed by publishPreKeys. Time comes from clock.

Remote identity keys are trusted on first use (docs/identity-trust.md). The first identity key that sets up a session with a remote device, as initiator or responder, is pinned in the same transaction that stores the session. A different key for that device later fails with SecureMessageClientException.IdentityChanged and changes nothing. This detects identity changes after first contact; it does not prove who the remote party is on first contact.

A sender with the pinned identity may set up a new session while one exists (lost session state, simultaneous initiation); see docs/session-lifecycle.md. The new session replaces the old one atomically. Initiations that were replaced or lost a collision are retired and rejected for good (SecureMessageClientException.StaleSessionInitiation). When both sides initiate at once, both keep the initiation with the smaller SessionInitiationId; the losing one's messages fail with SecureMessageClientException.SessionCollision and are not delivered.

Sessions converge only if, per (sender, recipient) pair, a PreKeyMessage is processed before every envelope the sender handed to the transport after it (docs/transport-ordering.md). send hands envelopes over in encryption order; the server keeps them in that order per pair; the application must decrypt one sender's envelopes one at a time, in the order SecureMessageTransport.receive returned them.

A device whose device authentication key is lost can be recovered by another registered device of the same user (docs/device-recovery.md): prepareDeviceAuthenticationRecovery on the lost device, authorizeDeviceRecovery on the other one, completeDeviceAuthenticationRecovery on the lost device again. This replaces the server authentication key only, never the messaging identity.

A device that still holds its key can replace it itself with a routine rotation (docs/device-authentication-rotation.md): rotateDeviceAuthenticationKey, or prepareDeviceAuthenticationRotation and completeDeviceAuthenticationRotation. The current key authorizes the new one; no other device takes part. Nothing rotates automatically. deviceAuthenticationHealth classifies the state of these credentials read-only (docs/device-authentication-health.md).

Identities can be verified manually and changes accepted explicitly (docs/identity-verification.md): safetyNumber gives a value both devices derive alike for the user to compare, markRemoteIdentityVerified records the user's confirmation for exactly the pinned key, and acceptRemoteIdentityChange installs a new identity the user approved, resetting the session and the verification. Nothing here happens automatically.

Not handled yet: pending messages are not retried automatically, and prekeys are not published automatically.

Constructors

Link copied to clipboard
constructor(localAddress: DeviceAddress, storage: ClientStorage, protocol: ProtocolEngine, transport: SecureMessageTransport, preKeyConfiguration: PreKeyConfiguration = PreKeyConfiguration(), clock: Clock = Clock.System)

Properties

Link copied to clipboard

Functions

Link copied to clipboard

Abandons message (docs/outbound-message-lifecycle.md).

Gives up on delivering message id to recipient: removes it, plaintext included, from the pending messages in one transaction (AbandonStatus.ABANDONED), so no retry sends it again (docs/outbound-message-lifecycle.md). Only this message changes; the session and every other message stay as they are.

Link copied to clipboard

Accepts change, an identity change the user explicitly approved (docs/identity-verification.md). Never called by the client itself: until this runs, every message and bundle with the new key fails with SecureMessageClientException.IdentityChanged.

Link copied to clipboard

Authorizes request, another device's recovery request, with this device's active device authentication key (docs/device-recovery.md). The application shows the user which device (DeviceRecoveryRequest.target) is being recovered before calling this, and transfers the result back to it. No network I/O.

Link copied to clipboard

Deletes the pending recovery key. Only for a recovery that will not be completed: if the server already accepted it, the device has to be recovered again.

Link copied to clipboard

Deletes the pending rotation key; the active key and a pending recovery key are not touched. Only for a rotation that will not be completed: call resolveDeviceAuthenticationRotation first, because if the server already accepted it, only the deleted key authenticates this device and it has to be recovered (docs/device-recovery.md).

Link copied to clipboard

Deletes the pending last-device recovery key; the active key and the other slots are not touched. Only for a recovery that will not be completed: call resolveLastDeviceRecovery first, because if the server already accepted it, the device has to be recovered again.

Link copied to clipboard

Cancels the pending recovery key reset as this device (a signed request; docs/recovery-key-reset.md). The recovery key and its epoch stay; last-device recovery challenges stay valid.

Cancels the pending recovery key reset with the offline recovery key it would replace, currentKey (docs/recovery-key-reset.md): an independent veto that needs no device and works without this device's authentication key. Throws IllegalArgumentException if currentKey is not the key reset replaces.

Link copied to clipboard

Records that the application has durably applied message and acknowledges it to its sender (docs/application-delivery.md).

Records that the application has durably applied message id from sender and acknowledges it (docs/application-delivery.md).

Link copied to clipboard

Compares the safety number of remote with scanned, the payload the other device shows (SafetyNumber.encode). Never changes trust state, also on a match: call markRemoteIdentityVerified once the user confirmed. Throws SecureMessageClientException.InvalidSafetyNumberPayload for a payload that does not decode and SecureMessageClientException.RemoteIdentityNotKnown if nothing is pinned.

Link copied to clipboard

Submits authorization for this device's pending recovery and, once the server replaced the registered key, makes the pending key the active device authentication key (docs/device-recovery.md). From then on every request is signed with it; the old key no longer works.

Link copied to clipboard

Submits the pending rotation and, once the server replaced the registered key, makes the pending key K2 the active device authentication key (docs/device-authentication-rotation.md). From then on every request is signed with K2; K1 no longer works.

Link copied to clipboard

Recovers this device's server authentication with the offline recoveryKey (docs/last-device-recovery.md): requests a challenge from the server, signs it together with the pending replacement key (with recoveryKey and, as proof of possession, with the replacement key), submits it, and once the server replaced the registered key makes the pending key the active device authentication key. recoveryKey is used only in memory and never stored.

Link copied to clipboard

Completes the pending recovery key reset with newKey (docs/recovery-key-reset.md): this device's signed request and newKey's proof of possession over the pending reset. The server accepts it only from the reset's eligibility time on (its own clock). newKey is not stored.

Link copied to clipboard

Creates a new offline last-device recovery key for this user (docs/last-device-recovery.md). Returns it to the application and stores nothing: the application backs it up offline (for example LastDeviceRecoveryKey.encode printed or in a password manager) and registers it with registerLastDeviceRecoveryKey. Whoever holds it can replace the server authentication key of every registered device of the user. No network I/O.

Link copied to clipboard

The public prekey data to publish: identity key and current signed prekey. PreKeyBundle.oneTimePreKey is always null. Which one-time prekey a fetcher gets is up to the server, see publicOneTimePreKeys.

Link copied to clipboard
suspend fun decrypt(envelope: EncryptedEnvelope): ReceiveResult

Decrypts and processes envelope (docs/message-reliability.md, docs/application-delivery.md).

Link copied to clipboard

The state of this device's server authentication credentials (docs/device-authentication-health.md), for the application to show or act on. Read-only: never initializes, registers, rotates, recovers, resolves, cancels, creates a key, writes storage or caches anything, and is never called implicitly.

Link copied to clipboard

This device's server authentication status (docs/device-authentication-rotation.md): the authentication epoch and the server time the registered key was installed at, read from the server with a request signed by the active key (never cached, never stored locally), the key's age by this client's clock (clamped at zero), and whether a rotation or recovery key is pending locally (DeviceAuthenticationRotationStatus.pendingRecovery covers device recovery and last-device recovery). Changes nothing. Throws SecureMessageClientException.NotInitialized without an active key and SecureMessageTransportException.AuthenticationFailed if the active key is not the registered one.

Link copied to clipboard

Discards message for good and acknowledges it to its sender (docs/message-discard.md).

Discards message id from sender for good: the application will never apply it (docs/message-discard.md). Only for permanent decisions; leave a message that failed for a temporary reason pending.

Link copied to clipboard

Evaluates policy against the server's key installation time (deviceAuthenticationRotationStatus): DeviceAuthenticationRotationDecision.Due once the key's age is at least DeviceAuthenticationRotationPolicy.maxKeyAge. A pending recovery or rotation is reported without asking the server. Never rotates and never creates a key.

Link copied to clipboard
suspend fun initialize()

Makes sure the local identity, a current signed prekey and PreKeyConfiguration.oneTimePreKeyTarget one-time prekeys exist, and runs signed prekey maintenance: rotation of a current key that reached PreKeyConfiguration.signedPreKeyRotationAge, deletion of replaced keys whose grace period is over, and removal of retired session initiations that only those keys could accept. All in one transaction. An existing identity is never replaced. Safe to call repeatedly. No network I/O: call publishPreKeys afterwards, a rotation changes the published signed prekey.

Link copied to clipboard

The pending recovery key reset of this user, or RecoveryKeyResetStatus.None (docs/recovery-key-reset.md). A signed request; never cached. The library never polls: applications that want every device to notice a reset must call this themselves (for example on start and periodically) and warn the user while one is pending.

The pending recovery key reset of this user, queried with the current offline recovery key currentKey instead of a device (docs/recovery-key-reset.md). Works without this device's authentication key and before initialize: the holder of the offline key can always see a reset that would replace it. currentKey is not stored or sent; only its signature is. Throws SecureMessageTransportException.RecoveryKeyResetRejected (INVALID_PROOF if currentKey is not the active key, NOT_CONFIGURED if none is active).

Link copied to clipboard

The server's state of this user's offline recovery key (docs/recovery-key-lifecycle.md): unconfigured, active (epoch, installation time, public key) or revoked (epoch, revocation time). A signed request with this device's active device authentication key; never cached. Compare LastDeviceRecoveryKeyStatus.Active.isKey with a backed-up key's LastDeviceRecoveryKey.publicKey to check that the backup is still the active key.

Link copied to clipboard

Makes the pinned identity of remote again, for example when the user withdraws a verification. The pin stays. Throws SecureMessageClientException.RemoteIdentityNotKnown.

Link copied to clipboard
suspend fun markRemoteIdentityVerified(safetyNumber: SafetyNumber)

Records that the user compared safetyNumber with the other device and confirmed it: the pinned identity of the remote device in it becomes VerificationState.VERIFIED. Bound to that exact key: if the pin changed since safetyNumber was derived, this throws SecureMessageClientException.RemoteIdentityConflict and changes nothing. A later accepted identity change resets it to VerificationState.UNVERIFIED.

Link copied to clipboard
suspend fun pendingMessageCount(recipient: DeviceAddress? = null): Long

The number of sent messages not acknowledged yet, to recipient only if given. Opens no record.

Link copied to clipboard
suspend fun pendingMessages(afterSequence: Long? = null, limit: Int, recipient: DeviceAddress? = null): PendingMessagePage

One page of the sent messages not acknowledged yet, with their plaintext (docs/outbound-message-lifecycle.md): at most limit (1..PendingMessagePage.MAX_SIZE) messages with a PendingMessage.sequence greater than afterSequence (from the start if null), to recipient only if given, in ascending sequence order.

Link copied to clipboard
suspend fun pendingReceivedMessageCount(sender: DeviceAddress? = null): Long

The number of received messages not finalized yet, from sender only if given. Opens no record.

Link copied to clipboard
suspend fun pendingReceivedMessages(afterSequence: Long? = null, limit: Int, sender: DeviceAddress? = null): PendingReceivedMessagePage

One page of the received messages the application has not finalized yet, with their plaintext (docs/message-discard.md): at most limit (1..PendingReceivedMessagePage.MAX_SIZE) messages with a ReceivedMessage.sequence greater than afterSequence (from the start if null), from sender only if given, in ascending sequence order. Call it after a restart: a message stays pending until commitReceivedMessage or discardReceivedMessage, whether or not its sender sends it again.

Link copied to clipboard

Starts or resumes a device authentication recovery of this device (docs/device-recovery.md): for a device whose registered device authentication key is lost, authorizer, another registered device of the same user, can authorize a replacement key.

Link copied to clipboard

Starts or resumes a routine rotation of this device's authentication key (docs/device-authentication-rotation.md), for a device that still holds its registered key K1. Creates the replacement key pair K2 once and keeps it as pending rotation key, sealed at rest; later calls reuse it until the rotation completes or is cancelled. K1 stays the active key and keeps signing every request. No network I/O: call completeDeviceAuthenticationRotation.

Link copied to clipboard

Starts or resumes a last-device recovery of this device (docs/last-device-recovery.md): for a device whose registered device authentication key is lost when no other registered device of the user can authorize a device recovery. Creates the replacement key pair once and keeps it as pending last-device recovery key, sealed at rest; later calls reuse it until the recovery completes or is cancelled. The active key (if any) is not touched. No network I/O: call completeLastDeviceRecovery with the offline recovery key.

Link copied to clipboard

Removes the tombstones of finalized (committed or discarded) messages whose age (the clock now minus their commit or discard time) is at least ProcessedInboundRetentionPolicy.maxAge, and returns their number (docs/application-delivery.md, docs/message-discard.md). Pending messages are never removed. Entries from before milestone 20 without a commit time are kept until initialize stamps them.

Link copied to clipboard

Public halves of all unused local one-time prekeys, for upload to a server. Nothing is reserved or marked as published: a key is only removed when a session is accepted with it.

Link copied to clipboard
suspend fun publishPreKeys()

Uploads the public prekey material: identity key, current signed prekey and the public halves of all local one-time prekeys. Private keys never leave the device.

Link copied to clipboard

Removes and returns the envelopes the server holds for this device, in an authenticated request. Pass them to decrypt one at a time, in this order (docs/transport-ordering.md).

Link copied to clipboard
suspend fun recoverLastDevice(recoveryKey: LastDeviceRecoveryKey)

prepareLastDeviceRecovery followed by completeLastDeviceRecovery. Resumes a pending recovery with its existing key.

Link copied to clipboard

Whether this user has a pending recovery key reset to surface (docs/recovery-key-reset.md, "Reset awareness"). Read-only: reads the recovery key status and then the reset status (two signed requests with this device's active device authentication key, never cached), checks that the reset is bound to the active key and epoch of this user, and classifies it with the client clock: RecoveryKeyResetAwareness.Pending before RecoveryKeyResetStatus.Pending.eligibleAt, RecoveryKeyResetAwareness.Eligible from then on. The server still decides eligibility with its own clock when a completion is submitted.

Link copied to clipboard
suspend fun registerDevice()

Registers this device's authentication public key with the server (docs/server-authentication.md), in a request signed with that key. Needed once before publishPreKeys and receive; calling it again is safe. Throws SecureMessageTransportException.DeviceRegistrationConflict if the server has another key for localAddress; there is no reset. The first registration of an address needs the host application's authorization on the server (S1; SecureMessageTransportException.DeviceRegistrationNotAuthorized).

Link copied to clipboard

Registers key's public key as this user's last-device recovery key, in a request signed with this device's active device authentication key and with the recovery key's proof of possession. Only the public key is sent. Registering the same key again is harmless; the server never replaces a registered recovery key (SecureMessageTransportException.LastDeviceRecoveryKeyRejected with CONFLICT). Never called implicitly. Throws SecureMessageClientException.NotInitialized without an active key.

Link copied to clipboard

The identity key pinned for remote, or null if there was no successful first contact with it yet (or only before identity keys were pinned). Public key material.

Link copied to clipboard

The trust state of remote: pinned identity key and verification state, or null if nothing is pinned yet (docs/identity-verification.md).

Link copied to clipboard

Requests a delayed reset of this user's offline recovery key, for when that key is lost (docs/recovery-key-reset.md). Exceptional: if the current key is still available, use rotateLastDeviceRecoveryKey.

Link copied to clipboard

Asks the server whether this device's pending recovery key is its registered key, with a registration request signed by that key (a registration of exactly the registered key changes nothing). If it is, makes it the active key and returns true; if the server holds another key, returns false and keeps the pending key. For a recovery whose response was lost and whose authorization expired. Throws SecureMessageClientException.NoPendingDeviceRecovery.

Link copied to clipboard

Asks the server whether this device's pending rotation key is its registered key, with a registration request signed by that key. If it is, makes it the active key and returns true; if the server holds another key, returns false and keeps both keys. For a rotation whose response was lost, or that the server applied before the local promotion failed. Throws SecureMessageClientException.NoPendingDeviceAuthenticationRotation.

Link copied to clipboard

Asks the server whether this device's pending last-device recovery key is its registered key, with a registration request signed by that key (a registration of exactly the registered key changes nothing). If it is, makes it the active key and returns true; if the server holds another key, returns false and keeps the pending key. Throws SecureMessageClientException.NoPendingLastDeviceRecovery.

Link copied to clipboard

Sends every message pending for remote again, oldest first, and returns the IDs whose new envelope the transport took. Each attempt is a fresh encryption under the current session with a new envelope ID; the logical ID stays. Messages stay pending until remote acknowledges them, which it does only after its application committed them (docs/application-delivery.md), so calling this again sends them again. Before that commit the recipient delivers the retry again as the same message; after it, the recipient only acknowledges it again.

Link copied to clipboard

Revokes this user's offline recovery key currentKey (docs/recovery-key-lifecycle.md): this device's signed request and currentKey's signature. Afterwards no last-device recovery is possible, and outstanding challenges are dead, until a new key is registered with registerLastDeviceRecoveryKey (the recovery key epoch continues). Nothing is generated as a replacement.

Link copied to clipboard

prepareDeviceAuthenticationRotation followed by completeDeviceAuthenticationRotation. Resumes a pending rotation with its existing key.

Link copied to clipboard

Rotates the device authentication key if policy says it is due, with the routine rotation of rotateDeviceAuthenticationKey (docs/device-authentication-rotation.md). Only runs when the application calls it: nothing in the library calls it implicitly.

Link copied to clipboard

Replaces this user's offline recovery key currentKey with newKey (docs/recovery-key-lifecycle.md). Two authorities: this device's signed request, and currentKey's signature; newKey proves possession. Neither key is stored.

Link copied to clipboard

Replaces the current signed prekey with a new one and returns its public half. The old private key stays stored for PreKeyConfiguration.signedPreKeyGracePeriod, so first-contact messages that still use it can be decrypted. Local only: call publishPreKeys afterwards.

Link copied to clipboard

The safety number change's presented key would have, so the user can compare it with the other device before accepting the change. Changes nothing and trusts nothing.

The safety number of this device and remote, from the local identity key and the key pinned for remote (docs/identity-verification.md). Both devices derive the same value. Show SafetyNumber.displayString or SafetyNumber.encode (for example as a QR code) and let the user compare it with the other device. Throws SecureMessageClientException.RemoteIdentityNotKnown if nothing is pinned; never fetches a bundle. Changes nothing.

Link copied to clipboard
suspend fun send(remote: DeviceAddress, plaintext: ByteArray): SentMessage

Sends plaintext to remote as a new logical message and returns its LogicalMessageId and the envelope of this first attempt. Starts a session if there is none. Bodies larger than SecurePayloadCodec.MAX_BODY_SIZE are rejected with ProtocolException.MessageTooLarge.