diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml index 140dc22..3017132 100644 --- a/.github/actions/setup-rust/action.yml +++ b/.github/actions/setup-rust/action.yml @@ -1,7 +1,9 @@ name: Setup Rust description: > Install the Rust toolchain with platform targets and a build cache, so - cargokit can compile the kolibri native library during the Flutter build. + cargokit can compile the native libraries during the Flutter build. kolibri + comes from pub.dev and builds inside the pub cache, so only the in-repo crate + is listed here; the shared cargo registry and git checkouts are cached anyway. inputs: targets: @@ -19,4 +21,4 @@ runs: - name: Cache Rust build uses: Swatinem/rust-cache@v2 with: - workspaces: third_party/kolibri/kolibri-dart/rust + workspaces: native/komet_crypto/rust diff --git a/.gitignore b/.gitignore index eba31df..7fb1504 100644 --- a/.gitignore +++ b/.gitignore @@ -148,3 +148,8 @@ maxmint/ maxtun/ turnprobe/ test/live_server_probe_test.dart + +# local kolibri development: point the plugin at third_party/kolibri and the +# Rust core at third_party/kolibri/kolibri-net instead of the published ones +pubspec_overrides.yaml +.cargo/ diff --git a/.gitmodules b/.gitmodules index 4032a51..cbeb1b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ [submodule "third_party/rlottie"] path = third_party/rlottie url = https://github.com/Samsung/rlottie.git -[submodule "third_party/kolibri"] - path = third_party/kolibri - url = https://github.com/KometTeam/kolibri.git diff --git a/lib/backend/api.dart b/lib/backend/api.dart index cd1432b..9a80f72 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -121,7 +121,7 @@ class Api { } if (gen != _connectGen) return; - ({String host, int port}) endpoint; + ({String host, int port, bool trustMincifryCa}) endpoint; try { endpoint = await ServerConfig.loadEndpoint().timeout(_endpointTimeout); } catch (e) { @@ -129,10 +129,13 @@ class Api { endpoint = ( host: ServerConfig.defaultHost, port: ServerConfig.defaultPort, + trustMincifryCa: ServerConfig.defaultTrustMincifryCa, ); } if (gen != _connectGen) return; + setTrustMincifryCa(enabled: endpoint.trustMincifryCa); + final (session, wireLog) = await _buildSessionOptions(endpoint); if (gen != _connectGen) return; @@ -366,7 +369,7 @@ class Api { /// Строит устройство-поля и создаёт сессию ядра. Заодно заполняет /// [_userAgent] и [_deviceId] для геттеров. Future<(KolibriSession, Stream)> _buildSessionOptions( - ({String host, int port}) endpoint, + ({String host, int port, bool trustMincifryCa}) endpoint, ) async { final deviceInfo = DeviceInfoPlugin(); diff --git a/lib/core/config/config.dart b/lib/core/config/config.dart index 40d09e6..ed57789 100644 --- a/lib/core/config/config.dart +++ b/lib/core/config/config.dart @@ -1,15 +1,18 @@ import 'package:shared_preferences/shared_preferences.dart'; abstract class ServerConfig { - static const String defaultHost = 'api.oneme.ru'; + static const String defaultHost = 'api2.oneme.ru'; static const int defaultPort = 443; + static const bool defaultTrustMincifryCa = true; static const String prefHostKey = 'server_host_override'; static const String prefPortKey = 'server_port_override'; + static const String prefTrustMincifryKey = 'server_trust_mincifry_ca'; static const Duration pingInterval = Duration(seconds: 10); static const Duration requestTimeout = Duration(seconds: 30); static const int maxReconnectAttempts = 50; - static Future<({String host, int port})> loadEndpoint() async { + static Future<({String host, int port, bool trustMincifryCa})> + loadEndpoint() async { final prefs = await SharedPreferences.getInstance(); final rawHost = prefs.getString(prefHostKey); final rawPort = prefs.getInt(prefPortKey); @@ -20,6 +23,11 @@ abstract class ServerConfig { if (rawPort != null && rawPort >= 1 && rawPort <= 65535) { port = rawPort; } - return (host: host, port: port); + return ( + host: host, + port: port, + trustMincifryCa: + prefs.getBool(prefTrustMincifryKey) ?? defaultTrustMincifryCa, + ); } } diff --git a/lib/core/push/push_service.dart b/lib/core/push/push_service.dart index 3a21860..ab7b020 100644 --- a/lib/core/push/push_service.dart +++ b/lib/core/push/push_service.dart @@ -16,6 +16,7 @@ import '../calls/ws2_signaling.dart'; import '../protocol/opcode_map.dart'; import '../storage/app_instance.dart'; import '../storage/token_storage.dart'; +import '../transport/tls_config.dart'; import '../utils/logger.dart'; const _channelId = 'komet_messages'; @@ -56,6 +57,7 @@ Future _handleCallDecline(String payloadJson) async { // Фоновый изолят: инициализируем ядро перед vcp-декодом/сигналингом. await initKolibri(); + await TlsConfig.applyMincifryTrust(); final params = ConversationParams.decode(vcp); if (params == null) return; @@ -93,6 +95,7 @@ Future _handleReply(String payloadJson, String text) async { SharedPreferences.setPrefix('flutter.${AppInstance.id}.'); } catch (_) {} } + await TlsConfig.applyMincifryTrust(); final plugin = FlutterLocalNotificationsPlugin(); final notifId = chatId & 0x7fffffff; diff --git a/lib/core/transport/tls_config.dart b/lib/core/transport/tls_config.dart index 50d8639..4db0a81 100644 --- a/lib/core/transport/tls_config.dart +++ b/lib/core/transport/tls_config.dart @@ -1,8 +1,20 @@ +import 'package:kolibri/kolibri.dart' show setTrustMincifryCa; import 'package:shared_preferences/shared_preferences.dart'; +import '../config/config.dart'; + abstract class TlsConfig { static const String prefKey = 'dev_tls_insecure'; + static Future applyMincifryTrust() async { + final prefs = await SharedPreferences.getInstance(); + setTrustMincifryCa( + enabled: + prefs.getBool(ServerConfig.prefTrustMincifryKey) ?? + ServerConfig.defaultTrustMincifryCa, + ); + } + static Future isInsecureAllowed() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool(prefKey) ?? false; diff --git a/lib/frontend/screens/auth/server_settings_sheet.dart b/lib/frontend/screens/auth/server_settings_sheet.dart index 7eb1707..67128a6 100644 --- a/lib/frontend/screens/auth/server_settings_sheet.dart +++ b/lib/frontend/screens/auth/server_settings_sheet.dart @@ -27,6 +27,7 @@ class _ServerSettingsSheetState extends State { text: '${ServerConfig.defaultPort}', ); bool _busy = false; + bool _trustMincifryCa = ServerConfig.defaultTrustMincifryCa; @override void initState() { @@ -40,6 +41,7 @@ class _ServerSettingsSheetState extends State { setState(() { _hostController.text = endpoint.host; _portController.text = '${endpoint.port}'; + _trustMincifryCa = endpoint.trustMincifryCa; }); } @@ -55,6 +57,7 @@ class _ServerSettingsSheetState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.setString(ServerConfig.prefHostKey, host); await prefs.setInt(ServerConfig.prefPortKey, port); + await prefs.setBool(ServerConfig.prefTrustMincifryKey, _trustMincifryCa); await api.disconnect(); unawaited(api.connect()); final online = await api.stateStream @@ -82,8 +85,10 @@ class _ServerSettingsSheetState extends State { final prefs = await SharedPreferences.getInstance(); await prefs.remove(ServerConfig.prefHostKey); await prefs.remove(ServerConfig.prefPortKey); + await prefs.remove(ServerConfig.prefTrustMincifryKey); _hostController.text = ServerConfig.defaultHost; _portController.text = '${ServerConfig.defaultPort}'; + _trustMincifryCa = ServerConfig.defaultTrustMincifryCa; await api.disconnect(); api.connect(); final online = await api.stateStream @@ -154,6 +159,49 @@ class _ServerSettingsSheetState extends State { inputFormatters: [FilteringTextInputFormatter.digitsOnly], enabled: !_busy, ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.fromLTRB(14, 12, 10, 12), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.serverTrustMincifryTitle, + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + l10n.serverTrustMincifrySubtitle, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12.5, + height: 1.3, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + Switch( + value: _trustMincifryCa, + onChanged: _busy + ? null + : (v) => setState(() => _trustMincifryCa = v), + ), + ], + ), + ), const SizedBox(height: 24), FilledButton( onPressed: _busy ? null : () => _apply(l10n), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4cbc7b9..864f655 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -18,6 +18,8 @@ "serverSettingsTitle": "Server", "serverHostLabel": "Host", "serverPortLabel": "Port", + "serverTrustMincifryTitle": "Trust the Минцифры CA", + "serverTrustMincifrySubtitle": "Required for api2.oneme.ru: its certificate chains to the Russian Trusted Root CA, which is absent from the standard trust store. The root is bundled with the app; other hosts keep using the usual roots.", "serverApply": "Apply and reconnect", "serverUseDefault": "Reset to default", "serverInvalidHostOrPort": "Enter a valid host and port (1–65535)", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 0340cdc..44c1d69 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -206,6 +206,18 @@ abstract class AppLocalizations { /// **'Port'** String get serverPortLabel; + /// No description provided for @serverTrustMincifryTitle. + /// + /// In en, this message translates to: + /// **'Trust the Минцифры CA'** + String get serverTrustMincifryTitle; + + /// No description provided for @serverTrustMincifrySubtitle. + /// + /// In en, this message translates to: + /// **'Required for api2.oneme.ru: its certificate chains to the Russian Trusted Root CA, which is absent from the standard trust store. The root is bundled with the app; other hosts keep using the usual roots.'** + String get serverTrustMincifrySubtitle; + /// No description provided for @serverApply. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 8f15f52..78e2cdd 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -63,6 +63,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get serverPortLabel => 'Port'; + @override + String get serverTrustMincifryTitle => 'Trust the Минцифры CA'; + + @override + String get serverTrustMincifrySubtitle => + 'Required for api2.oneme.ru: its certificate chains to the Russian Trusted Root CA, which is absent from the standard trust store. The root is bundled with the app; other hosts keep using the usual roots.'; + @override String get serverApply => 'Apply and reconnect'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 2b69149..bf26399 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -64,6 +64,13 @@ class AppLocalizationsRu extends AppLocalizations { @override String get serverPortLabel => 'Порт'; + @override + String get serverTrustMincifryTitle => 'Доверять сертификату Минцифры'; + + @override + String get serverTrustMincifrySubtitle => + 'Нужно для api2.oneme.ru: его сертификат выпущен под корнем Russian Trusted Root CA, которого нет в обычном хранилище. Корень зашит в приложение, остальные хосты проверяются как раньше.'; + @override String get serverApply => 'Применить и переподключиться'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 3969268..558e5d6 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -18,6 +18,8 @@ "serverSettingsTitle": "Сервер", "serverHostLabel": "Хост", "serverPortLabel": "Порт", + "serverTrustMincifryTitle": "Доверять сертификату Минцифры", + "serverTrustMincifrySubtitle": "Нужно для api2.oneme.ru: его сертификат выпущен под корнем Russian Trusted Root CA, которого нет в обычном хранилище. Корень зашит в приложение, остальные хосты проверяются как раньше.", "serverApply": "Применить и переподключиться", "serverUseDefault": "Сбросить к умолчанию", "serverInvalidHostOrPort": "Укажите корректный хост и порт (1–65535)", diff --git a/lib/main.dart b/lib/main.dart index 1490a09..894da9f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -174,6 +174,7 @@ void _installLogCapture() { void main(List args) async { WidgetsFlutterBinding.ensureInitialized(); await initKolibri(); + await TlsConfig.applyMincifryTrust(); DebugTest.parse(args); _installLogCapture(); VideoPlayerMediaKit.ensureInitialized( diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ec4ddd1..4b25fca 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -86,6 +86,8 @@ PODS: - GoogleUtilities/Privacy - kolibri (0.0.1): - FlutterMacOS + - komet_crypto (0.0.1): + - FlutterMacOS - media_kit_libs_macos_video (1.0.4): - FlutterMacOS - media_kit_video (0.0.1): @@ -143,6 +145,7 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) - kolibri (from `Flutter/ephemeral/.symlinks/plugins/kolibri/macos`) + - komet_crypto (from `Flutter/ephemeral/.symlinks/plugins/komet_crypto/macos`) - media_kit_libs_macos_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos`) - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - mobile_scanner (from `Flutter/ephemeral/.symlinks/plugins/mobile_scanner/darwin`) @@ -201,6 +204,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin kolibri: :path: Flutter/ephemeral/.symlinks/plugins/kolibri/macos + komet_crypto: + :path: Flutter/ephemeral/.symlinks/plugins/komet_crypto/macos media_kit_libs_macos_video: :path: Flutter/ephemeral/.symlinks/plugins/media_kit_libs_macos_video/macos media_kit_video: @@ -252,6 +257,7 @@ SPEC CHECKSUMS: GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 4f2618a4a1e762a1ee134a1e2323bba9843e06da kolibri: 93062ece67f68ec0b909876b527aa15e198b4a73 + komet_crypto: 856fa27dc180350f88a7cf6de6b512d2d221b737 media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93 diff --git a/pubspec.lock b/pubspec.lock index 0739da1..7139c80 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -601,10 +601,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.1.0" geolocator: dependency: "direct main" description: @@ -760,10 +760,11 @@ packages: kolibri: dependency: "direct main" description: - path: "third_party/kolibri/kolibri-dart" - relative: true - source: path - version: "0.1.0" + name: kolibri + sha256: e8ed4eab5687204a77d449743ceb73b0797f5702afb0a7841610a127fc102710 + url: "https://pub.dev" + source: hosted + version: "0.1.2" komet_crypto: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 17411ca..b3e1721 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,9 +35,9 @@ dependencies: intl: any # Rust networking core (kolibri) — FFI plugin, replaces the Dart transport. - # Pinned to the third_party/kolibri submodule. - kolibri: - path: third_party/kolibri/kolibri-dart + # Published from the KometTeam/kolibri repo; the native core is compiled at + # app build time and pulled from that repo by git tag. + kolibri: ^0.1.2 # Rust message-encryption core — Argon2id + ChaCha20-Poly1305, output encoded # as lowercase Cyrillic base32. Separate from kolibri: that is vendored diff --git a/third_party/kolibri b/third_party/kolibri deleted file mode 160000 index 1aa610f..0000000 --- a/third_party/kolibri +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1aa610f55b577fc299c713522576d5c67b65b6af