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}