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:
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1212711005;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1702138901;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -223,6 +223,42 @@ fn wire__crate__api__events_stream_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__init_storage_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "init_storage",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
let api_dir = <String>::sse_decode(&mut deserializer);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, crate::BridgeError>(
|
||||
(move || async move {
|
||||
let output_ok = crate::api::init_storage(api_dir).await?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__is_connected_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -677,10 +713,11 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
3 => wire__crate__api__connect_impl(port, ptr, rust_vec_len, data_len),
|
||||
4 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
|
||||
5 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
|
||||
6 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__start_audio_impl(port, ptr, rust_vec_len, data_len),
|
||||
6 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
||||
7 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__start_audio_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -693,7 +730,7 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
7 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
8 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user