Skip to main content

tacenta_client/
lib.rs

1//! The high-level Tacenta client.
2//!
3//! One [`Client`] wraps everything a caller would otherwise orchestrate by
4//! hand — an identity and its provider session store, a directory connection, an
5//! authenticated relay connection, and per-peer sessions — behind
6//! [`connect`](Client::connect), [`send`](Client::send), and
7//! [`receive`](Client::receive) (or [`inbound`](Client::inbound), the same
8//! one message at a time). This is the surface the platform bindings
9//! (UniFFI / wasm) export.
10//!
11//! An app starts one layer up, at the tenant handle (decision record 0090):
12//! [`Tacenta::connect`] takes an API key and a server name, fetches the
13//! server's service document, and hands out signed-in clients through
14//! [`Tacenta::sign_up`] and [`Tacenta::sign_in`], so no app carries a host or
15//! a port. The [`Config`]-based constructors below are the layer under that:
16//! explicit addresses, for a test or a deployment that already knows them.
17//!
18//! ```no_run
19//! # async fn f() -> Result<(), tacenta_client::Error> {
20//! use tacenta_client::Tacenta;
21//! let tenant = Tacenta::connect("tct_your_api_key").await?;
22//! tenant.sign_up("alice", "correct horse").await?;
23//! let mut alice = tenant.sign_in("alice", "correct horse").await?;
24//! let to_bob = alice.find("bob").await?.expect("bob signed up");
25//! alice.send(&to_bob.address, b"hello").await?;
26//! # Ok(()) }
27//! ```
28//!
29//! With explicit addresses:
30//!
31//! ```no_run
32//! # async fn f() -> Result<(), tacenta_client::Error> {
33//! use tacenta_client::{Config, DefaultClient, DeviceAddr};
34//! let mut alice = DefaultClient::connect(&Config {
35//!     directory: "127.0.0.1:4720".parse().unwrap(),
36//!     relay: "127.0.0.1:4721".parse().unwrap(),
37//!     user: "+alice".into(),
38//!     device: 1,
39//! })
40//! .await?;
41//! alice.send(&DeviceAddr::new("+bob", 1), b"hello").await?;
42//! for message in alice.receive().await? {
43//!     println!("from {}: {:?}", message.from.user, message.plaintext);
44//! }
45//! # Ok(()) }
46//! ```
47
48use std::collections::HashSet;
49use std::net::SocketAddr;
50use std::sync::Arc;
51
52use rand::TryRngCore as _;
53// Of the imports here, only `DefaultProvider` names a provider, and only as the
54// default type parameter -- so a caller writing `Client` gets open-tacenta's
55// provider. Nothing else in this file names a provider's concrete types, which
56// is what makes it a client of the seam rather than of any one provider.
57use tacenta_core::crypto::{Address, CryptoProvider, DefaultProvider};
58use tacenta_core::persist::{SealError, seal, unseal};
59use tacenta_relay::{Request, Response, decode_response, encode_request};
60use tacenta_transport::{Connection, DirConnection};
61use tacenta_wire::{Envelope, Kind};
62
63mod secure_store;
64pub use secure_store::{SecureStore, SecureStoreError};
65mod dial;
66use dial::Dialer;
67pub use dial::{ByteStream, Connecting, Connector};
68mod tenant;
69pub use tacenta_discovery::{ServiceDocument, Tls, WELL_KNOWN_PATH};
70pub use tenant::{Endpoints, Tacenta};
71
72pub use tacenta_accounts::{AccountResponse, SignupReason};
73pub use tacenta_directory::DirResponse;
74pub use tacenta_relay::DeviceAddr;
75pub use tacenta_transport::{ClientTls, ProvisionOutcome};
76
77/// Where to reach a Tacenta server, and who to connect as.
78#[derive(Clone, Debug)]
79pub struct Config {
80    /// The directory service address.
81    pub directory: SocketAddr,
82    /// The relay server address.
83    pub relay: SocketAddr,
84    /// This client's user identifier.
85    pub user: String,
86    /// This client's device number.
87    pub device: u8,
88}
89
90/// Where to reach a server's account endpoints, plus the credentials to sign
91/// in with. Used by the account flow ([`Client::sign_in`]) — sign in, then
92/// provision this device under the account's handle — in contrast to
93/// [`Config`], which registers a device directly under a raw handle (the
94/// pre-account path).
95#[derive(Clone)]
96pub struct AccountConfig {
97    /// The directory service address (peer lookups).
98    pub directory: SocketAddr,
99    /// The relay server address.
100    pub relay: SocketAddr,
101    /// The account service address (sign in).
102    pub accounts: SocketAddr,
103    /// The provisioning service address (bind this device).
104    pub provisioning: SocketAddr,
105    /// The tenant API key that scopes the account operations.
106    pub api_key: String,
107    /// The user's username (their per-tenant handle).
108    pub identifier: String,
109    /// The user's password.
110    pub password: String,
111    /// This client's device number.
112    pub device: u8,
113}
114
115/// Redacts the two secrets: a config in a log line or a crash breadcrumb
116/// must not carry the password or the API key.
117impl std::fmt::Debug for AccountConfig {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        f.debug_struct("AccountConfig")
120            .field("directory", &self.directory)
121            .field("relay", &self.relay)
122            .field("accounts", &self.accounts)
123            .field("provisioning", &self.provisioning)
124            .field("api_key", &"<redacted>")
125            .field("identifier", &self.identifier)
126            .field("password", &"<redacted>")
127            .field("device", &self.device)
128            .finish()
129    }
130}
131
132/// The most bytes one `send` accepts: the relay's per-message envelope
133/// limit less room for the envelope's own header and the ciphertext's
134/// overhead. Larger is refused as [`ErrorKind::InvalidArgument`] before any
135/// ratchet step, on every head.
136pub const MAX_MESSAGE_BYTES: usize = tacenta_relay::MAX_ENVELOPE_BYTES - 1024;
137
138/// The signal a client pings when mail may be waiting; see
139/// [`Client::mail`]. Cheap to clone and hold apart from the client.
140#[derive(Clone)]
141pub struct MailSignal(Arc<tokio::sync::Notify>);
142
143impl MailSignal {
144    /// Wait until the relay has pushed since the last wait (or one permit
145    /// was stored by a push that landed earlier), or a connection ended.
146    pub async fn wait(&self) {
147        self.0.notified().await;
148    }
149}
150
151/// What the last sign-in or connect found, read with
152/// [`restore_outcome`](Client::restore_outcome): whether the sessions a
153/// restored state carried are in use, or were discarded because the state
154/// was older than one already seen. The same three values on every head.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156#[non_exhaustive]
157pub enum RestoreOutcome {
158    /// No sessions were restored: a fresh identity, or one restored from
159    /// an identity alone.
160    Fresh,
161    /// The state's sessions resumed as they were.
162    Resumed,
163    /// The state was older than one already seen (a rollback, caught by
164    /// the secure store's counter or by the directory), so its sessions
165    /// were discarded and the identity kept: conversations re-establish on
166    /// next contact. Show it to the user; keep the blob.
167    SessionsDiscarded,
168}
169
170impl RestoreOutcome {
171    /// The outcome's name as the heads that carry a string spell it:
172    /// `"fresh"`, `"resumed"`, `"sessionsDiscarded"`.
173    pub fn as_str(self) -> &'static str {
174        match self {
175            RestoreOutcome::Fresh => "fresh",
176            RestoreOutcome::Resumed => "resumed",
177            RestoreOutcome::SessionsDiscarded => "sessionsDiscarded",
178        }
179    }
180}
181
182/// A decrypted inbound message and the device that sent it.
183#[derive(Clone, Debug)]
184pub struct Received {
185    pub from: DeviceAddr,
186    pub plaintext: Vec<u8>,
187}
188
189/// A resolved contact: another user's addressable handle. Produced by
190/// [`Client::find`].
191#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct Contact {
193    /// The addressable device — the handle (e.g. `acme/bob`) and device number.
194    pub address: DeviceAddr,
195}
196
197impl Contact {
198    /// The contact's handle, e.g. `acme/bob`.
199    pub fn handle(&self) -> &str {
200        &self.address.user
201    }
202}
203
204/// A client-local contact list. It lives on the device and is **never sent to
205/// the server**, so the server never learns a user's contact graph — the
206/// metadata-privacy choice for an end-to-end-encrypted messenger. Serialize
207/// with [`to_bytes`](Contacts::to_bytes) to persist it across restarts; a
208/// user's contacts do not sync across their devices without doing that
209/// deliberately.
210#[derive(Clone, Debug, Default)]
211pub struct Contacts {
212    entries: Vec<Contact>,
213}
214
215impl Contacts {
216    /// An empty contact list.
217    pub fn new() -> Contacts {
218        Contacts::default()
219    }
220
221    /// Add a contact, ignoring a duplicate (by address).
222    pub fn add(&mut self, contact: Contact) {
223        if !self.entries.iter().any(|c| c.address == contact.address) {
224            self.entries.push(contact);
225        }
226    }
227
228    /// Remove every contact with this handle.
229    pub fn remove(&mut self, handle: &str) {
230        self.entries.retain(|c| c.address.user != handle);
231    }
232
233    /// The contact with this handle, if any.
234    pub fn get(&self, handle: &str) -> Option<&Contact> {
235        self.entries.iter().find(|c| c.address.user == handle)
236    }
237
238    /// Every contact.
239    pub fn all(&self) -> &[Contact] {
240        &self.entries
241    }
242
243    /// Serialize the list to bytes, to persist on the device.
244    pub fn to_bytes(&self) -> Vec<u8> {
245        let mut out = Vec::new();
246        out.extend_from_slice(&(self.entries.len() as u32).to_be_bytes());
247        for contact in &self.entries {
248            let handle = contact.address.user.as_bytes();
249            out.extend_from_slice(&(handle.len() as u32).to_be_bytes());
250            out.extend_from_slice(handle);
251            out.extend_from_slice(&contact.address.device.to_be_bytes());
252        }
253        out
254    }
255
256    /// Reconstruct a list from [`to_bytes`](Contacts::to_bytes); `None` on any
257    /// malformation.
258    pub fn from_bytes(bytes: &[u8]) -> Option<Contacts> {
259        let mut rest = bytes;
260        let count = take_u32(&mut rest)?;
261        // Never sized from the bytes: a rewritten count would ask for the
262        // world before a byte was checked.
263        let mut entries = Vec::new();
264        for _ in 0..count {
265            let len = take_u32(&mut rest)? as usize;
266            let (handle, r) = rest.split_at_checked(len)?;
267            rest = r;
268            let user = String::from_utf8(handle.to_vec()).ok()?;
269            let device = take_u32(&mut rest)?;
270            entries.push(Contact {
271                address: DeviceAddr::new(user, device),
272            });
273        }
274        rest.is_empty().then_some(Contacts { entries })
275    }
276}
277
278/// Read a big-endian u32 from the front of `bytes`, advancing it.
279fn take_u32(bytes: &mut &[u8]) -> Option<u32> {
280    let (head, rest) = bytes.split_at_checked(4)?;
281    *bytes = rest;
282    Some(u32::from_be_bytes(head.try_into().ok()?))
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    fn contact(handle: &str, device: u32) -> Contact {
290        Contact {
291            address: DeviceAddr::new(handle, device),
292        }
293    }
294
295    #[test]
296    fn contacts_dedup_remove_and_round_trip() {
297        let mut contacts = Contacts::new();
298        contacts.add(contact("acme/bob", 1));
299        contacts.add(contact("acme/bob", 1)); // duplicate: ignored
300        contacts.add(contact("acme/carol", 2));
301        assert_eq!(contacts.all().len(), 2);
302        assert_eq!(contacts.get("acme/bob"), Some(&contact("acme/bob", 1)));
303        assert_eq!(contacts.get("acme/nobody"), None);
304
305        // Round-trip through the serialized form.
306        let restored = Contacts::from_bytes(&contacts.to_bytes()).unwrap();
307        assert_eq!(restored.all(), contacts.all());
308
309        contacts.remove("acme/bob");
310        assert_eq!(contacts.all().len(), 1);
311        assert!(contacts.get("acme/bob").is_none());
312    }
313
314    /// The session-state split parser is hand-rolled and reached with attacker-
315    /// influenced bytes on restore; no byte string may panic it. `split_state`
316    /// is private, so this lives in the crate rather than the fuzz integration
317    /// test.
318    #[test]
319    fn a_blob_without_a_provider_restores_as_legacy() {
320        // What every blob written before the provider was recorded is, because
321        // nothing else could have written one.
322        let identity = [0xaa_u8; 36];
323        let sessions = [0xbb_u8; 8];
324        let mut blob = (identity.len() as u32).to_be_bytes().to_vec();
325        blob.extend_from_slice(&identity);
326        blob.extend_from_slice(&sessions);
327
328        let split = split_state(&blob).expect("legacy blob parses");
329        assert_eq!(split.provider, SessionProvider::Legacy);
330        assert_eq!(split.identity, identity);
331        assert_eq!(split.sessions, sessions);
332        assert_eq!(split.prekeys, None, "a legacy blob carries no prekeys");
333    }
334
335    #[test]
336    fn a_tagged_blob_round_trips_either_provider() {
337        for provider in [SessionProvider::Legacy, SessionProvider::OpenTacenta] {
338            let identity = [0xcc_u8; 36];
339            let sessions = [0xdd_u8; 4];
340            let mut blob = vec![STATE_TAGGED, STATE_VERSION_2, provider.to_byte()];
341            blob.extend_from_slice(&(identity.len() as u32).to_be_bytes());
342            blob.extend_from_slice(&identity);
343            blob.extend_from_slice(&sessions);
344
345            let split = split_state(&blob).expect("tagged blob parses");
346            assert_eq!(split.provider, provider);
347            assert_eq!(split.identity, identity);
348            assert_eq!(split.sessions, sessions);
349            assert_eq!(split.prekeys, None, "a v2 blob carries no prekeys");
350        }
351    }
352
353    #[test]
354    fn a_v3_blob_carries_a_framed_sessions_and_a_prekeys_section() {
355        let identity = [0xce_u8; 36];
356        let sessions = [0xdf_u8; 5];
357        let prekeys = [0xa1_u8; 9];
358        let mut blob = vec![
359            STATE_TAGGED,
360            STATE_VERSION_3,
361            SessionProvider::OpenTacenta.to_byte(),
362        ];
363        put_lp(&mut blob, &identity);
364        put_lp(&mut blob, &sessions);
365        put_lp(&mut blob, &prekeys);
366
367        let split = split_state(&blob).expect("v3 blob parses");
368        assert_eq!(split.provider, SessionProvider::OpenTacenta);
369        assert_eq!(split.identity, identity);
370        assert_eq!(split.sessions, sessions);
371        assert_eq!(
372            split.prekeys,
373            Some(&prekeys[..]),
374            "v3 carries the prekeys section"
375        );
376
377        // Trailing bytes after the prekeys section are refused, so a v3 blob is
378        // exactly its three framed parts and nothing smuggled after them.
379        blob.push(0x00);
380        assert!(split_state(&blob).is_err(), "trailing bytes are refused");
381    }
382
383    #[test]
384    fn the_two_shapes_cannot_be_confused() {
385        // A legacy blob starts with the high byte of a u32 identity length. An
386        // identity is a registration id and a serialized key, so that length is
387        // far below 2^24 and the byte is zero. The tag is 0xff, so no legacy
388        // blob can be read as tagged and no tagged blob as legacy.
389        assert_ne!(STATE_TAGGED, 0x00);
390        let identity = [0x11_u8; 36];
391        let mut legacy = (identity.len() as u32).to_be_bytes().to_vec();
392        legacy.extend_from_slice(&identity);
393        assert_eq!(legacy[0], 0x00);
394    }
395
396    #[test]
397    fn an_unknown_version_or_provider_is_refused() {
398        let body = {
399            let identity = [0x22_u8; 36];
400            let mut b = (identity.len() as u32).to_be_bytes().to_vec();
401            b.extend_from_slice(&identity);
402            b
403        };
404        let mut bad_version = vec![STATE_TAGGED, 0x09, SessionProvider::Legacy.to_byte()];
405        bad_version.extend_from_slice(&body);
406        assert!(split_state(&bad_version).is_err());
407
408        let mut bad_provider = vec![STATE_TAGGED, STATE_VERSION_2, 0x7f];
409        bad_provider.extend_from_slice(&body);
410        assert!(split_state(&bad_provider).is_err());
411    }
412
413    #[test]
414    fn split_state_is_panic_free() {
415        let _ = split_state(&[]);
416        for a in 0u16..=255 {
417            let _ = split_state(&[a as u8]);
418            for b in 0u16..=255 {
419                let _ = split_state(&[a as u8, b as u8]);
420            }
421        }
422        let mut seed = 0x9E37_79B9_7F4A_7C15u64;
423        let mut next = || {
424            seed = seed
425                .wrapping_mul(6364136223846793005)
426                .wrapping_add(1442695040888963407);
427            (seed >> 33) as u8
428        };
429        for _ in 0..50_000 {
430            let len = (next() as usize) % 128;
431            let input: Vec<u8> = (0..len).map(|_| next()).collect();
432            let _ = split_state(&input);
433        }
434    }
435
436    struct FixedKeyStore {
437        key: [u8; 32],
438        counter: std::sync::atomic::AtomicU64,
439    }
440    impl FixedKeyStore {
441        fn new(key: [u8; 32]) -> Self {
442            Self {
443                key,
444                counter: std::sync::atomic::AtomicU64::new(0),
445            }
446        }
447    }
448    impl SecureStore for FixedKeyStore {
449        fn wrap_key(&self) -> std::result::Result<[u8; 32], SecureStoreError> {
450            Ok(self.key)
451        }
452        fn rollback_counter(&self) -> std::result::Result<u64, SecureStoreError> {
453            Ok(self.counter.load(std::sync::atomic::Ordering::SeqCst))
454        }
455        fn bump_rollback_counter(&self) -> std::result::Result<u64, SecureStoreError> {
456            Ok(self
457                .counter
458                .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
459                + 1)
460        }
461    }
462
463    /// The sealed (v5) and unsealed (v4) formats do not cross-parse, which is
464    /// what makes a downgrade attack a non-starter: an attacker cannot strip the
465    /// seal off a v5 blob and feed the remainder to the unsealed parser as a
466    /// forged-generation v4 blob, nor route a v4 blob through the sealed path.
467    #[test]
468    fn sealed_and_unsealed_formats_do_not_cross_parse() {
469        let store = FixedKeyStore::new([0x5a; 32]);
470        let key = store.wrap_key().unwrap();
471
472        // A minimal v3 body to seal.
473        let body = {
474            let mut b = vec![
475                STATE_TAGGED,
476                STATE_VERSION_3,
477                SessionProvider::Legacy.to_byte(),
478            ];
479            put_lp(&mut b, &[0x11; 36]); // identity
480            put_lp(&mut b, &[]); // sessions
481            put_lp(&mut b, &[]); // prekeys
482            b
483        };
484        // The sealed body is the v3 body followed by the 8-byte rollback counter.
485        let mut inner = body.clone();
486        inner.extend_from_slice(&0u64.to_be_bytes());
487        let mut sealed = vec![STATE_TAGGED, STATE_VERSION_5];
488        sealed.extend_from_slice(&seal(&key, 7, &inner));
489
490        // The sealed path opens it, recovers the authenticated generation, and
491        // strips the counter back off to hand the v3 body to the parser.
492        let opened = open_sealed(&store, &sealed).unwrap();
493        assert_eq!(opened.generation, 7);
494        assert_eq!(opened.body, body);
495        assert!(!opened.stale, "counter 0 against a store at 0 is fresh");
496        assert!(opened.bound.is_none(), "a v5 blob binds no address");
497
498        // A v6 blob leads with the address it was sealed for, and refuses
499        // another (BR-17); the body and counter follow as in v5.
500        let mut bound = Vec::new();
501        put_lp(&mut bound, b"acme/bob");
502        bound.extend_from_slice(&1u32.to_be_bytes());
503        bound.extend_from_slice(&inner);
504        let mut sealed6 = vec![STATE_TAGGED, STATE_VERSION_6];
505        sealed6.extend_from_slice(&seal(&key, 7, &bound));
506        let opened = open_sealed(&store, &sealed6).unwrap();
507        assert_eq!(opened.body, body);
508        assert_eq!(opened.bound, Some(DeviceAddr::new("acme/bob", 1)));
509        assert!(
510            opened
511                .expect_address(&DeviceAddr::new("acme/bob", 1))
512                .is_ok()
513        );
514        assert!(opened.expect_user("bob", 1).is_ok());
515        assert!(matches!(
516            opened.expect_address(&DeviceAddr::new("acme/carol", 1)),
517            Err(Error::StateMismatch { .. })
518        ));
519        assert!(matches!(
520            opened.expect_user("bob", 2),
521            Err(Error::StateMismatch { .. })
522        ));
523        assert!(matches!(
524            opened.expect_user("carol", 1),
525            Err(Error::StateMismatch { .. })
526        ));
527
528        // The *unsealed* parser refuses a v5 blob rather than misreading it.
529        assert!(
530            split_state(&sealed).is_err(),
531            "v5 must not parse as unsealed"
532        );
533
534        // And the sealed path refuses a v4 (unsealed) blob.
535        let mut v4 = body.clone();
536        v4[1] = STATE_VERSION_4;
537        v4.extend_from_slice(&7u64.to_be_bytes());
538        assert!(
539            open_sealed(&store, &v4).is_err(),
540            "v4 must not open as sealed"
541        );
542    }
543
544    /// The authenticated generation cannot be forged: altering it in the sealed
545    /// bytes makes the restore fail closed (the crate-level counterpart of the
546    /// end-to-end `a_forged_sealed_generation_is_refused`).
547    #[test]
548    fn a_forged_generation_in_a_sealed_blob_is_refused() {
549        let store = FixedKeyStore::new([0x5a; 32]);
550        let key = store.wrap_key().unwrap();
551        let body = vec![
552            STATE_TAGGED,
553            STATE_VERSION_3,
554            SessionProvider::Legacy.to_byte(),
555        ];
556        let mut sealed = vec![STATE_TAGGED, STATE_VERSION_5];
557        sealed.extend_from_slice(&seal(&key, 1, &body));
558
559        // Forge the generation (bytes 3..11: after TAGGED, V5, SEAL_VERSION).
560        sealed[3..11].copy_from_slice(&u64::MAX.to_be_bytes());
561        assert!(matches!(
562            open_sealed(&store, &sealed),
563            Err(Error::SecureStore(_))
564        ));
565    }
566}
567
568/// What can go wrong talking to a Tacenta server. [`Error::kind`] is the
569/// contract an app branches on; the variants carry the detail, including
570/// the server's own reply where there was one, and may grow (the enum is
571/// non-exhaustive).
572#[derive(Debug)]
573#[non_exhaustive]
574pub enum Error {
575    /// A network or transport error.
576    Io(std::io::Error),
577    /// The relay refused a send, returning this outcome.
578    Relay(RelayRefusal),
579    /// The caller's own input was wrong: a device number the protocol cannot
580    /// carry, a message over the size limit, a call the client's shape does
581    /// not support. Fix the call, not the network.
582    InvalidArgument(&'static str),
583    /// The directory refused an operation, returning this outcome.
584    Directory(DirResponse),
585    /// The account service refused an operation (a refused signup or
586    /// sign-in), returning this outcome.
587    Account(AccountResponse),
588    /// Provisioning was refused (bad session, failed possession, or the
589    /// handle is bound to a different key), returning this outcome.
590    Provision(ProvisionOutcome),
591    /// A cryptographic operation failed.
592    Crypto(String),
593    /// The server sent an unexpected or malformed response.
594    Protocol(&'static str),
595    /// The persisted-state authenticator was refused, or the secure-storage key
596    /// behind it was unavailable (decision 0078, anchor B). A refused
597    /// authenticator on a restore means the state file was altered — including
598    /// the deliberate forgery EX-03 is about, which is now caught rather than
599    /// resumed.
600    SecureStore(String),
601    /// The platform's secure store could not be reached: a Keychain before
602    /// first unlock, a Keystore that needs the user. Nothing was refused;
603    /// retry after unlock, and keep the blob.
604    StoreUnavailable(String),
605    /// A sealed state that belongs to another user or device was offered
606    /// for this one, and refused before its identity could be registered
607    /// under an address it was never bound to.
608    StateMismatch {
609        /// The address the state was sealed for.
610        state: DeviceAddr,
611        /// The address it was offered to.
612        client: DeviceAddr,
613    },
614    /// The server's service document could not be fetched or read, so the
615    /// tenant handle does not know where the services are (decision 0090).
616    Discovery(String),
617}
618
619/// A secure store's failure as the client reports it: a store that is
620/// not reachable is its own kind, so an app can wait for the unlock
621/// rather than treat it as tampering.
622fn store_err(e: SecureStoreError) -> Error {
623    match e {
624        SecureStoreError::Unavailable(reason) => Error::StoreUnavailable(reason),
625        SecureStoreError::Backend(reason) => Error::SecureStore(reason),
626    }
627}
628
629/// Why the relay refused a send.
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631pub enum RelayRefusal {
632    /// The message is over the relay's per-message size limit. Permanent:
633    /// a smaller message, not a retry.
634    TooLarge,
635    /// The recipient's queue is at its count or byte budget. Transient:
636    /// retry after it drains.
637    QueueFull,
638    /// The recipient's device is not registered.
639    UnknownRecipient,
640}
641
642/// What an app can branch on: the kind of an [`Error`], the same set on
643/// every head (decision 0090, the list before the packages). The message
644/// carries the detail; the kind is the contract. Non-exhaustive: a kind may
645/// be added, so match with a wildcard arm.
646#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
647#[non_exhaustive]
648pub enum ErrorKind {
649    /// The network or the transport failed, including a reconnect that ran
650    /// out of patience. Retry later.
651    Network,
652    /// The server's service document could not be fetched or read, so the
653    /// handle does not know where the services are.
654    Discovery,
655    /// The API key selects no tenant.
656    UnknownTenant,
657    /// The username is already taken in this tenant.
658    UsernameTaken,
659    /// The username is not one the server accepts.
660    InvalidUsername,
661    /// The password is too weak.
662    WeakPassword,
663    /// A sign-up was refused for another reason: registration is closed,
664    /// the handle is reserved, or (for a tenant) the email is taken or
665    /// invalid.
666    SignUpRefused,
667    /// The credentials were refused, or the session they opened has expired.
668    /// Coarse by design: it does not say whether the account exists.
669    SignInRefused,
670    /// The address is bound to a different device identity than the one
671    /// presented (trust on first use). Resume from the saved state that
672    /// holds the bound identity, or use another device number.
673    IdentityMismatch,
674    /// The address is not registered: the recipient's device is gone, or
675    /// never was.
676    NotFound,
677    /// The server asked for a slower pace: too many failed sign-ins, or the
678    /// recipient's queue is full. Back off and retry.
679    RateLimited,
680    /// The server could not process the request; nothing was applied. Retry.
681    ServerFailure,
682    /// The persisted state was refused: altered, older than the last send,
683    /// or its authenticator did not verify.
684    State,
685    /// The platform's secure store could not be reached (a Keychain before
686    /// first unlock, a Keystore that needs the user). Retry after unlock;
687    /// keep the blob.
688    StoreUnavailable,
689    /// The caller's own input was wrong: a malformed address, a message
690    /// over the size limit. Fix the call, not the network.
691    InvalidArgument,
692    /// A protocol or cryptographic failure, or an unexpected server reply:
693    /// a bug on one side or the other, worth reporting.
694    Internal,
695}
696
697impl ErrorKind {
698    /// The kind's name as the heads that carry a string spell it (the
699    /// TypeScript `kind`, and the manifest's `typescript` column):
700    /// `"network"`, `"signInRefused"`, ...
701    pub fn as_str(self) -> &'static str {
702        match self {
703            ErrorKind::Network => "network",
704            ErrorKind::Discovery => "discovery",
705            ErrorKind::UnknownTenant => "unknownTenant",
706            ErrorKind::UsernameTaken => "usernameTaken",
707            ErrorKind::InvalidUsername => "invalidUsername",
708            ErrorKind::WeakPassword => "weakPassword",
709            ErrorKind::SignUpRefused => "signUpRefused",
710            ErrorKind::SignInRefused => "signInRefused",
711            ErrorKind::IdentityMismatch => "identityMismatch",
712            ErrorKind::NotFound => "notFound",
713            ErrorKind::RateLimited => "rateLimited",
714            ErrorKind::ServerFailure => "serverFailure",
715            ErrorKind::State => "state",
716            ErrorKind::StoreUnavailable => "storeUnavailable",
717            ErrorKind::InvalidArgument => "invalidArgument",
718            ErrorKind::Internal => "internal",
719        }
720    }
721}
722
723impl Error {
724    /// The kind of this error, for an app to branch on.
725    pub fn kind(&self) -> ErrorKind {
726        match self {
727            // The relay refused the identity itself (rotated away, or never
728            // bound): not the network's fault, and not fixed by retrying.
729            Error::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
730                ErrorKind::IdentityMismatch
731            }
732            Error::Io(_) => ErrorKind::Network,
733            Error::InvalidArgument(_) => ErrorKind::InvalidArgument,
734            Error::Discovery(_) => ErrorKind::Discovery,
735            Error::Relay(RelayRefusal::TooLarge) => ErrorKind::InvalidArgument,
736            Error::Relay(RelayRefusal::QueueFull) => ErrorKind::RateLimited,
737            Error::Relay(RelayRefusal::UnknownRecipient) => ErrorKind::NotFound,
738            Error::Account(response) => match response {
739                AccountResponse::UnknownTenant => ErrorKind::UnknownTenant,
740                AccountResponse::SignupRefused { reason } => match reason {
741                    SignupReason::UsernameTaken => ErrorKind::UsernameTaken,
742                    SignupReason::InvalidUsername => ErrorKind::InvalidUsername,
743                    SignupReason::WeakPassword => ErrorKind::WeakPassword,
744                    SignupReason::EmailTaken | SignupReason::InvalidEmail => {
745                        ErrorKind::SignUpRefused
746                    }
747                },
748                AccountResponse::SignInRefused => ErrorKind::SignInRefused,
749                AccountResponse::RateLimited => ErrorKind::RateLimited,
750                AccountResponse::ServerError => ErrorKind::ServerFailure,
751                _ => ErrorKind::Internal,
752            },
753            Error::Directory(response) => match response {
754                DirResponse::Rejected
755                | DirResponse::DepositRejected
756                | DirResponse::Unauthorized => ErrorKind::IdentityMismatch,
757                DirResponse::NotFound | DirResponse::Unregistered => ErrorKind::NotFound,
758                DirResponse::RateLimited => ErrorKind::RateLimited,
759                DirResponse::RegistrationClosed | DirResponse::ReservedHandle => {
760                    ErrorKind::SignUpRefused
761                }
762                DirResponse::RolledBack => ErrorKind::State,
763                _ => ErrorKind::Internal,
764            },
765            Error::Provision(outcome) => match outcome {
766                ProvisionOutcome::Rejected => ErrorKind::IdentityMismatch,
767                ProvisionOutcome::BadSession => ErrorKind::SignInRefused,
768                ProvisionOutcome::ServerError => ErrorKind::ServerFailure,
769                _ => ErrorKind::Internal,
770            },
771            Error::SecureStore(_) => ErrorKind::State,
772            Error::StoreUnavailable(_) => ErrorKind::StoreUnavailable,
773            Error::StateMismatch { .. } => ErrorKind::IdentityMismatch,
774            Error::Crypto(_) | Error::Protocol(_) => ErrorKind::Internal,
775        }
776    }
777}
778
779impl std::fmt::Display for Error {
780    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781        match self {
782            Error::Io(e) => write!(f, "transport error: {e}"),
783            Error::InvalidArgument(m) => write!(f, "invalid argument: {m}"),
784            Error::Relay(RelayRefusal::TooLarge) => {
785                write!(f, "message exceeds the relay's per-message size limit")
786            }
787            Error::Relay(RelayRefusal::QueueFull) => {
788                write!(f, "recipient's queue is full; retry after it drains")
789            }
790            Error::Relay(RelayRefusal::UnknownRecipient) => {
791                write!(f, "recipient is not registered")
792            }
793            Error::Directory(o) => write!(f, "directory refused the request: {o:?}"),
794            Error::Account(o) => write!(f, "account service refused the request: {o:?}"),
795            Error::Provision(o) => write!(f, "provisioning refused: {o:?}"),
796            Error::Crypto(e) => write!(f, "crypto error: {e}"),
797            Error::Protocol(m) => write!(f, "protocol error: {m}"),
798            Error::SecureStore(e) => write!(f, "persisted-state authentication error: {e}"),
799            Error::StoreUnavailable(e) => write!(f, "secure storage is not available: {e}"),
800            Error::StateMismatch { state, client } => write!(
801                f,
802                "the sealed state belongs to {}/{}, not {}/{}",
803                state.user, state.device, client.user, client.device
804            ),
805            Error::Discovery(e) => write!(f, "service discovery failed: {e}"),
806        }
807    }
808}
809
810impl std::error::Error for Error {}
811
812impl From<std::io::Error> for Error {
813    fn from(e: std::io::Error) -> Error {
814        Error::Io(e)
815    }
816}
817
818/// A convenience alias for results from this crate.
819pub type Result<T> = std::result::Result<T, Error>;
820
821fn crypto(e: impl std::fmt::Debug) -> Error {
822    Error::Crypto(format!("{e:?}"))
823}
824
825/// The crypto-layer address for a routing address (device numbers are u8 at
826/// the crypto layer; the relay carries them as u32).
827///
828/// `Address` rather than libsignal's `ProtocolAddress`: this client is written
829/// against the provider seam, so it must not name either provider's types.
830fn peer_address(addr: &DeviceAddr) -> Result<Address> {
831    let device =
832        u8::try_from(addr.device).map_err(|_| Error::InvalidArgument("device id out of range"))?;
833    Ok(Address::new(&addr.user, device))
834}
835
836/// The routing address for a crypto-layer peer address (the inverse of
837/// [`peer_address`]).
838fn device_addr(peer: &Address) -> DeviceAddr {
839    DeviceAddr::new(peer.user.clone(), u32::from(peer.device))
840}
841
842// There was a `const THIS_PROVIDER: SessionProvider` here, described as "a
843// constant rather than a choice, because a client compiles
844// against one provider", and it said it would become a runtime value at step 4
845// of decision 0056.
846//
847// That step could not have arrived while it was a constant. The provider a
848// session was established under is a property of `P`, and a constant would have
849// written the wrong tag into a blob held by a client running the other one:
850// exactly the wrong answer, in the field whose only job is to say which
851// provider's session a blob holds. It is `SessionProvider::of::<P>()` now.
852
853/// Which implementation established the sessions in a state blob.
854///
855/// A session cannot be carried between them: the two derive keys under
856/// different labels, so one provider's session is unreadable by the other. A
857/// client restoring state has to know which it is holding before it can decide
858/// whether to continue the session or drop it and establish again. That is what
859/// this records, and it is step 2 of the migration in decision record 0056.
860///
861/// It lives in the state envelope rather than inside a provider's own session
862/// blob, because the question it answers is which blob to hand to which
863/// provider.
864#[derive(Clone, Copy, PartialEq, Eq, Debug)]
865pub enum SessionProvider {
866    /// The provider used before this field existed; the default a pre-tag blob
867    /// restores as.
868    Legacy,
869    /// open-tacenta, behind the `open-provider` feature.
870    OpenTacenta,
871}
872
873impl SessionProvider {
874    /// The byte this provider is written as in the state envelope.
875    pub fn to_byte(self) -> u8 {
876        match self {
877            SessionProvider::Legacy => 0x01,
878            SessionProvider::OpenTacenta => 0x02,
879        }
880    }
881
882    /// Which provider `P` is.
883    ///
884    /// Keyed off `CryptoProvider::NAME`, which is the only thing the seam
885    /// exposes about a provider's identity. An unrecognised name is a provider
886    /// this envelope has no byte for, and saying so is better than picking one:
887    /// a wrong tag here makes a client continue a session it cannot read.
888    fn of<P: CryptoProvider>() -> SessionProvider {
889        match P::NAME {
890            "open-tacenta" => SessionProvider::OpenTacenta,
891            other => panic!("no state-envelope tag for crypto provider {other:?}"),
892        }
893    }
894
895    fn from_byte(b: u8) -> Option<SessionProvider> {
896        match b {
897            0x01 => Some(SessionProvider::Legacy),
898            0x02 => Some(SessionProvider::OpenTacenta),
899            _ => None,
900        }
901    }
902}
903
904/// Marks a state blob that carries a version and a provider.
905///
906/// A blob written before this existed begins with the high byte of a `u32`
907/// identity length. An identity is a registration id and a serialized key, so
908/// that length is far below 2^24 and the byte is always zero. `0xff` therefore
909/// cannot be the start of an older blob, which is what makes the two
910/// distinguishable without guessing.
911const STATE_TAGGED: u8 = 0xff;
912
913/// The v2 version byte: identity and sessions, no prekeys. Written until
914/// 2026-08-14; still read.
915const STATE_VERSION_2: u8 = 0x02;
916
917/// The v3 version byte: identity, sessions **and the published prekey store**,
918/// so a restored device can decrypt a first-contact message a new peer sent to
919/// a published one-time prekey while it was offline. v2's tail-encoded sessions
920/// cannot carry a section after them, so v3 length-frames the sessions and
921/// appends a prekeys section.
922const STATE_VERSION_3: u8 = 0x03;
923
924/// The v4 version byte: v3 plus a trailing `u64` **persisted-state generation**,
925/// the anti-rollback counter of decision 0078. Carried so a restore knows which
926/// generation to present to the directory; a directory that has since witnessed
927/// a higher one reports the restore as a rollback.
928const STATE_VERSION_4: u8 = 0x04;
929
930/// The v5 marker: a **sealed** state (decision 0078, anchor B). The two bytes
931/// `STATE_TAGGED, STATE_VERSION_5` are followed by a `tacenta_core::persist`
932/// seal whose authenticated payload is a v3 body and whose bound counter is the
933/// persisted-state generation. Unlike v4 — which appends the generation as an
934/// unauthenticated plaintext `u64` a file-rewriter can forge (internal audit
935/// AR-1) — the generation here lives *inside* the authenticator, so it cannot be
936/// forged without the secure-storage key. This is the format the sealed
937/// export/restore pair uses — the rollback-resistant one, which refuses a forged
938/// state and, via the per-send `SecureStore` counter, any state older than the
939/// latest send (closing EX-03 against a file-rewriter given a rollback-resistant
940/// store, 0078); v4 remains for the unsealed path, which makes no rollback claim.
941const STATE_VERSION_5: u8 = 0x05;
942
943/// The v6 marker: v5 with **the address the state was sealed for** under
944/// the authenticator, ahead of the body: `lp(user) || device (u32 BE) ||
945/// v3 body || counter`. A restore for another user or device is refused
946/// before that identity is registered under the wrong address, where
947/// trust-on-first-use would have made it permanent (boundary review
948/// BR-17). Written by every sealed export; v5 is still read, unbound.
949const STATE_VERSION_6: u8 = 0x06;
950
951/// The parts of an [`export_state`](Client::export_state) blob. `prekeys` is
952/// `None` for a v2 or legacy blob that never carried them; `Some` (possibly
953/// empty) for v3 and v4. `generation` is 0 for anything before v4.
954struct SplitState<'a> {
955    provider: SessionProvider,
956    identity: &'a [u8],
957    sessions: &'a [u8],
958    prekeys: Option<&'a [u8]>,
959    generation: u64,
960}
961
962/// A big-endian `u32` length prefix, then the block.
963fn put_lp(out: &mut Vec<u8>, block: &[u8]) {
964    out.extend_from_slice(&(block.len() as u32).to_be_bytes());
965    out.extend_from_slice(block);
966}
967
968/// Read a `u32`-length-prefixed block, returning it and the rest. `None` on any
969/// truncation — this runs over untrusted bytes and must not panic.
970fn take_lp(b: &[u8]) -> Option<(&[u8], &[u8])> {
971    let (len, rest) = b.split_at_checked(4)?;
972    let len = u32::from_be_bytes(len.try_into().ok()?) as usize;
973    rest.split_at_checked(len)
974}
975
976/// Split an [`export_state`](Client::export_state) blob into its parts.
977///
978/// Three shapes are accepted, and accepting the older two is the point — a new
979/// client must restore a state an older one wrote. A legacy blob (no tag)
980/// restores as [`SessionProvider::Legacy`], which is what it is: nothing else
981/// could have written one. A v3 blob written by a newer client cannot be read
982/// here only if this *is* the newer client; an *older* client meeting a v3 blob
983/// fails closed with "unknown state blob version" rather than restoring it
984/// wrong.
985///
986/// Panic-free over arbitrary bytes: every field is bounds-checked, never
987/// indexed.
988fn split_state(state: &[u8]) -> Result<SplitState<'_>> {
989    let trunc = || Error::Protocol("state blob truncated");
990    match state.split_first() {
991        Some((&STATE_TAGGED, rest)) => {
992            let (&version, rest) = rest.split_first().ok_or_else(trunc)?;
993            let (&tag, rest) = rest.split_first().ok_or_else(trunc)?;
994            let provider = SessionProvider::from_byte(tag)
995                .ok_or(Error::Protocol("unknown session provider"))?;
996            match version {
997                STATE_VERSION_2 => {
998                    let (identity, sessions) = take_lp(rest).ok_or_else(trunc)?;
999                    Ok(SplitState {
1000                        provider,
1001                        identity,
1002                        sessions,
1003                        prekeys: None,
1004                        generation: 0,
1005                    })
1006                }
1007                STATE_VERSION_3 => {
1008                    let (identity, rest) = take_lp(rest).ok_or_else(trunc)?;
1009                    let (sessions, rest) = take_lp(rest).ok_or_else(trunc)?;
1010                    let (prekeys, rest) = take_lp(rest).ok_or_else(trunc)?;
1011                    if !rest.is_empty() {
1012                        return Err(Error::Protocol("state blob has trailing bytes"));
1013                    }
1014                    Ok(SplitState {
1015                        provider,
1016                        identity,
1017                        sessions,
1018                        prekeys: Some(prekeys),
1019                        generation: 0,
1020                    })
1021                }
1022                STATE_VERSION_4 => {
1023                    let (identity, rest) = take_lp(rest).ok_or_else(trunc)?;
1024                    let (sessions, rest) = take_lp(rest).ok_or_else(trunc)?;
1025                    let (prekeys, rest) = take_lp(rest).ok_or_else(trunc)?;
1026                    let (gen_bytes, rest) = rest.split_at_checked(8).ok_or_else(trunc)?;
1027                    if !rest.is_empty() {
1028                        return Err(Error::Protocol("state blob has trailing bytes"));
1029                    }
1030                    Ok(SplitState {
1031                        provider,
1032                        identity,
1033                        sessions,
1034                        prekeys: Some(prekeys),
1035                        generation: u64::from_be_bytes(gen_bytes.try_into().expect("8 bytes")),
1036                    })
1037                }
1038                _ => Err(Error::Protocol("unknown state blob version")),
1039            }
1040        }
1041        // No tag: a pre-provider blob. Identity length-prefixed, sessions the
1042        // tail, no prekeys.
1043        _ => {
1044            let (identity, sessions) = take_lp(state).ok_or_else(trunc)?;
1045            Ok(SplitState {
1046                provider: SessionProvider::Legacy,
1047                identity,
1048                sessions,
1049                prekeys: None,
1050                generation: 0,
1051            })
1052        }
1053    }
1054}
1055
1056/// Restore a split blob's prekey store into `party` **before it publishes its
1057/// bundle**.
1058///
1059/// The ordering is the whole point, and it is why this is a free function
1060/// called at the restore sites rather than a method run after connect.
1061/// `publish_bundle` reuses an existing store rather than minting a fresh one, so
1062/// importing first means the client republishes the *restored* bundle: the
1063/// directory and the party agree on one prekey set, the message a new peer
1064/// queued against it still decrypts, and a first contact made *after* the
1065/// restore uses that same republished set. Importing after connect would leave
1066/// the party holding old prekeys while the directory advertised new ones —
1067/// fixing the queued message by breaking the next one.
1068///
1069/// A no-op unless the blob carried a non-empty prekeys section written by this
1070/// same provider; a cross-provider or v2 blob simply re-establishes on first
1071/// send, as it did before.
1072fn restore_prekeys_into<P: CryptoProvider>(party: &mut P, split: &SplitState<'_>) -> Result<()> {
1073    if split.provider != SessionProvider::of::<P>() {
1074        return Ok(());
1075    }
1076    if let Some(prekeys) = split.prekeys.filter(|p| !p.is_empty()) {
1077        party.import_prekeys(prekeys).map_err(crypto)?;
1078    }
1079    Ok(())
1080}
1081
1082/// Verify and open a sealed state blob (decision 0078, anchor B), returning the
1083/// unsealed v3-shaped body and its **authenticated** generation.
1084///
1085/// **A refused authenticator is EX-03's forgery being caught, not a soft
1086/// error.** The attacker EX-03 is about rewrites the state file to present old
1087/// sessions under a high generation; without the secure-storage key they cannot
1088/// produce a matching authenticator, so `unseal` refuses and the restore fails
1089/// closed rather than resuming the rolled-back state. That is the whole point of
1090/// sealing, and it is why the sealed restore paths surface this as an error
1091/// instead of falling back to the unsealed parse.
1092///
1093/// Returns the v3 body, the authenticated `state_generation`, and whether the
1094/// store's rollback counter reports this state as **stale** — a value the store
1095/// has already moved past, i.e. a same-generation rollback the directory witness
1096/// cannot see. The caller discards sessions on a stale verdict, exactly as it
1097/// does on a directory `RolledBack`.
1098/// A sealed state, opened: the v3 body, its authenticated generation,
1099/// whether the store's counter reports it stale, and the address it was
1100/// sealed for (`None` for a v5 blob, written before the address was bound).
1101struct Opened {
1102    body: Vec<u8>,
1103    generation: u64,
1104    stale: bool,
1105    bound: Option<DeviceAddr>,
1106}
1107
1108impl Opened {
1109    /// Refuse a state sealed for another address (BR-17). Exact.
1110    fn expect_address(&self, client: &DeviceAddr) -> Result<()> {
1111        match &self.bound {
1112            Some(state) if state != client => Err(Error::StateMismatch {
1113                state: state.clone(),
1114                client: client.clone(),
1115            }),
1116            _ => Ok(()),
1117        }
1118    }
1119
1120    /// The same check before a sign-in, when only the username and device
1121    /// are known and the tenant prefix is not: the bound user must be that
1122    /// username, bare or under a tenant. The exact check follows sign-in.
1123    fn expect_user(&self, identifier: &str, device: u32) -> Result<()> {
1124        match &self.bound {
1125            Some(state)
1126                if state.device != device
1127                    || !(state.user == identifier
1128                        || state.user.ends_with(&format!("/{identifier}"))) =>
1129            {
1130                Err(Error::StateMismatch {
1131                    state: state.clone(),
1132                    client: DeviceAddr::new(identifier, device),
1133                })
1134            }
1135            _ => Ok(()),
1136        }
1137    }
1138}
1139
1140fn open_sealed(store: &dyn SecureStore, state: &[u8]) -> Result<Opened> {
1141    let rest = match state.split_first() {
1142        Some((&STATE_TAGGED, rest)) => rest,
1143        _ => return Err(Error::SecureStore("not a sealed state blob".into())),
1144    };
1145    let (&version, sealed) = rest
1146        .split_first()
1147        .ok_or(Error::Protocol("state blob truncated"))?;
1148    if version != STATE_VERSION_5 && version != STATE_VERSION_6 {
1149        return Err(Error::SecureStore("not a sealed state blob".into()));
1150    }
1151    let key = zeroize::Zeroizing::new(store.wrap_key().map_err(store_err)?);
1152    let opened = unseal(&key, sealed).map_err(|e| match e {
1153        SealError::BadAuthenticator => Error::SecureStore(
1154            "state authenticator does not match: the file was altered or the key is wrong".into(),
1155        ),
1156        SealError::TooShort => Error::SecureStore("sealed state is truncated".into()),
1157        SealError::UnknownVersion => Error::SecureStore("sealed state envelope version".into()),
1158    })?;
1159    // The sealed body is `v3 body || rollback counter (8 bytes)`; both are under
1160    // the authenticator, so the counter cannot be edited without breaking it.
1161    let split_at = opened
1162        .payload
1163        .len()
1164        .checked_sub(8)
1165        .ok_or(Error::SecureStore(
1166            "sealed state missing its rollback counter".into(),
1167        ))?;
1168    let (mut body, counter_bytes) = opened.payload.split_at(split_at);
1169    let counter = u64::from_be_bytes(counter_bytes.try_into().expect("8 bytes"));
1170    // v6 leads with the address the state was sealed for.
1171    let bound = if version == STATE_VERSION_6 {
1172        let truncated = || Error::SecureStore("sealed state missing its address".into());
1173        let (len, rest) = body.split_at_checked(4).ok_or_else(truncated)?;
1174        let len = u32::from_be_bytes(len.try_into().expect("4 bytes")) as usize;
1175        let (user, rest) = rest.split_at_checked(len).ok_or_else(truncated)?;
1176        let (device, rest) = rest.split_at_checked(4).ok_or_else(truncated)?;
1177        let user = std::str::from_utf8(user)
1178            .map_err(|_| truncated())?
1179            .to_owned();
1180        body = rest;
1181        Some(DeviceAddr::new(
1182            user,
1183            u32::from_be_bytes(device.try_into().expect("4 bytes")),
1184        ))
1185    } else {
1186        None
1187    };
1188    let highest = store.rollback_counter().map_err(store_err)?;
1189    // Older than the newest state this device sealed: a same-generation rollback.
1190    // Equal is fresh — restoring the latest sealed state is ordinary recovery.
1191    let stale = counter < highest;
1192    Ok(Opened {
1193        body: body.to_vec(),
1194        generation: opened.generation,
1195        stale,
1196        bound,
1197    })
1198}
1199
1200/// Where a client dials its services — retained so the connections can be
1201/// re-established after a drop without asking the caller to reconfigure.
1202/// (`Endpoints` without qualification is the SDK's public type, in `tenant`.)
1203struct Dialed {
1204    directory: SocketAddr,
1205    relay: SocketAddr,
1206    dialer: Dialer,
1207}
1208
1209/// The client this build establishes sessions with.
1210///
1211/// **This alias is where the product's provider choice lives**, and it is the
1212/// line decision 0056 step 4 flips. `Client` on its own defaults to the same
1213/// thing, but Rust does not apply a default type parameter when inferring an
1214/// associated function call, so `Client::connect(..)` cannot tell which
1215/// provider it is for. Callers write `DefaultClient::connect(..)` instead.
1216///
1217/// Naming it here rather than making callers write the provider at every call
1218/// site is what made the flip on 2026-08-04 one edit rather than a sweep: a
1219/// caller says "the default", not which provider it is.
1220///
1221/// **It said `Client<Party>` until that day, and that was the bug the flip
1222/// found.** Naming the concrete provider here rather than
1223/// `crypto::DefaultProvider` meant the client kept signing as libsignal after
1224/// the server had moved to verifying as open-tacenta, and every provisioning
1225/// attempt failed its possession check. Two halves of one deployment have to
1226/// move together, which `CryptoProvider::verify_challenge` says in its own
1227/// documentation; the seam was there and this alias was routing around it.
1228pub type DefaultClient = Client<DefaultProvider>;
1229
1230/// A connected client: an identity, a directory connection, an
1231/// authenticated relay connection, and the peers it has open sessions with.
1232///
1233/// Generic over the crypto provider, defaulting to open-tacenta's provider so that every
1234/// existing caller keeps working by writing `Client` and nothing else.
1235///
1236/// **The default is the point, and so is the parameter.** Before this the
1237/// client held libsignal's concrete `Party`, so a feature flag swapped the
1238/// whole binary and the two providers could not both be live. Decision 0056
1239/// steps 4 and 5 assume otherwise: flipping the default for new sessions while
1240/// existing ones continue, and draining them, both need two providers running
1241/// at once. That was not possible and nothing said so.
1242pub struct Client<P: CryptoProvider = DefaultProvider> {
1243    party: P,
1244    directory: DirConnection,
1245    relay: Connection,
1246    /// Pinged by every relay connection this client makes (across
1247    /// reconnects) on a push and when the connection ends; what
1248    /// [`mail`](Client::mail) hands out.
1249    mail: Arc<tokio::sync::Notify>,
1250    me: DeviceAddr,
1251    sessions: HashSet<DeviceAddr>,
1252    endpoints: Dialed,
1253    /// Which implementation established this client's sessions. Recorded so a
1254    /// restored blob says what it holds rather than leaving a caller to assume.
1255    session_provider: SessionProvider,
1256    /// This client's persisted-state generation, the *coarse* anti-rollback
1257    /// counter of decision 0078. Advances on a session-affecting event (a new
1258    /// session), travels in the exported blob, and is presented to the directory
1259    /// by [`checkpoint`](Client::checkpoint). Not every message moves it — that
1260    /// is decision 2's trade.
1261    state_generation: u64,
1262    /// The secure-storage store, once [`attach_secure_store`](Client::attach_secure_store)
1263    /// (or a sealed restore) has provided it. When present, **rollback protection
1264    /// is on**: every send and every ratchet-advancing receive commits a
1265    /// monotonic counter to it (the fine-grained freshness marker that closes the
1266    /// per-send window ER-1 left open), and a sealed export binds that counter's
1267    /// current value. When absent, the client behaves as before — no per-send
1268    /// commits, and only the unsealed / coarse paths are available.
1269    secure_store: Option<Arc<dyn SecureStore + Send + Sync>>,
1270    /// What the constructor found; see [`RestoreOutcome`].
1271    restore: RestoreOutcome,
1272    /// The highest rollback counter this client has seen from its store,
1273    /// so a bump that does not advance past it (a store that was reset or
1274    /// replaced under the client) is refused rather than trusted. Zero
1275    /// until a store is attached.
1276    counter_seen: std::sync::atomic::AtomicU64,
1277}
1278
1279impl<P: CryptoProvider> Client<P> {
1280    /// Generate a fresh identity, publish it to the directory, and
1281    /// authenticate to the relay — leaving a client ready to send and
1282    /// receive. This is first-run enrolment: the identity is new every call,
1283    /// so re-running it for an address that is already bound to a different
1284    /// key is refused by trust-on-first-use. To keep an identity across
1285    /// restarts, save [`export_identity`](Client::export_identity) and
1286    /// reconnect with [`connect_with_identity`](Client::connect_with_identity).
1287    pub async fn connect(config: &Config) -> Result<Self> {
1288        let mut rng = rand::rngs::OsRng.unwrap_err();
1289        let party = P::generate(&config.user, config.device, &mut rng).map_err(crypto)?;
1290        Self::connect_with_party(config, party, &Dialer::tcp(None)).await
1291    }
1292
1293    /// Like [`connect`](Client::connect), but over TLS to a server presenting
1294    /// `server_name` (e.g. the hosted `tacenta.com`). `tls` decides which
1295    /// certificate to trust — [`ClientTls::web_pki`] for a public CA.
1296    pub async fn connect_tls(config: &Config, server_name: &str, tls: &ClientTls) -> Result<Self> {
1297        let mut rng = rand::rngs::OsRng.unwrap_err();
1298        let party = P::generate(&config.user, config.device, &mut rng).map_err(crypto)?;
1299        Self::connect_with_party(config, party, &Dialer::tcp(Some((server_name, tls)))).await
1300    }
1301
1302    /// Reconnect under a previously saved identity (from
1303    /// [`export_identity`](Client::export_identity)): the client presents the
1304    /// same identity key, so the directory refreshes the existing binding
1305    /// rather than rejecting a new key for the address, and peers see no
1306    /// safety-number change. The session and prekey store starts empty —
1307    /// prekeys are re-published and peer sessions re-establish on next use.
1308    pub async fn connect_with_identity(config: &Config, identity: &[u8]) -> Result<Self> {
1309        let party = P::from_identity(&config.user, config.device, identity).map_err(crypto)?;
1310        Self::connect_with_party(config, party, &Dialer::tcp(None)).await
1311    }
1312
1313    /// Reconnect under a full saved state (from
1314    /// [`export_state`](Client::export_state)): the same identity *and* the
1315    /// live ratchet sessions, so open conversations resume mid-ratchet — a
1316    /// message a peer sent while this device was down still decrypts. The
1317    /// stronger sibling of
1318    /// [`connect_with_identity`](Client::connect_with_identity), which keeps
1319    /// only the identity and re-establishes sessions from scratch.
1320    pub async fn connect_with_state(config: &Config, state: &[u8]) -> Result<Self> {
1321        let split = split_state(state)?;
1322        let mut party =
1323            P::from_identity(&config.user, config.device, split.identity).map_err(crypto)?;
1324        restore_prekeys_into(&mut party, &split)?;
1325        let mut client = Self::connect_with_party(config, party, &Dialer::tcp(None)).await?;
1326        client
1327            .restore_sessions(split.provider, split.sessions)
1328            .await?;
1329        client.restore = RestoreOutcome::Resumed;
1330        client.state_generation = split.generation;
1331        // Anchor A (0078) is deliberately NOT auto-invoked here. Its detection
1332        // is defeatable by a file-rewriter (AR-1), so this unsealed default path
1333        // makes no rollback claim and adds no exposure; `checkpoint` is public
1334        // for a deployment that opts into detection-only. The real control is
1335        // anchor B (an authenticated generation plus a per-send `SecureStore`
1336        // counter) — built, and living on the *sealed* path
1337        // (`connect_with_state_sealed` and siblings), not here.
1338        Ok(client)
1339    }
1340
1341    /// Like [`connect_with_state`](Client::connect_with_state), but over TLS to
1342    /// a server presenting `server_name`.
1343    pub async fn connect_with_state_tls(
1344        config: &Config,
1345        server_name: &str,
1346        tls: &ClientTls,
1347        state: &[u8],
1348    ) -> Result<Self> {
1349        let split = split_state(state)?;
1350        let mut party =
1351            P::from_identity(&config.user, config.device, split.identity).map_err(crypto)?;
1352        restore_prekeys_into(&mut party, &split)?;
1353        let mut client =
1354            Self::connect_with_party(config, party, &Dialer::tcp(Some((server_name, tls)))).await?;
1355        client
1356            .restore_sessions(split.provider, split.sessions)
1357            .await?;
1358        client.restore = RestoreOutcome::Resumed;
1359        client.state_generation = split.generation;
1360        // Anchor A (0078) is deliberately NOT auto-invoked here. Its detection
1361        // is defeatable by a file-rewriter (AR-1), so this unsealed default path
1362        // makes no rollback claim and adds no exposure; `checkpoint` is public
1363        // for a deployment that opts into detection-only. The real control is
1364        // anchor B (an authenticated generation plus a per-send `SecureStore`
1365        // counter) — built, and living on the *sealed* path
1366        // (`connect_with_state_sealed` and siblings), not here.
1367        Ok(client)
1368    }
1369
1370    /// Reconnect under a **sealed** full state (from
1371    /// [`export_state_sealed`](Client::export_state_sealed)) — decision 0078's
1372    /// anchor B, the rollback-resistant restore path. Attaches `store` (per-send
1373    /// commits resume), refuses a forged state, and catches a state older than
1374    /// the latest send via the counter — closing EX-03 against a file-rewriter
1375    /// given a rollback-resistant store.
1376    ///
1377    /// Two things happen that the unsealed
1378    /// [`connect_with_state`](Client::connect_with_state) does not do. First,
1379    /// the state's authenticator is verified under a key from `store`; a state a
1380    /// file-rewriter forged (old sessions, high generation) has no matching
1381    /// authenticator and is **refused** here rather than resumed, which is the
1382    /// AR-1 gap closed. Second, because the generation that survives that check
1383    /// is authenticated, presenting it to the directory (`checkpoint`) is a real
1384    /// freshness control: a genuine *older* sealed state — a legitimate backup
1385    /// restore, or an attacker replaying an untouched old file — authenticates
1386    /// but is caught as a rollback, and its sessions are discarded (decision 3).
1387    ///
1388    /// A directory that is unreachable does not fail the connect: the client
1389    /// resumes optimistically and the witness runs on the next reachable
1390    /// checkpoint (decision 3a).
1391    pub async fn connect_with_state_sealed(
1392        config: &Config,
1393        state: &[u8],
1394        store: Arc<dyn SecureStore + Send + Sync>,
1395    ) -> Result<Self> {
1396        let opened = open_sealed(&*store, state)?;
1397        opened.expect_address(&DeviceAddr::new(
1398            config.user.clone(),
1399            u32::from(config.device),
1400        ))?;
1401        let split = split_state(&opened.body)?;
1402        let mut party =
1403            P::from_identity(&config.user, config.device, split.identity).map_err(crypto)?;
1404        restore_prekeys_into(&mut party, &split)?;
1405        let mut client = Self::connect_with_party(config, party, &Dialer::tcp(None)).await?;
1406        client
1407            .restore_sessions(split.provider, split.sessions)
1408            .await?;
1409        client.restore = RestoreOutcome::Resumed;
1410        client.state_generation = opened.generation;
1411        // Hold the store for the restored client's life: per-send commits are on
1412        // from here, so a future rollback is caught at send granularity.
1413        client.attach_secure_store(store)?;
1414        // Same-generation / per-send rollback caught locally by the counter,
1415        // independently of (and before) the directory witness. `checkpoint` then
1416        // discards on a coarse rollback; a network error resumes optimistically
1417        // (3a), so it is swallowed.
1418        if opened.stale {
1419            client.discard_sessions();
1420        }
1421        let _ = client.checkpoint().await;
1422        Ok(client)
1423    }
1424
1425    /// Like [`connect_with_state_sealed`](Client::connect_with_state_sealed),
1426    /// but over TLS to a server presenting `server_name`.
1427    pub async fn connect_with_state_sealed_tls(
1428        config: &Config,
1429        server_name: &str,
1430        tls: &ClientTls,
1431        state: &[u8],
1432        store: Arc<dyn SecureStore + Send + Sync>,
1433    ) -> Result<Self> {
1434        let opened = open_sealed(&*store, state)?;
1435        opened.expect_address(&DeviceAddr::new(
1436            config.user.clone(),
1437            u32::from(config.device),
1438        ))?;
1439        let split = split_state(&opened.body)?;
1440        let mut party =
1441            P::from_identity(&config.user, config.device, split.identity).map_err(crypto)?;
1442        restore_prekeys_into(&mut party, &split)?;
1443        let mut client =
1444            Self::connect_with_party(config, party, &Dialer::tcp(Some((server_name, tls)))).await?;
1445        client
1446            .restore_sessions(split.provider, split.sessions)
1447            .await?;
1448        client.restore = RestoreOutcome::Resumed;
1449        client.state_generation = opened.generation;
1450        // Hold the store for the restored client's life (per-send commits on),
1451        // then act on any rollback the counter caught, before the witness.
1452        client.attach_secure_store(store)?;
1453        if opened.stale {
1454            client.discard_sessions();
1455        }
1456        let _ = client.checkpoint().await;
1457        Ok(client)
1458    }
1459
1460    /// Restore serialized sessions into the party's store and rebuild this
1461    /// client's record of who it has an open session with.
1462    ///
1463    /// Sessions established under a different provider are dropped rather than
1464    /// restored. The two providers derive keys under different labels, so such
1465    /// a session cannot be carried across, and anything that appeared to would
1466    /// be a bug worth more than the convenience. The conversation re-establishes
1467    /// on the next send, which is what a peer reinstalling already does.
1468    async fn restore_sessions(&mut self, provider: SessionProvider, sessions: &[u8]) -> Result<()> {
1469        if provider != SessionProvider::of::<P>() {
1470            return Ok(());
1471        }
1472        let restored = self.party.import_sessions(sessions).await.map_err(crypto)?;
1473        for peer in &restored {
1474            self.sessions.insert(device_addr(peer));
1475        }
1476        Ok(())
1477    }
1478
1479    /// Offer the directory a batch of one-time bundles, best-effort.
1480    ///
1481    /// See the call site for why a failure here is not a connection failure.
1482    async fn deposit_prekeys(party: &mut P, directory: &mut DirConnection, me: &DeviceAddr) {
1483        let mut rng = rand::rngs::OsRng.unwrap_err();
1484        let Ok(batch) = party.publish_one_time_batch(&mut rng).await else {
1485            return;
1486        };
1487        if batch.is_empty() {
1488            return;
1489        }
1490        let identity = party.identity_key();
1491        let _ = directory
1492            .deposit_prekeys(me, identity, batch, |ch| {
1493                party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err())
1494            })
1495            .await;
1496    }
1497
1498    /// Publish `party`'s bundle to the directory and authenticate it to the
1499    /// relay. Shared by [`connect`](Client::connect) (fresh identity) and
1500    /// [`connect_with_identity`](Client::connect_with_identity) (saved one).
1501    async fn connect_with_party(config: &Config, mut party: P, dialer: &Dialer) -> Result<Self> {
1502        let mut rng = rand::rngs::OsRng.unwrap_err();
1503        let me = DeviceAddr::new(config.user.clone(), u32::from(config.device));
1504
1505        // Publish identity + prekey bundle to the directory.
1506        let bundle_bytes = party.publish_bundle(&mut rng).await.map_err(crypto)?;
1507        let identity = party.identity_key();
1508        let mut directory = dialer.directory(config.directory).await?;
1509        let outcome = directory
1510            .register(&me, identity, bundle_bytes, |ch| {
1511                party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err())
1512            })
1513            .await?;
1514        if !matches!(outcome, DirResponse::Registered | DirResponse::Refreshed) {
1515            return Err(Error::Directory(outcome));
1516        }
1517
1518        // Stock the one-time bundle pool the directory dispenses from
1519        // (decision 0074). Registration replaces the pool, so this has to
1520        // follow it rather than precede it.
1521        //
1522        // **Not fatal if it fails, and that is a deliberate asymmetry.**
1523        // Without a pool the directory serves the multi-use bundle to
1524        // everyone, which is sound -- the signed prekey and the KEM
1525        // last-resort key are multi-use by design -- and costs only the extra
1526        // forward secrecy a one-time key would have added. Refusing to
1527        // connect over it would trade a working session for a stronger one
1528        // that is not available, which is the wrong way round. A provider
1529        // with no one-time prekeys returns an empty batch and skips the round
1530        // trip entirely.
1531        Self::deposit_prekeys(&mut party, &mut directory, &me).await;
1532
1533        // Authenticate to the relay; it verifies against the directory.
1534        let sign = |ch: &[u8]| party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err());
1535        let mail = Arc::new(tokio::sync::Notify::new());
1536        let relay = dialer.relay(config.relay, &me, sign, mail.clone()).await?;
1537
1538        Ok(Self {
1539            party,
1540            directory,
1541            relay,
1542            mail,
1543            me,
1544            sessions: HashSet::new(),
1545            session_provider: SessionProvider::of::<P>(),
1546            state_generation: 0,
1547            secure_store: None,
1548            restore: RestoreOutcome::Fresh,
1549            counter_seen: std::sync::atomic::AtomicU64::new(0),
1550            endpoints: Dialed {
1551                directory: config.directory,
1552                relay: config.relay,
1553                dialer: dialer.clone(),
1554            },
1555        })
1556    }
1557
1558    /// This client's own address.
1559    pub fn address(&self) -> &DeviceAddr {
1560        &self.me
1561    }
1562
1563    /// Serialize this client's identity secret so it can be persisted and
1564    /// reused across restarts via
1565    /// [`connect_with_identity`](Client::connect_with_identity). The bytes
1566    /// carry a private key — store them as a secret, at rest as carefully as
1567    /// any other key material.
1568    pub fn export_identity(&self) -> Vec<u8> {
1569        self.party.export_identity()
1570    }
1571
1572    /// Serialize this client's full resumable state — its identity, its live
1573    /// ratchet sessions with every peer it has an open conversation with, **and
1574    /// its published prekey store** — so a process restart resumes conversations
1575    /// mid-ratchet instead of re-establishing them, and can still decrypt a
1576    /// *first-contact* message a new peer sent while this device was down.
1577    /// Re-establishing loses any message a peer sent while this device was down;
1578    /// persisting the sessions keeps live conversations decryptable, and
1579    /// persisting the prekeys keeps first contact decryptable — a message a new
1580    /// peer sent to a published one-time prekey is encrypted to that prekey, and
1581    /// its private half lives only in the prekey store. Restore with
1582    /// [`connect_with_state`](Client::connect_with_state) or
1583    /// [`sign_in_with_state`](Client::sign_in_with_state).
1584    ///
1585    /// The bytes carry the identity private key, session secrets and prekey
1586    /// private halves — store them as a secret, encrypted at rest. Supersedes
1587    /// [`export_identity`](Client::export_identity), which keeps only the
1588    /// identity.
1589    pub async fn export_state(&self) -> Result<Vec<u8>> {
1590        // A v3-shaped body, then the generation appended as a plaintext `u64`.
1591        // That plaintext tail is precisely what a file-rewriter forges (AR-1),
1592        // which is why this path makes no rollback claim; `export_state_sealed`
1593        // binds the same generation inside an authenticator instead.
1594        let mut out = self.export_body_v3().await?;
1595        out[1] = STATE_VERSION_4;
1596        out.extend_from_slice(&self.state_generation.to_be_bytes());
1597        Ok(out)
1598    }
1599
1600    /// Build the v3-shaped state body — `STATE_TAGGED, STATE_VERSION_3,
1601    /// provider`, then length-prefixed identity, sessions and prekeys.
1602    ///
1603    /// Shared by [`export_state`](Client::export_state) (which overwrites the
1604    /// version byte to v4 and appends a plaintext generation) and
1605    /// [`export_state_sealed`](Client::export_state_sealed) (which seals this
1606    /// body under a secure-storage key). Keeping one builder means the two paths
1607    /// cannot drift in what they carry.
1608    async fn export_body_v3(&self) -> Result<Vec<u8>> {
1609        let peers: Vec<Address> = self
1610            .sessions
1611            .iter()
1612            .filter_map(|addr| peer_address(addr).ok())
1613            .collect();
1614        let identity = self.party.export_identity();
1615        let sessions = self.party.export_sessions(&peers).await.map_err(crypto)?;
1616        // Empty when `publish_bundle` was never called; the section is still
1617        // written so the format is uniform, and an empty one is skipped on
1618        // restore.
1619        let prekeys = self.party.export_prekeys().unwrap_or_default();
1620        let mut out = vec![
1621            STATE_TAGGED,
1622            STATE_VERSION_3,
1623            self.session_provider.to_byte(),
1624        ];
1625        put_lp(&mut out, &identity);
1626        put_lp(&mut out, &sessions);
1627        put_lp(&mut out, &prekeys);
1628        Ok(out)
1629    }
1630
1631    /// Export a **sealed** full state — decision 0078's anchor B, the
1632    /// rollback-resistant path: against an attacker who can rewrite the state
1633    /// file it refuses a forged generation and, via the per-send store counter,
1634    /// any state older than the latest send — closing EX-03 against a
1635    /// file-rewriter given a rollback-resistant store.
1636    ///
1637    /// The bytes carry the same identity, sessions and prekeys as
1638    /// [`export_state`](Client::export_state), but the persisted-state
1639    /// generation is bound *inside* an authenticator keyed by the attached
1640    /// [`SecureStore`] rather than appended in plaintext, and the store's
1641    /// **rollback counter** is bound alongside it. An attacker who rewrites the
1642    /// file cannot forge either without the secure-storage key. Restore with
1643    /// [`connect_with_state_sealed`](Client::connect_with_state_sealed) or
1644    /// [`sign_in_with_state_sealed`](Client::sign_in_with_state_sealed).
1645    ///
1646    /// **Requires a store attached** ([`attach_secure_store`](Client::attach_secure_store),
1647    /// or a sealed restore, which attaches it). The counter it binds is the
1648    /// *current* value — send and receive advance it, so it already reflects this
1649    /// state's ratchet position; export does not bump it. That is what makes a
1650    /// restore of a state older than the latest send caught, not just an older
1651    /// *export* (the per-send window ER-1 flagged).
1652    ///
1653    /// The seal authenticates but does **not** encrypt: the bytes carry secrets
1654    /// and must be stored encrypted at rest. What the seal adds is freshness — the
1655    /// property EX-03 is about — not confidentiality.
1656    pub async fn export_state_sealed(&self) -> Result<Vec<u8>> {
1657        let store = self.secure_store.as_ref().ok_or_else(|| {
1658            Error::SecureStore(
1659                "no secure store attached; call attach_secure_store before exporting sealed state"
1660                    .into(),
1661            )
1662        })?;
1663        let key = zeroize::Zeroizing::new(store.wrap_key().map_err(store_err)?);
1664        // Bind the counter's *current* value — send/receive advanced it, so it
1665        // already reflects this state's ratchet position. Reading, not bumping,
1666        // is what closes the per-send window: a restore of any state older than
1667        // the latest send presents a counter below the store's high-water mark.
1668        let counter = store.rollback_counter().map_err(store_err)?;
1669        let mut payload = Vec::new();
1670        put_lp(&mut payload, self.me.user.as_bytes());
1671        payload.extend_from_slice(&self.me.device.to_be_bytes());
1672        payload.extend_from_slice(&self.export_body_v3().await?);
1673        payload.extend_from_slice(&counter.to_be_bytes());
1674        let sealed = seal(&key, self.state_generation, &payload);
1675        let mut out = Vec::with_capacity(2 + sealed.len());
1676        out.push(STATE_TAGGED);
1677        out.push(STATE_VERSION_6);
1678        out.extend_from_slice(&sealed);
1679        Ok(out)
1680    }
1681
1682    /// Present this client's persisted-state generation to the directory
1683    /// (decision 0078), and act on a rollback.
1684    ///
1685    /// The directory advances its per-device anchor and reports whether the
1686    /// state is current (`Fresh`) or older than one it has already witnessed
1687    /// (`RolledBack`). On a rollback the sessions are discarded and the identity
1688    /// kept (decision 3): the ratchets an attacker rewound are not resumed, so
1689    /// they re-establish fresh on the next message and the attacker gets a
1690    /// client that has forgotten the chain keys it wanted replayed.
1691    ///
1692    /// **Opt-in, and detection-only.** This is 0078's anchor A. It is *not*
1693    /// invoked automatically on any shipping path, because the generation it
1694    /// presents is unauthenticated in the state file and a file-rewriter forges
1695    /// it (internal audit AR-1) — so calling it defends only against a
1696    /// legitimate backup restore or a naive whole-file replay, not against the
1697    /// attacker EX-03 is about, and it exposes the anchor-poisoning DoS of AR-2.
1698    /// A deployment that wants that limited detection calls it after connecting,
1699    /// after activity, and on a schedule; the real control waits for anchor B
1700    /// (an authenticated generation under a secure-storage key). Returns the
1701    /// directory's verdict; a network error is surfaced, the rollback is not an
1702    /// error.
1703    pub async fn checkpoint(&mut self) -> Result<DirResponse> {
1704        let generation = self.state_generation;
1705        // Disjoint field borrows: the witness borrows `directory`, the closure
1706        // borrows `party`.
1707        let party = &self.party;
1708        let response = self
1709            .directory
1710            .witness(&self.me, generation, |ch| {
1711                party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err())
1712            })
1713            .await?;
1714        if matches!(response, DirResponse::RolledBack) {
1715            self.discard_sessions();
1716        }
1717        Ok(response)
1718    }
1719
1720    /// Discard every session while keeping the identity — decision 0078's
1721    /// decision 3. The single action taken on a detected rollback, whether the
1722    /// directory witness ([`checkpoint`](Client::checkpoint)) caught a coarse
1723    /// cross-generation one or the secure-storage counter caught a
1724    /// same-generation one on a sealed restore.
1725    fn discard_sessions(&mut self) {
1726        self.party.clear_sessions();
1727        self.sessions.clear();
1728        self.restore = RestoreOutcome::SessionsDiscarded;
1729    }
1730
1731    /// What the constructor found: whether restored sessions are in use or
1732    /// were discarded as a rollback. Read it after any sign-in or connect;
1733    /// a rollback the directory catches later (`checkpoint`) moves it too.
1734    pub fn restore_outcome(&self) -> RestoreOutcome {
1735        self.restore
1736    }
1737
1738    /// Turn on **per-send rollback protection** (decision 0078, closing the ER-1
1739    /// per-send window): hold `store` for the client's life so every send and
1740    /// every ratchet-advancing receive commits the freshness counter to it, and
1741    /// a sealed export binds that counter's current value.
1742    ///
1743    /// Call this once, before sending, on a freshly [`connect`](Client::connect)ed
1744    /// or [`sign_in`](Client::sign_in)ed client; a sealed restore
1745    /// ([`connect_with_state_sealed`](Client::connect_with_state_sealed)) attaches
1746    /// the store itself. Attaching the same store the platform holds for
1747    /// [`export_state_sealed`](Client::export_state_sealed) is what makes the
1748    /// counter monotone across restarts.
1749    ///
1750    /// One store per client: a second attach is refused as
1751    /// [`Error::InvalidArgument`], since re-rooting the counter mid-life
1752    /// would let a state older than the last send pass. The store's
1753    /// current counter is read on attach and every later bump must exceed
1754    /// it, so a store reset underneath the client fails the next send.
1755    pub fn attach_secure_store(&mut self, store: Arc<dyn SecureStore + Send + Sync>) -> Result<()> {
1756        if self.secure_store.is_some() {
1757            return Err(Error::InvalidArgument(
1758                "a secure store is already attached; a client takes one store for its life",
1759            ));
1760        }
1761        let seen = store.rollback_counter().map_err(store_err)?;
1762        self.counter_seen
1763            .store(seen, std::sync::atomic::Ordering::SeqCst);
1764        self.secure_store = Some(store);
1765        Ok(())
1766    }
1767
1768    /// Commit one ratchet advance to the secure-storage counter, if a store is
1769    /// attached. Called on every send and every ratchet-advancing receive: it is
1770    /// the fine-grained freshness marker that closes the per-send window a
1771    /// per-export counter left open (ER-1).
1772    ///
1773    /// **Fails the operation on a secure-storage error, deliberately.** If the
1774    /// advance cannot be recorded, a later restore could not tell that the
1775    /// ratchet had moved, so the safe choice is to refuse the send/receive rather
1776    /// than proceed with an un-recorded advance. A client that cannot tolerate
1777    /// that dependency should not attach a store (and gets no rollback claim).
1778    fn commit_ratchet_advance(&self) -> Result<()> {
1779        if let Some(store) = &self.secure_store {
1780            let advanced = store.bump_rollback_counter().map_err(store_err)?;
1781            let seen = self.counter_seen.load(std::sync::atomic::Ordering::SeqCst);
1782            // Monotone or nothing: a counter that did not move past what
1783            // this client has seen is a store reset or replaced under it,
1784            // and a send recorded against it would be unprotected.
1785            if advanced <= seen {
1786                return Err(Error::SecureStore(format!(
1787                    "the secure store's rollback counter did not advance ({advanced} after {seen})"
1788                )));
1789            }
1790            self.counter_seen
1791                .store(advanced, std::sync::atomic::Ordering::SeqCst);
1792        }
1793        Ok(())
1794    }
1795
1796    /// Whether this client currently holds an open session with `peer` — a
1797    /// conversation it can resume without a fresh handshake. Goes to `false`
1798    /// for every peer after a rollback [`checkpoint`](Client::checkpoint)
1799    /// discards the sessions.
1800    pub fn has_open_session(&self, peer: &DeviceAddr) -> bool {
1801        self.sessions.contains(peer)
1802    }
1803
1804    /// Re-establish the directory and relay connections, re-authenticating
1805    /// with this client's identity. Sessions live in memory and survive, so
1806    /// conversations continue where they left off. [`send`](Client::send) and
1807    /// [`receive`](Client::receive) call this themselves (with bounded
1808    /// backoff) when they find the connection gone; it is public for a caller
1809    /// that wants to reconnect eagerly.
1810    pub async fn reconnect(&mut self) -> Result<()> {
1811        let dialer = &self.endpoints.dialer;
1812        let directory = dialer.directory(self.endpoints.directory).await?;
1813        let party = &self.party;
1814        let sign = |ch: &[u8]| party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err());
1815        let relay = dialer
1816            .relay(self.endpoints.relay, &self.me, sign, self.mail.clone())
1817            .await?;
1818        self.directory = directory;
1819        self.relay = relay;
1820        Ok(())
1821    }
1822
1823    /// [`reconnect`](Client::reconnect) with bounded patience: an immediate
1824    /// attempt, then 1s/2s/4s/8s backoff — enough to ride out a server
1825    /// restart, bounded so a dead server surfaces as an error, not a hang.
1826    async fn reconnect_with_patience(&mut self) -> Result<()> {
1827        let mut delay = std::time::Duration::from_secs(1);
1828        let mut outcome = self.reconnect().await;
1829        for _ in 0..4 {
1830            if outcome.is_ok() {
1831                break;
1832            }
1833            // Half to one-and-a-half times the step: a fleet cut off by one
1834            // restart does not come back in lockstep.
1835            let jitter: u8 = rand::Rng::random(&mut rand::rngs::OsRng.unwrap_err());
1836            sleep(delay.mul_f64(0.5 + f64::from(jitter) / 255.0)).await;
1837            delay *= 2;
1838            outcome = self.reconnect().await;
1839        }
1840        outcome
1841    }
1842
1843    /// Fetch and decode `to`'s published prekey bundle from the directory.
1844    /// A peer's published bundle, as bytes.
1845    ///
1846    /// Bytes rather than a decoded bundle: the seam takes a serialized bundle,
1847    /// so decoding it here would mean naming a provider's type to hand it
1848    /// straight back. Whether the bytes are a bundle at all is the provider's
1849    /// question, and it answers it in `establish_session`.
1850    async fn fetch_bundle(&mut self, to: &DeviceAddr) -> Result<Vec<u8>> {
1851        let DirResponse::Found { bundle, .. } = self.directory.lookup(to).await? else {
1852            return Err(Error::Relay(RelayRefusal::UnknownRecipient));
1853        };
1854        Ok(bundle)
1855    }
1856
1857    /// Encrypt and send `message` to `to`, opening a session (via a
1858    /// directory lookup of the recipient's bundle) on first contact.
1859    ///
1860    /// If the connection is found dead, a bounded reconnect runs and the
1861    /// same ciphertext is retried once — the retry re-sends, it does not
1862    /// re-encrypt, so the ratchet advances exactly once per call. In the
1863    /// rare case where the first attempt landed before the connection
1864    /// died, the duplicate is dropped by the recipient (a replayed
1865    /// ciphertext cannot decrypt twice).
1866    pub async fn send(&mut self, to: &DeviceAddr, message: &[u8]) -> Result<()> {
1867        // Before any ratchet step or store commit: a message the relay would
1868        // refuse is the caller's mistake, and costs nothing here.
1869        if message.len() > MAX_MESSAGE_BYTES {
1870            return Err(Error::InvalidArgument(
1871                "message exceeds the relay's per-message size limit",
1872            ));
1873        }
1874        let mut rng = rand::rngs::OsRng.unwrap_err();
1875        let peer = peer_address(to)?;
1876        if !self.sessions.contains(to) {
1877            let peer_bundle = match self.fetch_bundle(to).await {
1878                Err(Error::Io(_)) => {
1879                    self.reconnect_with_patience().await?;
1880                    self.fetch_bundle(to).await?
1881                }
1882                other => other?,
1883            };
1884            self.party
1885                .establish_session(&peer, &peer_bundle, &mut rng)
1886                .await
1887                .map_err(crypto)?;
1888            if self.sessions.insert(to.clone()) {
1889                // A new session is a session-affecting event (0078, decision 2).
1890                self.state_generation += 1;
1891            }
1892        }
1893        let framed = self
1894            .party
1895            .encrypt(&peer, message, &mut rng)
1896            .await
1897            .map_err(crypto)?;
1898        // The sending ratchet advanced; record it in secure storage before the
1899        // message goes out, so a restore of any state older than this send is
1900        // caught (ER-1, the per-send window). No-op unless a store is attached.
1901        self.commit_ratchet_advance()?;
1902        let request = encode_request(&Request::Send {
1903            to: to.clone(),
1904            envelope: Envelope {
1905                kind: Kind::Dm,
1906                payload: framed,
1907            },
1908        });
1909        let resp = match self.relay.request(&request).await {
1910            Err(_) => {
1911                self.reconnect_with_patience().await?;
1912                self.relay.request(&request).await?
1913            }
1914            Ok(resp) => resp,
1915        };
1916        match decode_response(&resp) {
1917            Some(Response::Ok) => Ok(()),
1918            // Permanent: the message is over the relay's per-message size limit
1919            // (ER-2). Distinct from backpressure so a caller does not retry it.
1920            Some(Response::TooLarge) => Err(Error::Relay(RelayRefusal::TooLarge)),
1921            // Transient: the recipient's queue is at its count or byte budget.
1922            Some(Response::QueueFull) => Err(Error::Relay(RelayRefusal::QueueFull)),
1923            Some(Response::UnknownRecipient) => Err(Error::Relay(RelayRefusal::UnknownRecipient)),
1924            _ => Err(Error::Protocol("send was not accepted")),
1925        }
1926    }
1927
1928    /// Wait for the next non-empty batch of mail, decrypting and
1929    /// acknowledging every pending message and returning each with its
1930    /// sender. Because the relay attributes messages, this works even for a
1931    /// first-contact message from a peer this client has never talked to.
1932    ///
1933    /// The relay is polled *before* waiting, so everything queued while this
1934    /// client was offline is drained immediately — a reconnecting client
1935    /// gets its backlog without waiting for the next push. If the connection
1936    /// is found dead, a bounded reconnect (immediate, then 1s/2s/4s/8s) runs
1937    /// before giving up.
1938    ///
1939    /// A message that cannot be decrypted — a replayed ciphertext, a corrupt
1940    /// envelope — is acknowledged past and dropped rather than returned:
1941    /// retrying it can never succeed, and refusing to advance would wedge
1942    /// the queue behind one poison message forever.
1943    pub async fn receive(&mut self) -> Result<Vec<Received>> {
1944        loop {
1945            let batch = self.drain().await?;
1946            if !batch.is_empty() {
1947                return Ok(batch);
1948            }
1949            // Nothing pending: wait for the server's push, then poll again.
1950            // The wait is on the mail signal rather than the connection, so
1951            // a head that holds this client behind a lock can do the same
1952            // wait without the lock (see [`mail`](Client::mail)); a dead
1953            // connection pings the signal too, and the next poll reconnects.
1954            self.mail.notified().await;
1955        }
1956    }
1957
1958    /// The signal that says mail may be waiting: pinged on every push from
1959    /// the relay, across reconnects, and when a connection ends. A caller
1960    /// that shares this client behind a lock waits on this outside the lock
1961    /// and then calls [`drain`](Client::drain), so another task can `send`
1962    /// meanwhile; [`receive`](Client::receive) is that loop for a caller
1963    /// holding the client itself. The signal keeps one permit, so a push
1964    /// that lands between a poll and the wait is not missed.
1965    pub fn mail(&self) -> MailSignal {
1966        MailSignal(self.mail.clone())
1967    }
1968
1969    /// Inbound messages as a stream, one at a time in the order the relay
1970    /// delivered them: [`receive`](Client::receive) flattened, for a caller
1971    /// that wants each message as it arrives rather than batches. The
1972    /// stream borrows the client while it is polled, so a task that also
1973    /// sends keeps the client behind a lock and does what the FFI and
1974    /// browser heads do: wait on [`mail`](Client::mail) outside the lock
1975    /// and [`drain`](Client::drain) inside it. An error is yielded once
1976    /// and ends the stream; calling again starts another. The stream is
1977    /// not `Unpin`: pin it (`std::pin::pin!`) before calling `next`.
1978    pub fn inbound(&mut self) -> impl futures_util::Stream<Item = Result<Received>> + '_ {
1979        futures_util::stream::try_unfold(
1980            (self, std::collections::VecDeque::new()),
1981            |(client, mut buffered)| async move {
1982                loop {
1983                    if let Some(message) = buffered.pop_front() {
1984                        return Ok(Some((message, (client, buffered))));
1985                    }
1986                    buffered.extend(client.receive().await?);
1987                }
1988            },
1989        )
1990    }
1991
1992    /// A single non-blocking poll: whatever is queued for this device right now,
1993    /// or an empty batch if nothing is pending. Unlike [`receive`](Self::receive)
1994    /// it never waits for the server's push — the caller drives the cadence.
1995    ///
1996    /// This exists for a loop that must also do something else between checks
1997    /// for mail (read the keyboard, redraw a UI): `receive` would park in its
1998    /// wait and starve that work, and cancelling `receive` on a timer is unsafe
1999    /// because the poll it wraps is a socket round trip that must not be torn
2000    /// mid-response. `drain` returns promptly either way, so the caller decides
2001    /// when to poll again.
2002    pub async fn drain(&mut self) -> Result<Vec<Received>> {
2003        match self.poll_batch().await {
2004            Ok(batch) => Ok(batch),
2005            Err(Error::Io(_)) => {
2006                self.reconnect_with_patience().await?;
2007                self.poll_batch().await
2008            }
2009            Err(e) => Err(e),
2010        }
2011    }
2012
2013    /// One poll: fetch whatever the relay holds for this device, decrypt
2014    /// what decrypts, acknowledge everything fetched. Empty if nothing was
2015    /// pending (or the whole batch was poison).
2016    async fn poll_batch(&mut self) -> Result<Vec<Received>> {
2017        let mut rng = rand::rngs::OsRng.unwrap_err();
2018        let poll = encode_request(&Request::Poll {
2019            device: self.me.clone(),
2020        });
2021        let Some(Response::Delivered { from, messages }) =
2022            decode_response(&self.relay.request(&poll).await?)
2023        else {
2024            return Err(Error::Protocol("expected a delivery"));
2025        };
2026
2027        let mut received = Vec::with_capacity(messages.len());
2028        for message in &messages {
2029            let Ok(peer) = peer_address(&message.from) else {
2030                continue;
2031            };
2032            let Ok(plaintext) = self
2033                .party
2034                .decrypt(&peer, &message.envelope.payload, &mut rng)
2035                .await
2036            else {
2037                continue;
2038            };
2039            if self.sessions.insert(message.from.clone()) {
2040                // First contact from a new peer establishes a session — a
2041                // session-affecting event (0078, decision 2).
2042                self.state_generation += 1;
2043            }
2044            received.push(Received {
2045                from: message.from.clone(),
2046                plaintext,
2047            });
2048        }
2049
2050        // The receiving ratchet advanced for each decrypted message. Commit that
2051        // to the counter — **best-effort, unlike send**: the messages are already
2052        // decrypted, so failing here would lose them. The send path (the re-send
2053        // reuse the review named) is the fail-closed guarantee; a secure-storage
2054        // failure during receive leaves only a narrow receive-side replay window.
2055        if !received.is_empty() {
2056            let _ = self.commit_ratchet_advance();
2057        }
2058
2059        if !messages.is_empty() {
2060            let ack = encode_request(&Request::Ack {
2061                device: self.me.clone(),
2062                up_to: from + messages.len() as u64,
2063            });
2064            match decode_response(&self.relay.request(&ack).await?) {
2065                Some(Response::Acked { accepted: true }) => {}
2066                _ => return Err(Error::Protocol("acknowledgement was not accepted")),
2067            }
2068        }
2069        Ok(received)
2070    }
2071
2072    /// Re-key this client's identity: generate a fresh identity, rotate the
2073    /// directory binding to it — authorized by the current key (decision
2074    /// record 0024) — and adopt it.
2075    ///
2076    /// Rotation invalidates existing peer sessions: the new identity has an
2077    /// empty store, so the next [`send`](Client::send) to each peer opens a
2078    /// fresh session, and a peer who verified the old key sees a
2079    /// safety-number change. The live relay connection, authenticated under
2080    /// the old key when the client connected, stays valid for its lifetime.
2081    pub async fn rotate(&mut self) -> Result<()> {
2082        let mut rng = rand::rngs::OsRng.unwrap_err();
2083        let device =
2084            u8::try_from(self.me.device).map_err(|_| Error::Protocol("device id out of range"))?;
2085        let mut next = P::generate(&self.me.user, device, &mut rng).map_err(crypto)?;
2086        let new_bundle = next.publish_bundle(&mut rng).await.map_err(crypto)?;
2087        let new_identity = next.identity_key();
2088
2089        // Possession is proved by the new key; the change is authorized by
2090        // the currently bound key. Disjoint borrows: the directory (mut), the
2091        // address, and the current key are separate fields of `self`.
2092        let current = &self.party;
2093        let outcome = self
2094            .directory
2095            .rotate(
2096                &self.me,
2097                new_identity,
2098                new_bundle,
2099                |ch| next.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err()),
2100                |stmt| current.sign_challenge(stmt, &mut rand::rngs::OsRng.unwrap_err()),
2101            )
2102            .await?;
2103        if outcome != DirResponse::Rotated {
2104            return Err(Error::Directory(outcome));
2105        }
2106        self.party = next;
2107        self.sessions.clear();
2108        Ok(())
2109    }
2110
2111    /// The delivered-to-all watermark across `devices`: how many messages the
2112    /// relay has confirmed delivered to *every* one of them (decision record
2113    /// 0026) — the minimum of their per-device delivered counts. Every
2114    /// address must belong to this client's user; the relay refuses a query
2115    /// that spans another user's devices.
2116    pub async fn delivered_watermark(&mut self, devices: &[DeviceAddr]) -> Result<u64> {
2117        let request = encode_request(&Request::Delivered {
2118            devices: devices.to_vec(),
2119        });
2120        match decode_response(&self.relay.request(&request).await?) {
2121            Some(Response::DeliveredCount { count }) => Ok(count),
2122            _ => Err(Error::Protocol("expected a delivered count")),
2123        }
2124    }
2125
2126    /// Resolve `username` within this client's own tenant to a [`Contact`], or
2127    /// `None` if no such user is registered. The username is combined with the
2128    /// client's tenant — read from its own handle `"<tenant>/<user>"` — so it
2129    /// finds users in the same tenant; exact resolution only, no enumeration.
2130    /// Errs if this client is not operating under a tenant handle (the
2131    /// pre-account path). Resolves the primary device (device 1); multi-device
2132    /// resolution is later work.
2133    pub async fn find(&mut self, username: &str) -> Result<Option<Contact>> {
2134        let tenant = self
2135            .me
2136            .user
2137            .split_once('/')
2138            .map(|(tenant, _)| tenant)
2139            .ok_or(Error::InvalidArgument(
2140                "client is not under a tenant handle",
2141            ))?;
2142        let address = DeviceAddr::new(format!("{tenant}/{username}"), 1);
2143        match self.directory.lookup(&address).await? {
2144            DirResponse::Found { .. } => Ok(Some(Contact { address })),
2145            DirResponse::NotFound => Ok(None),
2146            other => Err(Error::Directory(other)),
2147        }
2148    }
2149
2150    /// Sign up a new user under a tenant. A control-plane action: it creates
2151    /// the account but does not connect — call [`sign_in`](Client::sign_in)
2152    /// afterwards for a connected client. `api_key` scopes it to the tenant.
2153    pub async fn sign_up(
2154        accounts: SocketAddr,
2155        api_key: &str,
2156        username: &str,
2157        password: &str,
2158    ) -> Result<()> {
2159        Self::sign_up_trusting(accounts, &Dialer::tcp(None), api_key, username, password).await
2160    }
2161
2162    /// Like [`sign_up`](Client::sign_up), but over TLS to a server presenting
2163    /// `server_name` (the hosted path).
2164    pub async fn sign_up_tls(
2165        accounts: SocketAddr,
2166        server_name: &str,
2167        tls: &ClientTls,
2168        api_key: &str,
2169        username: &str,
2170        password: &str,
2171    ) -> Result<()> {
2172        Self::sign_up_trusting(
2173            accounts,
2174            &Dialer::tcp(Some((server_name, tls))),
2175            api_key,
2176            username,
2177            password,
2178        )
2179        .await
2180    }
2181
2182    /// [`sign_up`](Client::sign_up) and [`sign_up_tls`](Client::sign_up_tls)
2183    /// in one: `trust` is `None` for plaintext, or the server name and the
2184    /// trust to check its certificate against. The tenant handle calls this.
2185    pub(crate) async fn sign_up_trusting(
2186        accounts: SocketAddr,
2187        dialer: &Dialer,
2188        api_key: &str,
2189        username: &str,
2190        password: &str,
2191    ) -> Result<()> {
2192        let mut conn = dialer.accounts(accounts).await?;
2193        match conn.sign_up_user(api_key, username, password).await? {
2194            AccountResponse::UserCreated { .. } => Ok(()),
2195            // A success-shaped reply where a refusal was expected carries
2196            // things (a token, a key) that must not reach an error string.
2197            AccountResponse::TenantCreated { .. } | AccountResponse::SignedIn { .. } => {
2198                Err(Error::Protocol("unexpected account response"))
2199            }
2200            other => Err(Error::Account(other)),
2201        }
2202    }
2203
2204    /// Sign in and provision a fresh device identity, returning a client that
2205    /// sends and receives under the account's handle (e.g. `acme/alice`).
2206    ///
2207    /// Save [`export_identity`](Client::export_identity) and reconnect with
2208    /// [`sign_in_with_identity`](Client::sign_in_with_identity) on later runs,
2209    /// so the device keeps the same key: re-provisioning a *new* key for an
2210    /// already-bound handle is refused by trust-on-first-use.
2211    pub async fn sign_in(config: &AccountConfig) -> Result<Self> {
2212        Self::sign_in_trusting(config, &Dialer::tcp(None)).await
2213    }
2214
2215    /// Like [`sign_in`](Client::sign_in), but over TLS to a server presenting
2216    /// `server_name` (the hosted path). `tls` decides the trust —
2217    /// [`ClientTls::web_pki`] for a public CA such as Let's Encrypt.
2218    pub async fn sign_in_tls(
2219        config: &AccountConfig,
2220        server_name: &str,
2221        tls: &ClientTls,
2222    ) -> Result<Self> {
2223        Self::sign_in_trusting(config, &Dialer::tcp(Some((server_name, tls)))).await
2224    }
2225
2226    /// [`sign_in`](Client::sign_in) and [`sign_in_tls`](Client::sign_in_tls)
2227    /// in one, keyed on `trust`.
2228    pub(crate) async fn sign_in_trusting(config: &AccountConfig, dialer: &Dialer) -> Result<Self> {
2229        let mut rng = rand::rngs::OsRng.unwrap_err();
2230        let party = P::generate(&config.identifier, config.device, &mut rng).map_err(crypto)?;
2231        Self::sign_in_with_party(config, party, dialer).await
2232    }
2233
2234    /// Sign in and provision under a saved device identity (from
2235    /// [`export_identity`](Client::export_identity)), keeping the same bound
2236    /// key across restarts.
2237    pub async fn sign_in_with_identity(config: &AccountConfig, identity: &[u8]) -> Result<Self> {
2238        let party =
2239            P::from_identity(&config.identifier, config.device, identity).map_err(crypto)?;
2240        Self::sign_in_with_party(config, party, &Dialer::tcp(None)).await
2241    }
2242
2243    /// Sign in under a full saved state (from
2244    /// [`export_state`](Client::export_state)): the same device identity *and*
2245    /// its live ratchet sessions, so conversations resume mid-ratchet across a
2246    /// process restart. The account-path sibling of
2247    /// [`connect_with_state`](Client::connect_with_state).
2248    pub async fn sign_in_with_state(config: &AccountConfig, state: &[u8]) -> Result<Self> {
2249        Self::sign_in_with_state_trusting(config, &Dialer::tcp(None), state).await
2250    }
2251
2252    /// Like [`sign_in_with_state`](Client::sign_in_with_state), but over TLS to
2253    /// a server presenting `server_name` (the hosted path).
2254    pub async fn sign_in_with_state_tls(
2255        config: &AccountConfig,
2256        server_name: &str,
2257        tls: &ClientTls,
2258        state: &[u8],
2259    ) -> Result<Self> {
2260        Self::sign_in_with_state_trusting(config, &Dialer::tcp(Some((server_name, tls))), state)
2261            .await
2262    }
2263
2264    /// [`sign_in_with_state`](Client::sign_in_with_state) and its TLS sibling
2265    /// in one, keyed on `trust`.
2266    pub(crate) async fn sign_in_with_state_trusting(
2267        config: &AccountConfig,
2268        dialer: &Dialer,
2269        state: &[u8],
2270    ) -> Result<Self> {
2271        let split = split_state(state)?;
2272        let mut party =
2273            P::from_identity(&config.identifier, config.device, split.identity).map_err(crypto)?;
2274        restore_prekeys_into(&mut party, &split)?;
2275        let mut client = Self::sign_in_with_party(config, party, dialer).await?;
2276        client
2277            .restore_sessions(split.provider, split.sessions)
2278            .await?;
2279        client.restore = RestoreOutcome::Resumed;
2280        client.state_generation = split.generation;
2281        // Anchor A (0078) is deliberately NOT auto-invoked here. Its detection
2282        // is defeatable by a file-rewriter (AR-1), so this unsealed default path
2283        // makes no rollback claim and adds no exposure; `checkpoint` is public
2284        // for a deployment that opts into detection-only. The real control is
2285        // anchor B (an authenticated generation plus a per-send `SecureStore`
2286        // counter) — built, and living on the *sealed* path
2287        // (`connect_with_state_sealed` and siblings), not here.
2288        Ok(client)
2289    }
2290
2291    /// Sign in under a **sealed** full state (from
2292    /// [`export_state_sealed`](Client::export_state_sealed)) — the account-path
2293    /// sibling of
2294    /// [`connect_with_state_sealed`](Client::connect_with_state_sealed), and the
2295    /// rollback-resistant sign-in path (decision 0078, anchor B). Attaches
2296    /// `store`; a forged state is refused, any state older than the latest send
2297    /// is caught by the store counter, and the generation is witnessed to the
2298    /// directory — closing EX-03 against a file-rewriter given a
2299    /// rollback-resistant store.
2300    pub async fn sign_in_with_state_sealed(
2301        config: &AccountConfig,
2302        state: &[u8],
2303        store: Arc<dyn SecureStore + Send + Sync>,
2304    ) -> Result<Self> {
2305        Self::sign_in_with_state_sealed_trusting(config, &Dialer::tcp(None), state, store).await
2306    }
2307
2308    /// Like [`sign_in_with_state_sealed`](Client::sign_in_with_state_sealed),
2309    /// but over TLS to a server presenting `server_name` (the hosted path).
2310    pub async fn sign_in_with_state_sealed_tls(
2311        config: &AccountConfig,
2312        server_name: &str,
2313        tls: &ClientTls,
2314        state: &[u8],
2315        store: Arc<dyn SecureStore + Send + Sync>,
2316    ) -> Result<Self> {
2317        Self::sign_in_with_state_sealed_trusting(
2318            config,
2319            &Dialer::tcp(Some((server_name, tls))),
2320            state,
2321            store,
2322        )
2323        .await
2324    }
2325
2326    /// [`sign_in_with_state_sealed`](Client::sign_in_with_state_sealed) and
2327    /// its TLS sibling in one, keyed on the dialer. The tenant handle calls
2328    /// this.
2329    pub(crate) async fn sign_in_with_state_sealed_trusting(
2330        config: &AccountConfig,
2331        dialer: &Dialer,
2332        state: &[u8],
2333        store: Arc<dyn SecureStore + Send + Sync>,
2334    ) -> Result<Self> {
2335        let opened = open_sealed(&*store, state)?;
2336        // Before sign-in only the username and device are known; the exact
2337        // address (with its tenant) is checked once the server has said it.
2338        opened.expect_user(&config.identifier, u32::from(config.device))?;
2339        let split = split_state(&opened.body)?;
2340        let mut party =
2341            P::from_identity(&config.identifier, config.device, split.identity).map_err(crypto)?;
2342        restore_prekeys_into(&mut party, &split)?;
2343        let mut client = Self::sign_in_with_party(config, party, dialer).await?;
2344        opened.expect_address(&client.me)?;
2345        client
2346            .restore_sessions(split.provider, split.sessions)
2347            .await?;
2348        client.restore = RestoreOutcome::Resumed;
2349        client.state_generation = opened.generation;
2350        // Hold the store for the restored client's life (per-send commits
2351        // on), then act on any rollback the counter caught, before the
2352        // witness: the order is load-bearing.
2353        client.attach_secure_store(store)?;
2354        if opened.stale {
2355            client.discard_sessions();
2356        }
2357        let _ = client.checkpoint().await;
2358        Ok(client)
2359    }
2360
2361    /// Sign in for a session token, provision `party`'s identity into the
2362    /// directory under the account's handle, then connect the directory and
2363    /// authenticate to the relay as that handle. Shared by
2364    /// [`sign_in`](Client::sign_in) and
2365    /// [`sign_in_with_identity`](Client::sign_in_with_identity).
2366    async fn sign_in_with_party(
2367        config: &AccountConfig,
2368        mut party: P,
2369        dialer: &Dialer,
2370    ) -> Result<Self> {
2371        let mut rng = rand::rngs::OsRng.unwrap_err();
2372
2373        // Sign in for a session token.
2374        let mut accounts = dialer.accounts(config.accounts).await?;
2375        let token = match accounts
2376            .sign_in(&config.api_key, &config.identifier, &config.password)
2377            .await?
2378        {
2379            AccountResponse::SignedIn { token, .. } => token,
2380            AccountResponse::TenantCreated { .. } | AccountResponse::UserCreated { .. } => {
2381                return Err(Error::Protocol("unexpected account response"));
2382            }
2383            other => return Err(Error::Account(other)),
2384        };
2385
2386        // Provision this device's identity under the account handle.
2387        let bundle_bytes = party.publish_bundle(&mut rng).await.map_err(crypto)?;
2388        let identity = party.identity_key();
2389        let mut provisioning = dialer.provisioning(config.provisioning).await?;
2390        let handle = match provisioning
2391            .provision(
2392                &token,
2393                u32::from(config.device),
2394                identity,
2395                bundle_bytes,
2396                |ch| party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err()),
2397            )
2398            .await?
2399        {
2400            ProvisionOutcome::Provisioned { handle } => handle,
2401            other => return Err(Error::Provision(other)),
2402        };
2403        let me = DeviceAddr::new(handle, u32::from(config.device));
2404
2405        // Connect the directory (peer lookups) and authenticate to the relay
2406        // as the provisioned handle.
2407        let directory = dialer.directory(config.directory).await?;
2408        let sign = |ch: &[u8]| party.sign_challenge(ch, &mut rand::rngs::OsRng.unwrap_err());
2409        let mail = Arc::new(tokio::sync::Notify::new());
2410        let relay = dialer.relay(config.relay, &me, sign, mail.clone()).await?;
2411
2412        Ok(Self {
2413            party,
2414            directory,
2415            relay,
2416            mail,
2417            me,
2418            sessions: HashSet::new(),
2419            session_provider: SessionProvider::of::<P>(),
2420            state_generation: 0,
2421            secure_store: None,
2422            restore: RestoreOutcome::Fresh,
2423            counter_seen: std::sync::atomic::AtomicU64::new(0),
2424            endpoints: Dialed {
2425                directory: config.directory,
2426                relay: config.relay,
2427                dialer: dialer.clone(),
2428            },
2429        })
2430    }
2431}
2432
2433/// Sleep on the runtime this target has: tokio's timer natively, the
2434/// browser's in wasm.
2435async fn sleep(d: std::time::Duration) {
2436    #[cfg(not(target_arch = "wasm32"))]
2437    tokio::time::sleep(d).await;
2438    #[cfg(target_arch = "wasm32")]
2439    gloo_timers::future::sleep(d).await;
2440}