Skip to main content

ping_mls_store/
backend.rs

1// `PathBuf` + `Zeroizing` are only referenced by the `Sqlite` variant, which is
2// native-only. Gate the imports the same way to avoid a wasm32 unused-imports lint.
3#[cfg(not(target_arch = "wasm32"))]
4use std::path::PathBuf;
5#[cfg(not(target_arch = "wasm32"))]
6use zeroize::Zeroizing;
7
8use std::sync::Arc;
9
10use crate::AsyncBlobStore;
11
12/// Where the persistent provider checkpoints `MemoryStorage` to. Selected at
13/// `MessagingClient::init` time via [`ping_core::ClientConfig::storage_backend`].
14#[derive(Debug, Clone, Default)]
15pub enum StorageBackend {
16    /// In-memory only. Default and what tests use; loses state on process exit.
17    /// Cold-start scenarios (iOS NSE, web Service Worker) MUST use [`Self::Sqlite`]
18    /// or [`Self::IndexedDb`] instead.
19    #[default]
20    Memory,
21
22    /// SQLite-backed; native targets only. The SDK owns the file at `path`; the
23    /// parent directory must exist. Pass `encryption_key = Some(...)` to enable
24    /// SQLCipher; absence means the file is unencrypted (tests, dev only).
25    #[cfg(not(target_arch = "wasm32"))]
26    Sqlite {
27        /// Absolute path. Host convention: `<app_support>/ping/<account_id>/mls.sqlite`
28        /// for per-account isolation (CR-18); the SDK doesn't enforce this — the host
29        /// picks the path.
30        path: PathBuf,
31        /// SQLCipher key. The SDK zeroes its copy on drop; the host is responsible
32        /// for sourcing this from the OS keyring (Keychain / Keystore / etc.).
33        encryption_key: Option<Zeroizing<[u8; 32]>>,
34    },
35
36    /// SQLite-backed, opened READ-ONLY and never written back; native only.
37    ///
38    /// Exists for one job: letting a SECOND process (the iOS Notification
39    /// Service Extension) decrypt an inbound message for a lock-screen preview
40    /// while the main app remains the sole writer.
41    ///
42    /// That restriction is not caution, it is the only sound arrangement given
43    /// how this crate persists. [`Self::Sqlite`] checkpoints the ENTIRE
44    /// `MemoryStorage` map as one CBOR blob in a single row, so two writers do
45    /// not merge — the second one to flush replaces every mutation the first
46    /// made, silently discarding epochs, ratchet state, and freshly-stored key
47    /// material. Read-only removes that failure mode by construction: a reader
48    /// takes a consistent WAL snapshot, decrypts from it, and drops everything.
49    /// Nothing it touches can outlive the process, so nothing it does can be
50    /// observed by the writer.
51    ///
52    /// The cost is that the ratchet advance is thrown away, so the main app
53    /// decrypts the same message again from its own state. That is correct and
54    /// intended — the two are independent readers of the same snapshot, and
55    /// neither can desynchronise the other.
56    ///
57    /// [`PersistentMlsProvider::checkpoint`] is a NO-OP against this backend
58    /// rather than an error, so shared code paths that flush opportunistically
59    /// stay usable. Pair it with a read-only host `Storage` and
60    /// [`ping_core::MessagingClient::open_read_only`], which refuses the
61    /// state-creating paths (`LocalDevice` minting, DeviceGroup creation) that
62    /// would otherwise write through a channel this backend does not cover.
63    #[cfg(not(target_arch = "wasm32"))]
64    SqliteReadOnly {
65        /// Absolute path to a file another process owns. Must already exist —
66        /// unlike [`Self::Sqlite`], this variant never creates one, because a
67        /// created file would mean the writer's real store was not found and
68        /// silently decrypting nothing is worse than failing loudly.
69        path: PathBuf,
70        /// SQLCipher key. Same value the writing process uses; on iOS both
71        /// read it from the shared Keychain access group.
72        encryption_key: Option<Zeroizing<[u8; 32]>>,
73    },
74
75    /// Host-supplied async blob storage. Available on every target.
76    ///
77    /// The provider snapshots the entire `MemoryStorage` HashMap into a
78    /// single CBOR blob and round-trips it through the
79    /// [`AsyncBlobStore`] the host implements. This is the universal
80    /// persistence path:
81    ///   * **WASM**: host wraps its IndexedDB layer (PingStorageWeb, which
82    ///     already AES-GCM-encrypts every row under a non-extractable
83    ///     wrap key kept inside the same IDB).
84    ///   * **iOS / macOS / Android**: host wraps the same `Storage` trait
85    ///     it already provides for conv metadata + cursors (typically
86    ///     Keychain-encrypted SQLite or AsyncStorage), so the OpenMLS
87    ///     snapshot sits under a reserved `("__mls", "snapshot")` slot
88    ///     alongside the host's other persisted KV.
89    ///
90    /// Why this replaces the wasm-only `IndexedDb` variant: any host
91    /// that already implements `Storage` (which every binding does)
92    /// gets persistence for free, without per-platform path management,
93    /// Keychain key plumbing, or SQLCipher dependency. The SQLite
94    /// backend remains available for hosts that want a separate file
95    /// (e.g. iOS NSE cold-start where the main app's Storage isn't
96    /// reachable from the extension).
97    AsyncBlob { blob_store: Arc<dyn AsyncBlobStore> },
98
99    /// [`Self::AsyncBlob`] read-only — the universal-target counterpart to
100    /// [`Self::SqliteReadOnly`], for the same job on a platform with no SQLite.
101    ///
102    /// The snapshot is read through the host's blob store at open and never
103    /// written back, so a second context (a web Service Worker decrypting a
104    /// push for its notification, the mirror of the iOS NSE) can read the tab's
105    /// state without racing it. Same reasoning, same guarantee: the whole-state
106    /// blob means two writers replace rather than merge, so the reader must not
107    /// be one.
108    ///
109    /// This variant is what keeps
110    /// [`ping_core::MessagingClient::open_read_only`] a genuinely cross-platform
111    /// API rather than a native-only one — on WASM it is the only backend that
112    /// can satisfy it.
113    AsyncBlobReadOnly { blob_store: Arc<dyn AsyncBlobStore> },
114}