02ffadfa525972fe66f2abcf117fdb5f852bcaaa
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
50768a8f48 |
feat(mvp): v1.0.0-rc.1 — keyring-backed DEK, encrypted bookmarks, MVP release-gate docs
Closes the v0.4 dual-file weakness in identity-at-rest and turns the release into an MVP public release candidate. The remaining work before `v1.0.0` is DEC-012 legal sign-off — see `docs/governance/legal-review-readiness.md` — and the staged platform promotions in `docs/governance/staged-release-plan.md`. No decision rows in `product-decision-register.md` change; the register's change-history advances to 0.9.8. `chanora_storage` ----------------- * New public `Crypto` trait + `IdentityFileStore::crypto()` give callers an encrypt / decrypt pair anchored on the per-install 32-byte DEK without exposing the key material. * `IdentityFileStore` keyring-first DEK retrieval (Linux Secret Service via D-Bus, macOS Keychain, Windows Credential Manager, iOS Keychain via the `keyring` crate). Pre-existing `identity.dek` files are opportunistically migrated into the keyring on first run; the on-disk DEK copy is removed once the keyring acknowledges. `CHANORA_DISABLE_KEYRING=1` forces the file-fallback path for tests and headless / CI hosts where a real keyring call would prompt the user or block on a missing D-Bus session. * `BookmarkRepository::with_crypto(dir, crypto)` encrypts the server password into a new `password_blob` BLOB column under the same per-install DEK. Schema v2 migration is idempotent — legacy v0.4 rows with a plain `password TEXT` are read transparently and lifted into `password_blob` on the next `update()`. `BookmarkRepository::new` (no crypto) is preserved for tests and as a documented fallback when the DEK is unreachable. * Storage tests rise from 8 to 10: encrypted bookmark password round-trip + legacy-plaintext-bookmark upgrade. `chanora_core` -------------- * `ChanoraSession::init_storage(dir)` wires the bookmark repository with crypto by default. On any crypto-derivation failure it falls back to the plain-password repository and logs the gap — better than hard-failing init. * `supervisor_loop` now tracks a 64-bit `snapshot_signature` over channels (id + parent + order + name) and clients (id + channel + name) instead of the old `(channel_count, client_count)` tuple. Any in-channel client move, channel rename, or reorder now fires `SessionEvent::SnapshotChanged`. The signature sorts by id before hashing so it's stable under input-vector reordering. * Two new unit tests cover the signature behaviour; new `tests/mvp_storage.rs` integration test drives `ChanoraSession::init_storage` end-to-end and verifies the bookmark `password_blob` does not contain the plaintext. * Re-export `ChannelId` + `ClientId` from `chanora_protocol` so downstream callers and tests can construct DTOs directly. Flutter ------- * New About dialog (info icon in the AppBar) surfaces DEC-018 (public name "Chanora"), DEC-019 (non-affiliation statement), and DEC-020 (Apache-2.0 OR MIT dual license). New ARB keys in `app_en.arb` and `app_zh.arb`: `aboutAction`, `aboutVersion`, `aboutNonAffiliation`, `aboutLicenseHeading`, `aboutLicenseBody`, `aboutThirdPartyHeading`, `aboutThirdPartyBody`. * `pubspec.yaml` version bumps to `1.0.0-rc.1+5`. Governance ---------- * `docs/governance/legal-review-readiness.md` — DEC-012 handoff package. Enumerates trademark / non-affiliation / license-text / third-party-attribution / `tsclientlib`-posture / crypto- export / data-handling items the legal reviewer must confirm, and lists the concrete engineering deliverables they block on (`cargo about generate`, `cargo deny check licenses`, Flutter `LicenseRegistry` dump). * `docs/governance/staged-release-plan.md` — DEC-002 channel schedule. Linux + Android sideload promote to GA on DEC-012 sign-off; Play Store / Windows / macOS / iOS gate on per- platform signed-build availability. Rollback policy included. * `product-decision-register.md` change-history advances to 0.9.8 with a single entry summarising v0.3, v0.4, and v1.0-rc.1 progress against DEC-001. No decision rows mutate. Build + ops ----------- * `NOTICE` refreshed for the MVP product-code dependency set: adds `chacha20poly1305`, `rand`, `zeroize`, `base64`, `keyring`, `connectivity_plus`, `path_provider`, `freezed_annotation`; drops PoC-only entries. * `CHANGELOG.md` restructured: explicit version sections for v0.3.0-beta.1, v0.4.0-beta.2, v1.0.0-rc.1. Previous "Unreleased" contents migrated into their respective milestone sections. * `.github/workflows/ci.yml` exports `CHANORA_DISABLE_KEYRING=1` for the cargo-test job — CI runners have no D-Bus session and the keyring crate would otherwise block. * `run-chanora.sh` reads `CHANORA_BUNDLE_FLAVOUR` (default `release`) and self-copies the latest cdylib into the bundle's `lib/` if missing. Verification ------------ * `cargo test --workspace` with `CHANORA_DISABLE_KEYRING=1`: all green (49 unit tests across the workspace; up from 36 at v0.4.0-beta.2). * `cargo test -p chanora_core --release -- --ignored alpha_smoke` passes against the live `cn.teamspeak.app` (DNS → connect → snapshot → disconnect in ~2.5 s). * `flutter analyze`: clean. * `cargo build -p chanora_bridge --release` + `flutter build linux --release` produce a working Linux x86_64 bundle. No Android live test in this commit per the user's note that the physical device was removed; the Android arm64-v8a build path is mechanically identical to v0.4.0-beta.2. |
||
|
|
780fd7eca2 |
feat(beta): External Beta — passwords, channel join, mute, bookmarks, encrypted identity
The v0.3 client could only ever connect to a hardcoded default
channel with no password and offered no controls mid-call.
External Beta closes those gaps and tightens identity-at-rest.
User-facing additions
---------------------
* **Server password** on the connect form. Plumbed through
`BridgeError`-aware `connect(host, nickname, password)`. Empty
string means "no password" — no behaviour change for open
servers.
* **Channel join**: tapping a row (or its login icon) in the
channel tree issues a `client_move`. Names containing "🔒" or
"password" prompt for a channel password first.
* **Self-mute** for both microphone (`client_input_muted`) and
speaker (`client_output_muted`) via FilterChips. Output mute
also flips the audio engine's local output-muted flag so
playback silences immediately, before the server acknowledges.
* **Master output gain** slider (0–200%). Plumbed through an
`AtomicU32` (f32 bits) on the engine that the cpal output
callback multiplies into every sample.
* **Bookmarks**: SQLite-backed list with Save / Connect / Delete
actions. Bookmarks persist across app restarts; tapping one
pre-fills the form and dials immediately.
Hardening
---------
* **Encrypted identity at rest** (RISK-PoC-002 closure for the
file-only threat model). ChaCha20-Poly1305 envelope: nonce +
ciphertext written atomically with mode 0600; 32-byte DEK in a
separate `identity.dek` file. Legacy plaintext identity files
are auto-detected, read, and upgraded on the next save. Full OS-
keyring integration is still v0.4 work — documented in the
store's doc comment.
* **Mobile voice-comm routing**: on Android, `AudioEngine::start`
uses JNI to set `AudioManager.setMode(MODE_IN_COMMUNICATION)`
when `cfg.mobile_voice_preset` is true (default). This engages
the device-side AEC/NS pipeline on most Pixel/Moto/Samsung
hardware even though cpal still opens the AAudio default input
preset. Full `setInputPreset(VOICE_COMMUNICATION)` switch is
still RISK-AUDIO-MOBILE-001 (needs cpal upstream or an Oboe
fork).
* **Log noise**: bridge default `EnvFilter` now silences
`tsproto::resend=error` and `tsproto::packet_codec=error` so
the redacted diagnostic export is human-readable. Still
overridable via `RUST_LOG=...`.
Engineering
-----------
* **`chanora_storage`** gains `BookmarkRepository` (rusqlite
bundled) with `add` / `update` / `delete` / `list`. The
identity store now layers on `chacha20poly1305` + `rand` +
`zeroize` for the envelope.
* **`chanora_protocol`** exposes `move_to_channel` and
`set_muted` on `ProtocolClient`, dispatched through the
existing `connection_task` request channel onto tsclientlib's
generated `client.client_move(...)` and
`state.client_update().set_input_muted/set_output_muted(...)`
paths.
* **`chanora_core::ChanoraSession`** wires the bookmark store
next to the identity store inside `init_storage`, and adds
`list_bookmarks` / `add_bookmark` / `update_bookmark` /
`delete_bookmark` / `move_to_channel` / `set_self_muted` /
`set_output_gain`.
* **`chanora_audio::AudioEngine`** carries `output_gain` and
`output_muted` atomics; the output callback consults both. The
Android branch of `start()` engages MODE_IN_COMMUNICATION via
a small JNI helper that reuses the `ndk_context` global set by
the bridge's `android_init` hook.
* **`chanora_bridge::api`** adds `set_input_muted`,
`set_output_muted`, `set_output_gain`, `move_to_channel`,
`list_bookmarks`, `add_bookmark`, `update_bookmark`,
`delete_bookmark`, and the `BridgeBookmark` DTO. FRB v2.12
codegen regenerated.
Tests + CI
----------
* `chanora_storage` test count rises from 3 to 8 — bookmark CRUD
round-trip, missing-row → `NotFound`, encrypted round-trip
(verifies ciphertext is not the plaintext on disk), and the
legacy plaintext upgrade path.
* New `.github/workflows/ci.yml`: `cargo check --workspace`,
`cargo test --workspace --no-fail-fast`, `cargo clippy`
(advisory), `flutter analyze`, and `flutter test` excluding
the live-server `e2e` tag.
Live-verified on Moto G Stylus 5G against cn.teamspeak.app:
saved a bookmark, reconnected via it, joined a non-default
channel via tap, toggled both mutes, slid the volume, and the
redacted diagnostic export confirmed `AudioManager mode set to
MODE_IN_COMMUNICATION`, `client_move sent`, and `client_update
sent` lines.
|
||
|
|
71ecb83781 |
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. |
||
|
|
974dda9601 |
feat(scaffold): create product workspace + Rust crates + Flutter app
Implements the canonical implementation directory layout adopted by
DEC-022 (register v0.9.5). Closes the scaffolding phase; no PoC code
has been promoted in yet (per proof-of-concept-plan.md §4 a PoC is
not product code unless explicitly promoted).
Rust workspace
==============
Top-level Cargo.toml declares seven workspace members:
core/chanora_core top-level Rust API + orchestration
crates/chanora_protocol tsclientlib isolation (SAD-067, SysDes-011/029)
crates/chanora_state state sync, reducers, deltas
crates/chanora_audio capture, DSP, Opus, jitter, mixer, playback
crates/chanora_storage non-secret DB + platform secure store
crates/chanora_diagnostics logs, redaction, export
crates/chanora_bridge typed Flutter/Rust DTOs
Workspace-wide pins:
license = "MIT OR Apache-2.0" (DEC-020)
rust-version = "1.95"
edition = "2021"
The Flutter app (apps/chanora_flutter) is NOT a Cargo workspace
member; it is owned by the Flutter / Gradle toolchain and is in the
workspace exclude array along with every poc/* spike.
Each crate ships:
* a Cargo.toml referring to workspace.dependencies pins;
* a lib.rs with #![forbid(unsafe_code)] + #![warn(missing_docs)],
a typed Error enum, and the public types relevant to the
subsystem's role per SAD §7.2;
* minimal unit tests so
running 1 test
test tests::defaults_match_decisions ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_compiles ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::session_can_be_constructed ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::marker_matches_poc ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_compiles ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::state_transitions_compile ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 1 test
test tests::it_compiles ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s is non-empty.
chanora_core's CoreError type-wraps every subsystem error via
#[from] so callers can match on origin without parsing strings.
chanora_audio's AudioEffects struct defaults all four effects to
true, matching DEC-007 (AEC), DEC-008 (AGC), DEC-009 (NS),
DEC-010 (HPF). A unit test pins this so a future regression that
flips a default fails immediately.
chanora_diagnostics exports REDACTION_MARKER = "[REDACTED]",
identical to the PoC's marker so audit grep patterns survive the
promotion.
chanora_bridge's BridgeError is Serialize + Deserialize so it can
flow across the FRB 2.x boundary (DEC-014).
Empirical verification: cargo check --workspace clean, cargo test
--workspace clean (7 unit tests + 7 doc-test runners, all passing)
against Rust 1.95.0 stable.
Flutter app
===========
apps/chanora_flutter created with .
DEC-004 applied: minSdk overridden to 28 in
android/app/build.gradle.kts with a comment that points back at the
decision register and forbids lowering it without re-opening DEC-004.
DEC-015 applied: shipped English + Simplified Chinese at MVP.
- pubspec.yaml gains flutter_localizations + intl + generate:true.
- l10n.yaml emits lib/l10n/generated/AppL10n (no synthetic package
— that was removed in Flutter 3.41+).
- lib/l10n/app_en.arb is the source of truth; lib/l10n/app_zh.arb
mirrors the key set in zh-Hans. ARB metadata explicitly
reaffirms ADR-008: server-provided content (channel names,
nicknames, welcome banners) is preserved verbatim and never
translated.
lib/main.dart and test/widget_test.dart were rewritten from the
template counter into a minimal localized scaffold that proves
both locales render correctly.
Empirical verification: reports no issues;
runs the two locale smoke tests and both pass.
|