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.
This commit is contained in:
EdisonJwa
2026-05-15 02:24:42 +08:00
parent 780fd7eca2
commit 50768a8f48
21 changed files with 1471 additions and 115 deletions
+10
View File
@@ -17,6 +17,16 @@
"disconnectAction": "Disconnect",
"refreshAction": "Refresh",
"diagnosticsAction": "Diagnostics",
"aboutAction": "About",
"aboutVersion": "Version {version}",
"@aboutVersion": {
"placeholders": { "version": { "type": "String" } }
},
"aboutNonAffiliation": "Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.",
"aboutLicenseHeading": "License",
"aboutLicenseBody": "Chanora is dual-licensed under the Apache License, Version 2.0 or the MIT License, at your option. The full license texts ship as LICENSE-APACHE and LICENSE-MIT at the repository root.",
"aboutThirdPartyHeading": "Third-party software",
"aboutThirdPartyBody": "Chanora is built on tsclientlib, flutter_rust_bridge, cpal, rusqlite, and the Flutter framework, among others. See the NOTICE file at the repository root for the current attribution list.",
"copyAction": "Copy",
"closeAction": "Close",
"startAudioAction": "Start audio",
+7
View File
@@ -16,6 +16,13 @@
"disconnectAction": "断开连接",
"refreshAction": "刷新",
"diagnosticsAction": "诊断信息",
"aboutAction": "关于",
"aboutVersion": "版本 {version}",
"aboutNonAffiliation": "Chanora 是独立项目,与 TeamSpeak 之间不存在任何附属、认可、赞助或官方关联关系。",
"aboutLicenseHeading": "许可协议",
"aboutLicenseBody": "Chanora 采用 Apache License 2.0 或 MIT License 双协议授权,使用者可任选其一。完整协议文本以 LICENSE-APACHE 与 LICENSE-MIT 形式随仓库一同分发。",
"aboutThirdPartyHeading": "第三方组件",
"aboutThirdPartyBody": "Chanora 基于 tsclientlib、flutter_rust_bridge、cpal、rusqlite、Flutter 框架等开源组件构建。完整归属信息请参阅仓库根目录的 NOTICE 文件。",
"copyAction": "复制",
"closeAction": "关闭",
"startAudioAction": "启动语音",
@@ -169,6 +169,48 @@ abstract class AppL10n {
/// **'Diagnostics'**
String get diagnosticsAction;
/// No description provided for @aboutAction.
///
/// In en, this message translates to:
/// **'About'**
String get aboutAction;
/// No description provided for @aboutVersion.
///
/// In en, this message translates to:
/// **'Version {version}'**
String aboutVersion(String version);
/// No description provided for @aboutNonAffiliation.
///
/// In en, this message translates to:
/// **'Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.'**
String get aboutNonAffiliation;
/// No description provided for @aboutLicenseHeading.
///
/// In en, this message translates to:
/// **'License'**
String get aboutLicenseHeading;
/// No description provided for @aboutLicenseBody.
///
/// In en, this message translates to:
/// **'Chanora is dual-licensed under the Apache License, Version 2.0 or the MIT License, at your option. The full license texts ship as LICENSE-APACHE and LICENSE-MIT at the repository root.'**
String get aboutLicenseBody;
/// No description provided for @aboutThirdPartyHeading.
///
/// In en, this message translates to:
/// **'Third-party software'**
String get aboutThirdPartyHeading;
/// No description provided for @aboutThirdPartyBody.
///
/// In en, this message translates to:
/// **'Chanora is built on tsclientlib, flutter_rust_bridge, cpal, rusqlite, and the Flutter framework, among others. See the NOTICE file at the repository root for the current attribution list.'**
String get aboutThirdPartyBody;
/// No description provided for @copyAction.
///
/// In en, this message translates to:
@@ -46,6 +46,32 @@ class AppL10nEn extends AppL10n {
@override
String get diagnosticsAction => 'Diagnostics';
@override
String get aboutAction => 'About';
@override
String aboutVersion(String version) {
return 'Version $version';
}
@override
String get aboutNonAffiliation =>
'Chanora is independent and is not affiliated with, endorsed by, sponsored by, or officially associated with TeamSpeak.';
@override
String get aboutLicenseHeading => 'License';
@override
String get aboutLicenseBody =>
'Chanora is dual-licensed under the Apache License, Version 2.0 or the MIT License, at your option. The full license texts ship as LICENSE-APACHE and LICENSE-MIT at the repository root.';
@override
String get aboutThirdPartyHeading => 'Third-party software';
@override
String get aboutThirdPartyBody =>
'Chanora is built on tsclientlib, flutter_rust_bridge, cpal, rusqlite, and the Flutter framework, among others. See the NOTICE file at the repository root for the current attribution list.';
@override
String get copyAction => 'Copy';
@@ -44,6 +44,32 @@ class AppL10nZh extends AppL10n {
@override
String get diagnosticsAction => '诊断信息';
@override
String get aboutAction => '关于';
@override
String aboutVersion(String version) {
return '版本 $version';
}
@override
String get aboutNonAffiliation =>
'Chanora 是独立项目,与 TeamSpeak 之间不存在任何附属、认可、赞助或官方关联关系。';
@override
String get aboutLicenseHeading => '许可协议';
@override
String get aboutLicenseBody =>
'Chanora 采用 Apache License 2.0 或 MIT License 双协议授权,使用者可任选其一。完整协议文本以 LICENSE-APACHE 与 LICENSE-MIT 形式随仓库一同分发。';
@override
String get aboutThirdPartyHeading => '第三方组件';
@override
String get aboutThirdPartyBody =>
'Chanora 基于 tsclientlib、flutter_rust_bridge、cpal、rusqlite、Flutter 框架等开源组件构建。完整归属信息请参阅仓库根目录的 NOTICE 文件。';
@override
String get copyAction => '复制';
+73
View File
@@ -19,6 +19,10 @@ import 'l10n/generated/app_localizations.dart';
import 'src/rust/api.dart' as rust;
import 'src/rust/frb_generated.dart';
/// Public version string shown in the About dialog. Aligned with
/// `pubspec.yaml` and the git tag for the MVP release candidate.
const String _kAppVersion = 'v1.0.0-rc.1';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await RustLib.init();
@@ -369,6 +373,70 @@ class _BetaHomeState extends State<_BetaHome> {
);
}
Future<void> _onShowAbout(BuildContext context) async {
// DEC-018 / DEC-019 / DEC-020 surface: public name, non-
// affiliation statement, dual-license declaration. The Flutter
// showAboutDialog widget is intentionally bare so the legal
// text comes from us, not a framework default.
final l10n = AppL10n.of(context);
final theme = Theme.of(context);
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n.aboutAction),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.appTitle,
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(
l10n.aboutVersion(_kAppVersion),
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 16),
Text(
l10n.aboutNonAffiliation,
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 12),
Text(
l10n.aboutLicenseHeading,
style: theme.textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
l10n.aboutLicenseBody,
style: theme.textTheme.bodySmall,
),
const SizedBox(height: 12),
Text(
l10n.aboutThirdPartyHeading,
style: theme.textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
l10n.aboutThirdPartyBody,
style: theme.textTheme.bodySmall,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(l10n.closeAction),
),
],
),
);
}
Future<void> _onAddCurrentBookmark() async {
final l10n = AppL10n.of(context);
final nameCtl = TextEditingController(text: _hostCtl.text.trim());
@@ -453,6 +521,11 @@ class _BetaHomeState extends State<_BetaHome> {
appBar: AppBar(
title: Text(l10n.appTitle),
actions: [
IconButton(
tooltip: l10n.aboutAction,
icon: const Icon(Icons.info_outline),
onPressed: () => _onShowAbout(context),
),
IconButton(
tooltip: l10n.diagnosticsAction,
icon: const Icon(Icons.bug_report_outlined),
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.2.0+2
version: 1.0.0-rc.1+5
environment:
sdk: ^3.11.5