feat(storage): A.2 — persist TS3 identity across app restarts

A fresh `Identity::create()` was generated on every connect, which
meant the server saw a different client UID each time. Long-lived
features (bookmarks, server-side bans, group membership) depend on a
stable UID — restoring that now via a minimal directory-backed
identity file.

* `chanora_storage::IdentityFileStore` reads / writes a single
  `identity.tskey` file under a caller-supplied directory. On Unix
  the file is created with `O_CREAT | O_TRUNC | mode 0600`; on
  non-Unix targets the platform sandbox does the access control.
  Writes are atomic (temp file + `fsync` + `rename`) so a crash
  mid-write cannot leave a half-written identity on disk. Empty
  files are treated as "no identity" rather than as an error.
* `chanora_protocol::ProtocolClient::generate_identity()` exposes
  the `counterVbase64key` serialisation used by tsclientlib's
  `Identity::new_from_str`, so the core layer can mint an identity
  and store it before dialling.
* `chanora_core::ChanoraSession::init_storage(dir)` wires the
  store. `connect()` then resolves the identity in this order:
  (1) `cfg.identity` if explicitly supplied; (2) persisted value if
  any; (3) generate-and-persist a fresh one.
* `chanora_bridge::api::init_storage(dir: String)` is the
  Flutter-facing entrypoint; the matching Dart side resolves
  `path_provider`'s `getApplicationSupportDirectory()` and calls
  it once on app start.
* `BridgeError` now maps `CoreError::Storage`.

Beta caveat (RISK-PoC-002 / SS-RISK-FALLBACK): the identity is not
encrypted at rest. The v0.4 storage rework lands proper Secret
Service + Android Keystore + iOS Keychain backends. Documented
under `IdentityFileStore`'s doc comment.

Live-verified on Moto G Stylus 5G: first connect generated +
persisted the identity (visible in the redacted diagnostic export
as "generated + persisted fresh identity"); disconnect + reconnect
in the same session logged "reusing persisted identity" and dialled
with the same UID.
This commit is contained in:
EdisonJwa
2026-05-15 01:25:07 +08:00
parent f52d702e27
commit 71ecb83781
12 changed files with 506 additions and 23 deletions
+11
View File
@@ -130,6 +130,17 @@ impl SnapshotProbe {
}
impl ProtocolClient {
/// Generate a fresh, persistable TS3 identity string. The
/// returned value is the canonical `counter`V`base64key` form
/// accepted by [`ConnectConfig::identity`] and by tsclientlib's
/// `Identity::new_from_str`. Callers should persist it via
/// `chanora_storage` so subsequent connects reuse the same
/// identity and the server sees the same client UID.
pub fn generate_identity() -> String {
let id = Identity::create();
format!("{}V{}", id.counter(), id.key().to_ts())
}
/// Dial the server and wait for the initial state snapshot. The
/// returned client is ready for [`Self::snapshot`] and
/// [`Self::disconnect`] calls.