From 4d39eb0bf4e813685c49abb58bd6c4597c87b121 Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 12 May 2026 22:55:56 +0200 Subject: [PATCH] Seamless recovery for iOS reinstallations --- CHANGELOG.md | 1 + lib/app.dart | 21 ++- lib/core/bridge/wrapper/key_manager.dart | 11 ++ lib/core/frb_generated.dart | 119 +++++++++++- lib/main.dart | 23 ++- .../generated/app_localizations.dart | 36 ++++ .../generated/app_localizations_de.dart | 21 +++ .../generated/app_localizations_en.dart | 21 +++ lib/src/localization/translations | 2 +- lib/src/services/backup.service.dart | 40 ++-- lib/src/services/user.service.dart | 15 +- lib/src/visual/views/recovery.view.dart | 178 ++++++++++++++++++ rust/src/bridge/wrapper/key_manager.rs | 19 ++ rust/src/frb_generated.rs | 107 ++++++++++- rust/src/keys/mod.rs | 6 + rust/src/secure_storage.rs | 22 +-- 16 files changed, 594 insertions(+), 48 deletions(-) create mode 100644 lib/src/visual/views/recovery.view.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 3246dbbb..3fd151a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.2.11 +- New: Seamless recovery for iOS reinstallations - Improved: Redesigned snackbar notifications - Improved: New backup mechanism to allow larger backup files - Improved: Move keys into a centralized Rust-owned structure stored in secure storage diff --git a/lib/app.dart b/lib/app.dart index f24a3471..43b63dfb 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -15,14 +15,20 @@ import 'package:twonly/src/visual/themes/dark.dart'; import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/visual/views/critical_error.view.dart'; import 'package:twonly/src/visual/views/home.view.dart'; +import 'package:twonly/src/visual/views/recovery.view.dart'; import 'package:twonly/src/visual/views/onboarding/onboarding.view.dart'; import 'package:twonly/src/visual/views/onboarding/register.view.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/unlock_twonly.view.dart'; class App extends StatefulWidget { - const App({required this.storageError, super.key}); + const App({ + required this.storageError, + required this.recoveryPossible, + super.key, + }); final bool storageError; + final bool recoveryPossible; @override State createState() => _AppState(); } @@ -88,6 +94,19 @@ class _AppState extends State with WidgetsBindingObserver { ); } + if (widget.recoveryPossible) { + return MaterialApp( + localizationsDelegates: localizationsDelegates, + debugShowCheckedModeBanner: false, + supportedLocales: supportedLocales, + title: 'twonly', + theme: lightTheme, + darkTheme: darkTheme, + themeMode: context.read().themeMode, + home: const RecoveryView(), + ); + } + return MaterialApp.router( routerConfig: routerProvider, localizationsDelegates: localizationsDelegates, diff --git a/lib/core/bridge/wrapper/key_manager.dart b/lib/core/bridge/wrapper/key_manager.dart index 5cf8bc95..46c57e90 100644 --- a/lib/core/bridge/wrapper/key_manager.dart +++ b/lib/core/bridge/wrapper/key_manager.dart @@ -17,6 +17,9 @@ class RustKeyManager { .api .crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity(); + static Future getUserId() => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerGetUserId(); + static Future importSignalIdentity({ required List identityKeyPairStructure, required PlatformInt64 registrationId, @@ -40,6 +43,9 @@ class RustKeyManager { .api .crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys(); + static Future removeKeyManager() => RustLib.instance.api + .crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager(); + static Future removeSignedPrekey({ required PlatformInt64 signedPreKeyId, }) => RustLib.instance.api @@ -47,6 +53,11 @@ class RustKeyManager { signedPreKeyId: signedPreKeyId, ); + static Future setUserId({required PlatformInt64 userId}) => RustLib + .instance + .api + .crateBridgeWrapperKeyManagerRustKeyManagerSetUserId(userId: userId); + static Future storeSignedPrekey({ required PlatformInt64 signedPreKeyId, required List record, diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 277cc034..ae786047 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -74,7 +74,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 1215442517; + int get rustContentHash => -1867463121; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -200,6 +200,8 @@ abstract class RustLibApi extends BaseApi { Future<(Uint8List, PlatformInt64)> crateBridgeWrapperKeyManagerRustKeyManagerGetSignalIdentity(); + Future crateBridgeWrapperKeyManagerRustKeyManagerGetUserId(); + Future crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({ required List identityKeyPairStructure, required PlatformInt64 registrationId, @@ -214,10 +216,16 @@ abstract class RustLibApi extends BaseApi { Future> crateBridgeWrapperKeyManagerRustKeyManagerLoadSignedPrekeys(); + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager(); + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey({ required PlatformInt64 signedPreKeyId, }); + Future crateBridgeWrapperKeyManagerRustKeyManagerSetUserId({ + required PlatformInt64 userId, + }); + Future crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey({ required PlatformInt64 signedPreKeyId, required List record, @@ -1035,6 +1043,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: [], ); + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerGetUserId() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 20, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_opt_box_autoadd_i_64, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerGetUserIdConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerGetUserIdConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_get_user_id", + argNames: [], + ); + @override Future crateBridgeWrapperKeyManagerRustKeyManagerImportSignalIdentity({ required List identityKeyPairStructure, @@ -1054,7 +1094,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 20, + funcId: 21, port: port_, ); }, @@ -1098,7 +1138,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 21, + funcId: 22, port: port_, ); }, @@ -1131,7 +1171,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 22, + funcId: 23, port: port_, ); }, @@ -1154,6 +1194,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: [], ); + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManager() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 24, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManagerConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerRemoveKeyManagerConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_remove_key_manager", + argNames: [], + ); + @override Future crateBridgeWrapperKeyManagerRustKeyManagerRemoveSignedPrekey({ required PlatformInt64 signedPreKeyId, @@ -1166,7 +1238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 23, + funcId: 25, port: port_, ); }, @@ -1189,6 +1261,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["signedPreKeyId"], ); + @override + Future crateBridgeWrapperKeyManagerRustKeyManagerSetUserId({ + required PlatformInt64 userId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_i_64(userId, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 26, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_unit, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateBridgeWrapperKeyManagerRustKeyManagerSetUserIdConstMeta, + argValues: [userId], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateBridgeWrapperKeyManagerRustKeyManagerSetUserIdConstMeta => + const TaskConstMeta( + debugName: "rust_key_manager_set_user_id", + argNames: ["userId"], + ); + @override Future crateBridgeWrapperKeyManagerRustKeyManagerStoreSignedPrekey({ required PlatformInt64 signedPreKeyId, @@ -1203,7 +1310,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 24, + funcId: 27, port: port_, ); }, diff --git a/lib/main.dart b/lib/main.dart index 45c99a06..d9c5b744 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:mutex/mutex.dart'; @@ -90,6 +88,8 @@ void main() async { var userExists = false; + var recoveryPossible = false; + if (!storageError) { try { userExists = await userService.tryInit(); @@ -99,12 +99,14 @@ void main() async { } } - if (Platform.isIOS && userExists) { - final dbFile = File('${AppEnvironment.supportDir}/twonly.sqlite'); - if (!dbFile.existsSync()) { - Log.error('[twonly] IOS: App was removed and then reinstalled again...'); - await SecureStorage.instance.deleteAll(); - userExists = false; + if (!userExists && !storageError) { + try { + final userId = await RustKeyManager.getUserId(); + if (userId != null) { + recoveryPossible = true; + } + } catch (e) { + Log.error('Could not check KeyManager userId for iOS recovery: $e'); } } @@ -152,7 +154,10 @@ void main() async { ChangeNotifierProvider(create: (_) => ImageEditorProvider()), ChangeNotifierProvider(create: (_) => PurchasesProvider()), ], - child: App(storageError: storageError), + child: App( + storageError: storageError, + recoveryPossible: recoveryPossible, + ), ), ); } diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 42d2cda2..8684ef56 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -3079,6 +3079,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Click here to open the app again'** String get recoverSuccessBody; + + /// No description provided for @iosRecoveryWelcomeBack. + /// + /// In en, this message translates to: + /// **'Welcome Back'** + String get iosRecoveryWelcomeBack; + + /// No description provided for @iosRecoveryPrompt. + /// + /// In en, this message translates to: + /// **'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?'** + String get iosRecoveryPrompt; + + /// No description provided for @iosRecoveryNoBackupFound. + /// + /// In en, this message translates to: + /// **'No backup archive could be retrieved from the server for this device.\n\nError: {error}\n\nPlease proceed to register a new twonly account.'** + String iosRecoveryNoBackupFound(Object error); + + /// No description provided for @registerNewAccount. + /// + /// In en, this message translates to: + /// **'Register New Account'** + String get registerNewAccount; + + /// No description provided for @tryRestoreAgain. + /// + /// In en, this message translates to: + /// **'Try Restore Again'** + String get tryRestoreAgain; + + /// No description provided for @registeringNewAccount. + /// + /// In en, this message translates to: + /// **'Registering new account'** + String get registeringNewAccount; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 4b5b9caf..db292824 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1736,4 +1736,25 @@ class AppLocalizationsDe extends AppLocalizations { @override String get recoverSuccessBody => 'Klicke hier, um die App wieder zu öffnen'; + + @override + String get iosRecoveryWelcomeBack => 'Willkommen zurück'; + + @override + String get iosRecoveryPrompt => + 'Wir haben eine zuvor gesicherte twonly-Identität auf diesem Gerät erkannt. Möchtest du deine Kontakte, Nachrichten und Einstellungen automatisch aus deinem Cloud-Archiv herunterladen und wiederherstellen?'; + + @override + String iosRecoveryNoBackupFound(Object error) { + return 'Für dieses Gerät konnte kein Backup-Archiv vom Server abgerufen werden.\n\nFehler: $error\n\nBitte fahre mit der Registrierung eines neuen twonly-Kontos fort.'; + } + + @override + String get registerNewAccount => 'Neues Konto registrieren'; + + @override + String get tryRestoreAgain => 'Wiederherstellung erneut versuchen'; + + @override + String get registeringNewAccount => 'Neues Konto wird registriert'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 65b8db55..7bd2a28f 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1721,4 +1721,25 @@ class AppLocalizationsEn extends AppLocalizations { @override String get recoverSuccessBody => 'Click here to open the app again'; + + @override + String get iosRecoveryWelcomeBack => 'Welcome Back'; + + @override + String get iosRecoveryPrompt => + 'We detected a previously secured twonly identity on this device. Would you like to automatically download and restore your contacts, messages, and settings from your cloud archive?'; + + @override + String iosRecoveryNoBackupFound(Object error) { + return 'No backup archive could be retrieved from the server for this device.\n\nError: $error\n\nPlease proceed to register a new twonly account.'; + } + + @override + String get registerNewAccount => 'Register New Account'; + + @override + String get tryRestoreAgain => 'Try Restore Again'; + + @override + String get registeringNewAccount => 'Registering new account'; } diff --git a/lib/src/localization/translations b/lib/src/localization/translations index 65bf6a4d..75b97e91 160000 --- a/lib/src/localization/translations +++ b/lib/src/localization/translations @@ -1 +1 @@ -Subproject commit 65bf6a4d161bfa0cd2db698446c58b4cd03db92c +Subproject commit 75b97e912f2e72a8e2a5da65e8ad12f0d1091855 diff --git a/lib/src/services/backup.service.dart b/lib/src/services/backup.service.dart index fb186044..ca314a23 100644 --- a/lib/src/services/backup.service.dart +++ b/lib/src/services/backup.service.dart @@ -7,6 +7,7 @@ import 'package:clock/clock.dart' as clock; import 'package:http/http.dart' as http; import 'package:mutex/mutex.dart'; import 'package:twonly/core/bridge/wrapper/backup.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/keyvalue.keys.dart'; @@ -102,23 +103,23 @@ class BackupService { backup.identityLastSuccessFull!.isBefore( lastWeek.subtract(const Duration(days: 1)), ))) { - Log.info('Performing a identity backup.'); - final encryptedBackup = - await RustBackupIdentity.getIdentityBackupBytes(); - - final backupTempFile = File( - '${AppEnvironment.cacheDir}/identity_backup.bin', - )..writeAsBytesSync(encryptedBackup); - - Log.info( - 'Identity backup has a size of ${backupTempFile.statSync().size}.', - ); - final backupId = await RustBackupIdentity.getBackupId(); if (backupId == null) { - Log.error('Got empty backup id.'); + Log.error('No backup password was set by the user.'); backup.identityState = LastBackupUploadState.failed; } else { + Log.info('Performing a identity backup.'); + final encryptedBackup = + await RustBackupIdentity.getIdentityBackupBytes(); + + final backupTempFile = File( + '${AppEnvironment.cacheDir}/identity_backup.bin', + )..writeAsBytesSync(encryptedBackup); + + Log.info( + 'Identity backup has a size of ${backupTempFile.statSync().size}.', + ); + final task = UploadTask.fromFile( taskId: 'backup_identity', httpRequestMethod: 'PUT', @@ -290,6 +291,19 @@ class BackupService { }); } + static Future tryToReinstallTheArchive() async { + final userId = await RustKeyManager.getUserId(); + if (userId == null) return null; + + final state = BackupRecovery( + username: '', + userId: userId, + password: '', + )..state = BackupRecoveryState.archiveBackupStarted; + await KeyValueStore.put(KeyValueKeys.backupRecoveryState, state.toJson()); + return _nextBackupStage(); + } + static Future startFullBackupRecovery( String username, String password, diff --git a/lib/src/services/user.service.dart b/lib/src/services/user.service.dart index 4c551463..700e01bb 100644 --- a/lib/src/services/user.service.dart +++ b/lib/src/services/user.service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:mutex/mutex.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/secure_storage.keys.dart'; import 'package:twonly/src/model/json/userdata.model.dart'; @@ -30,7 +31,9 @@ class UserService { // 1. Try to load from KeyValueStore (user.json) final userDataMap = await KeyValueStore.get('user'); if (userDataMap != null) { - return UserData.fromJson(userDataMap); + final userData = UserData.fromJson(userDataMap); + await RustKeyManager.setUserId(userId: userData.userId); + return userData; } // 2. If not found, try to load from SecureStorage (Migration path) @@ -58,6 +61,11 @@ class UserService { static Future _migrateFromSecureStorage(UserData userData) async { // Currently empty migration logic as requested, but we MUST store the data await KeyValueStore.put('user', userData.toJson()); + try { + await RustKeyManager.setUserId(userId: userData.userId); + } catch (e) { + Log.error('Could not set userId in RustKeyManager during migration: $e'); + } // Optional: Log migration Log.info('Migrated user data from SecureStorage to KeyValueStore'); @@ -87,6 +95,11 @@ class UserService { static Future save(UserData user) async { await KeyValueStore.put('user', user.toJson()); + try { + await RustKeyManager.setUserId(userId: user.userId); + } catch (e) { + Log.error('Could not set userId in RustKeyManager during save: $e'); + } await userService.tryInit(); } diff --git a/lib/src/visual/views/recovery.view.dart b/lib/src/visual/views/recovery.view.dart new file mode 100644 index 00000000..0708262d --- /dev/null +++ b/lib/src/visual/views/recovery.view.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:twonly/core/bridge/wrapper/key_manager.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/backup.service.dart'; +import 'package:twonly/src/utils/log.dart'; +import 'package:twonly/src/utils/misc.dart'; +import 'package:twonly/src/utils/storage.dart'; + +class RecoveryView extends StatefulWidget { + const RecoveryView({super.key}); + + @override + State createState() => _RecoveryViewState(); +} + +class _RecoveryViewState extends State { + bool _isLoading = false; + String? _errorMessage; + bool _showRegisterNewPrompt = false; + + Future _startRestore() async { + setState(() { + _isLoading = true; + _errorMessage = null; + }); + + final error = await BackupService.tryToReinstallTheArchive(); + if (!mounted) return; + + if (error != null) { + String msg; + switch (error) { + case RecoveryError.noInternet: + msg = context.lang.recoverErrorNoInternet; + case RecoveryError.usernameNotValid: + msg = context.lang.recoverErrorUsernameNotValid; + case RecoveryError.passwordInvalid: + msg = context.lang.recoverErrorPasswordInvalid; + case RecoveryError.tryAgainLater: + msg = context.lang.recoverErrorTryAgainLater; + case RecoveryError.unkownError: + msg = context.lang.recoverErrorUnknown; + } + setState(() { + _isLoading = false; + _errorMessage = msg; + _showRegisterNewPrompt = true; + }); + return; + } + + final userExists = await userService.tryInit(); + if (userExists && mounted) { + await Restart.restartApp( + notificationTitle: context.lang.recoverSuccessTitle, + notificationBody: context.lang.recoverSuccessBody, + forceKill: true, + ); + } else { + setState(() { + _isLoading = false; + _errorMessage = context.lang.recoverErrorUnknown; + _showRegisterNewPrompt = true; + }); + } + } + + Future _registerNewAccount() async { + try { + await RustKeyManager.removeKeyManager(); + } catch (e) { + Log.error('Could not remove KeyManager during account reset: $e'); + } + await deleteLocalUserData(); + if (!mounted) return; + await Restart.restartApp( + notificationTitle: 'twonly', + notificationBody: context.lang.registeringNewAccount, + forceKill: true, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.lang.twonlySafeRecoverTitle), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: ListView( + children: [ + const SizedBox(height: 100), + Center( + child: FaIcon( + FontAwesomeIcons.cloudArrowDown, + size: 80, + color: context.color.primary, + ), + ), + const SizedBox(height: 24), + Text( + context.lang.iosRecoveryWelcomeBack, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + _showRegisterNewPrompt + ? context.lang.iosRecoveryNoBackupFound( + _errorMessage ?? '', + ) + : context.lang.iosRecoveryPrompt, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 15), + ), + const SizedBox(height: 32), + if (!_showRegisterNewPrompt) ...[ + FilledButton.icon( + onPressed: _isLoading ? null : _startRestore, + icon: _isLoading + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.restore_rounded), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + ), + label: Text( + context.lang.twonlySafeRecoverBtn, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + TextButton( + onPressed: _isLoading ? null : _registerNewAccount, + child: Text(context.lang.registerNewAccount), + ), + ] else ...[ + FilledButton.icon( + onPressed: _registerNewAccount, + icon: const Icon(Icons.person_add_rounded), + style: FilledButton.styleFrom( + backgroundColor: Colors.redAccent, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 12, + ), + ), + label: Text( + context.lang.registerNewAccount, + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: _startRestore, + icon: const Icon(Icons.refresh_rounded), + label: Text(context.lang.tryRestoreAgain), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/rust/src/bridge/wrapper/key_manager.rs b/rust/src/bridge/wrapper/key_manager.rs index d18d61a2..d7ba4b14 100644 --- a/rust/src/bridge/wrapper/key_manager.rs +++ b/rust/src/bridge/wrapper/key_manager.rs @@ -12,6 +12,19 @@ impl RustKeyManager { Ok(key_manager.main_key.get_login_token().to_vec()) } + pub async fn get_user_id() -> Result> { + let key_manager = get_twonly_flutter()?.key_manager.lock().await; + Ok(key_manager.user_id) + } + + pub async fn set_user_id(user_id: i64) -> Result<()> { + let ctx = get_twonly_flutter()?; + let mut key_manager = ctx.key_manager.lock().await; + key_manager.user_id = Some(user_id); + key_manager.store_to_keychain(&ctx.secure_storage)?; + Ok(()) + } + pub async fn import_signal_identity( identity_key_pair_structure: Vec, registration_id: i64, @@ -89,4 +102,10 @@ impl RustKeyManager { Err(TwonlyError::SignalIdentityNotFound) } } + + pub async fn remove_key_manager() -> Result<()> { + let ctx = get_twonly_flutter()?; + crate::keys::KeyManager::remove_from_keychain(&ctx.secure_storage)?; + Ok(()) + } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index f134f3a5..0be3e628 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -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 = 1215442517; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1867463121; // Section: executor @@ -424,6 +424,43 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identi })().await) } }) } +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_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::TaskInfo { + debug_name: "rust_key_manager_get_user_id", + 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); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::wrapper::key_manager::RustKeyManager::get_user_id() + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -471,6 +508,21 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_preke })().await) } }) } +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_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::TaskInfo{ debug_name: "rust_key_manager_remove_key_manager", 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); + deserializer.end(); move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move { + let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::remove_key_manager().await?; Ok(output_ok) + })().await) + } }) +} fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -486,6 +538,46 @@ fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_pre })().await) } }) } +fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_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::TaskInfo { + debug_name: "rust_key_manager_set_user_id", + 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_user_id = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::bridge::wrapper::key_manager::RustKeyManager::set_user_id( + api_user_id, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -1327,11 +1419,14 @@ fn pde_ffi_dispatcher_primary_impl( 17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), 19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), -20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), -21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), -23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), -24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), +21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), +22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), +24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), +25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), +26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), +27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), _ => unreachable!(), } } diff --git a/rust/src/keys/mod.rs b/rust/src/keys/mod.rs index 01230f14..2fcfedc1 100644 --- a/rust/src/keys/mod.rs +++ b/rust/src/keys/mod.rs @@ -53,4 +53,10 @@ impl KeyManager { Ok(()) } + + /// Removes the KeyManager from the secure keychain/local storage. + pub fn remove_from_keychain(storage: &SecureStorage) -> Result<()> { + storage.delete(KEY_MANAGER_ID)?; + Ok(()) + } } diff --git a/rust/src/secure_storage.rs b/rust/src/secure_storage.rs index 7fd08870..0d44de4a 100644 --- a/rust/src/secure_storage.rs +++ b/rust/src/secure_storage.rs @@ -92,15 +92,15 @@ impl SecureStorage { /// Deletes the secret associated with the given key from the secure keyring. /// /// If the key does not exist, this function returns `Ok(())` (idempotent). - // pub fn delete(&self, key: &str) -> Result<(), String> { - // let entry = self.get_entry(key)?; + pub fn delete(&self, key: &str) -> Result<(), String> { + let entry = self.get_entry(key)?; - // match entry.delete_credential() { - // Ok(()) => Ok(()), - // Err(KeyringError::NoEntry) => Ok(()), - // Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)), - // } - // } + match entry.delete_credential() { + Ok(()) => Ok(()), + Err(KeyringError::NoEntry) => Ok(()), + Err(e) => Err(format!("Failed to delete secret from keyring: {}", e)), + } + } /// Helper to create a keyring entry with the appropriate platform modifiers. fn get_entry(&self, key: &str) -> Result { @@ -142,10 +142,10 @@ mod tests { assert_eq!(read_val, Some(secret.to_string())); // 3. Delete the secret - // storage.delete(key).expect("Failed to delete secret"); + storage.delete(key).expect("Failed to delete secret"); // 4. Verify the secret is gone - // let after_delete = storage.read(key).expect("Failed to read after delete"); - // assert_eq!(after_delete, None); + let after_delete = storage.read(key).expect("Failed to read after delete"); + assert_eq!(after_delete, None); } }