From f914263b2f34ec5d63c53b6361617455ee87abd4 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 1 Aug 2025 11:58:22 +0530 Subject: [PATCH] Remove old password reentry, recovery key, recovery, and password verification pages; refactor imports and update references in settings and home page. --- mobile/apps/auth/lib/app/view/app.dart | 2 +- mobile/apps/auth/lib/main.dart | 5 +- .../lib/onboarding/view/onboarding_page.dart | 12 +- .../auth/lib/services/passkey_service.dart | 57 - .../apps/auth/lib/services/user_service.dart | 1082 ----------------- .../lib/ui/account/password_reentry_page.dart | 326 ----- .../lib/ui/account/recovery_key_page.dart | 355 ------ .../auth/lib/ui/account/recovery_page.dart | 164 --- .../request_pwd_verification_page.dart | 219 ---- mobile/apps/auth/lib/ui/home_page.dart | 2 +- .../ui/settings/account_section_widget.dart | 5 +- .../settings/notification_banner_widget.dart | 4 +- .../ui/settings/security_section_widget.dart | 7 +- mobile/apps/auth/lib/ui/settings_page.dart | 2 +- 14 files changed, 24 insertions(+), 2218 deletions(-) delete mode 100644 mobile/apps/auth/lib/services/passkey_service.dart delete mode 100644 mobile/apps/auth/lib/services/user_service.dart delete mode 100644 mobile/apps/auth/lib/ui/account/password_reentry_page.dart delete mode 100644 mobile/apps/auth/lib/ui/account/recovery_key_page.dart delete mode 100644 mobile/apps/auth/lib/ui/account/recovery_page.dart delete mode 100644 mobile/apps/auth/lib/ui/account/request_pwd_verification_page.dart diff --git a/mobile/apps/auth/lib/app/view/app.dart b/mobile/apps/auth/lib/app/view/app.dart index aee7b610b5..833e502975 100644 --- a/mobile/apps/auth/lib/app/view/app.dart +++ b/mobile/apps/auth/lib/app/view/app.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:ente_accounts/services/user_service.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ente_theme_data.dart'; import "package:ente_auth/l10n/l10n.dart"; @@ -9,7 +10,6 @@ import 'package:ente_auth/locale.dart'; import "package:ente_auth/onboarding/view/onboarding_page.dart"; import 'package:ente_auth/services/authenticator_service.dart'; import 'package:ente_auth/services/update_service.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/services/window_listener_service.dart'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/settings/app_update_dialog.dart'; diff --git a/mobile/apps/auth/lib/main.dart b/mobile/apps/auth/lib/main.dart index 94cdb778ca..52ad328bbe 100644 --- a/mobile/apps/auth/lib/main.dart +++ b/mobile/apps/auth/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:ente_accounts/services/user_service.dart'; import "package:ente_auth/app/view/app.dart"; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/constants.dart'; @@ -13,11 +14,11 @@ import 'package:ente_auth/services/billing_service.dart'; import 'package:ente_auth/services/notification_service.dart'; import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/services/update_service.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/services/window_listener_service.dart'; import 'package:ente_auth/store/authenticator_db.dart'; import 'package:ente_auth/store/code_display_store.dart'; import 'package:ente_auth/store/code_store.dart'; +import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/utils/icon_utils.dart'; import 'package:ente_auth/utils/directory_utils.dart'; import 'package:ente_auth/utils/platform_util.dart'; @@ -162,7 +163,7 @@ Future _init(bool bool, {String? via}) async { await CodeDisplayStore.instance.init(); await Configuration.instance.init([AuthenticatorDB.instance]); await Network.instance.init(Configuration.instance); - await UserService.instance.init(); + await UserService.instance.init(Configuration.instance, const HomePage()); await AuthenticatorService.instance.init(); await BillingService.instance.init(); await NotificationService.instance.init(); diff --git a/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart b/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart index c9fcd645f5..5dc8d92763 100644 --- a/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart +++ b/mobile/apps/auth/lib/onboarding/view/onboarding_page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:ente_accounts/pages/email_entry_page.dart'; import 'package:ente_accounts/pages/login_page.dart'; import 'package:ente_accounts/pages/password_entry_page.dart'; +import 'package:ente_accounts/pages/password_reentry_page.dart'; import 'package:ente_auth/app/view/app.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ente_theme_data.dart'; @@ -12,7 +13,6 @@ import "package:ente_auth/l10n/l10n.dart"; import 'package:ente_auth/locale.dart'; import 'package:ente_auth/theme/text_style.dart'; import 'package:ente_auth/ui/account/logout_dialog.dart'; -import 'package:ente_auth/ui/account/password_reentry_page.dart'; import 'package:ente_auth/ui/common/gradient_button.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/models/button_result.dart'; @@ -272,7 +272,10 @@ class _OnboardingPageState extends State { ); } else if (Configuration.instance.getKey() == null) { // Yet to decrypt the key - page = const PasswordReentryPage(); + page = PasswordReentryPage( + Configuration.instance, + const HomePage(), + ); } else { // All is well, user just has not subscribed page = const HomePage(); @@ -302,7 +305,10 @@ class _OnboardingPageState extends State { ); } else if (Configuration.instance.getKey() == null) { // Yet to decrypt the key - page = const PasswordReentryPage(); + page = PasswordReentryPage( + Configuration.instance, + const HomePage(), + ); } else { // All is well, user just has not subscribed // page = getSubscriptionPage(isOnBoarding: true); diff --git a/mobile/apps/auth/lib/services/passkey_service.dart b/mobile/apps/auth/lib/services/passkey_service.dart deleted file mode 100644 index 20fd037f3f..0000000000 --- a/mobile/apps/auth/lib/services/passkey_service.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'package:ente_auth/core/constants.dart'; -import 'package:ente_auth/utils/dialog_util.dart'; -import 'package:ente_network/network.dart'; -import 'package:flutter/widgets.dart'; -import 'package:logging/logging.dart'; -import 'package:url_launcher/url_launcher_string.dart'; - -class PasskeyService { - PasskeyService._privateConstructor(); - static final PasskeyService instance = PasskeyService._privateConstructor(); - - final _enteDio = Network.instance.enteDio; - - Future getAccountsUrl() async { - final response = await _enteDio.get( - "/users/accounts-token", - ); - final accountsUrl = response.data!["accountsUrl"] ?? kAccountsUrl; - final jwtToken = response.data!["accountsToken"] as String; - return "$accountsUrl/passkeys?token=$jwtToken"; - } - - Future isPasskeyRecoveryEnabled() async { - final response = await _enteDio.get( - "/users/two-factor/recovery-status", - ); - return response.data!["isPasskeyRecoveryEnabled"] as bool; - } - - Future configurePasskeyRecovery( - String secret, - String userEncryptedSecret, - String userSecretNonce, - ) async { - await _enteDio.post( - "/users/two-factor/passkeys/configure-recovery", - data: { - "secret": secret, - "userSecretCipher": userEncryptedSecret, - "userSecretNonce": userSecretNonce, - }, - ); - } - - Future openPasskeyPage(BuildContext context) async { - try { - final url = await getAccountsUrl(); - await launchUrlString( - url, - mode: LaunchMode.externalApplication, - ); - } catch (e) { - Logger('PasskeyService').severe("failed to open passkey page", e); - showGenericErrorDialog(context: context, error: e).ignore(); - } - } -} diff --git a/mobile/apps/auth/lib/services/user_service.dart b/mobile/apps/auth/lib/services/user_service.dart deleted file mode 100644 index cb676ad237..0000000000 --- a/mobile/apps/auth/lib/services/user_service.dart +++ /dev/null @@ -1,1082 +0,0 @@ -import 'dart:async'; -import "dart:convert"; -import "dart:math"; - -import 'package:bip39/bip39.dart' as bip39; -import 'package:dio/dio.dart'; -import 'package:ente_accounts/models/delete_account.dart'; -import 'package:ente_accounts/models/sessions.dart'; -import 'package:ente_accounts/models/set_keys_request.dart'; -import 'package:ente_accounts/models/set_recovery_key_request.dart'; -import 'package:ente_accounts/models/two_factor.dart'; -import 'package:ente_accounts/models/user_details.dart'; -import 'package:ente_accounts/pages/login_page.dart'; -import 'package:ente_accounts/pages/ott_verification_page.dart'; -import 'package:ente_accounts/pages/passkey_page.dart'; -import 'package:ente_accounts/pages/password_entry_page.dart'; -import 'package:ente_accounts/pages/two_factor_authentication_page.dart'; -import 'package:ente_accounts/pages/two_factor_recovery_page.dart'; -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/core/constants.dart'; -import 'package:ente_auth/core/errors.dart'; -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/models/api/user/srp.dart'; -import 'package:ente_auth/ui/account/password_reentry_page.dart'; -import 'package:ente_auth/ui/account/recovery_page.dart'; -import 'package:ente_auth/ui/common/progress_dialog.dart'; -import 'package:ente_auth/ui/home_page.dart'; -import 'package:ente_auth/utils/dialog_util.dart'; -import 'package:ente_auth/utils/toast_util.dart'; -import 'package:ente_base/models/key_attributes.dart'; -import 'package:ente_base/models/key_gen_result.dart'; -import 'package:ente_crypto_dart/ente_crypto_dart.dart'; -import 'package:ente_events/event_bus.dart'; -import 'package:ente_events/models/user_details_changed_event.dart'; -import 'package:ente_network/network.dart'; -import "package:flutter/foundation.dart"; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import "package:pointycastle/export.dart"; -import "package:pointycastle/srp/srp6_client.dart"; -import "package:pointycastle/srp/srp6_standard_groups.dart"; -import "package:pointycastle/srp/srp6_util.dart"; -import "package:pointycastle/srp/srp6_verifier_generator.dart"; -import 'package:shared_preferences/shared_preferences.dart'; -import "package:uuid/uuid.dart"; - -class UserService { - static const keyHasEnabledTwoFactor = "has_enabled_two_factor"; - static const keyUserDetails = "user_details"; - static const kReferralSource = "referral_source"; - static const kCanDisableEmailMFA = "can_disable_email_mfa"; - static const kIsEmailMFAEnabled = "is_email_mfa_enabled"; - final SRP6GroupParameters kDefaultSrpGroup = SRP6StandardGroups.rfc5054_4096; - final _dio = Network.instance.getDio(); - final _enteDio = Network.instance.enteDio; - final _logger = Logger((UserService).toString()); - final _config = Configuration.instance; - late SharedPreferences _preferences; - - late ValueNotifier emailValueNotifier; - - UserService._privateConstructor(); - - static final UserService instance = UserService._privateConstructor(); - - Future init() async { - emailValueNotifier = - ValueNotifier(Configuration.instance.getEmail()); - _preferences = await SharedPreferences.getInstance(); - } - - Future sendOtt( - BuildContext context, - String email, { - bool isChangeEmail = false, - bool isCreateAccountScreen = false, - bool isResetPasswordScreen = false, - String? purpose, - }) async { - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - try { - final response = await _dio.post( - "${_config.getHttpEndpoint()}/users/ott", - data: { - "email": email, - "purpose": isChangeEmail ? "change" : purpose ?? "", - }, - ); - await dialog.hide(); - if (response.statusCode == 200) { - unawaited( - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return OTTVerificationPage( - email, - isChangeEmail: isChangeEmail, - isCreateAccountScreen: isCreateAccountScreen, - isResetPasswordScreen: isResetPasswordScreen, - ); - }, - ), - ), - ); - return; - } - unawaited(showGenericErrorDialog(context: context, error: null)); - } on DioException catch (e) { - await dialog.hide(); - _logger.info(e); - final String? enteErrCode = e.response?.data["code"]; - if (enteErrCode != null && enteErrCode == "USER_ALREADY_REGISTERED") { - unawaited( - showErrorDialog( - context, - context.l10n.oops, - context.l10n.emailAlreadyRegistered, - ), - ); - } else if (enteErrCode != null && enteErrCode == "USER_NOT_REGISTERED") { - unawaited( - showErrorDialog( - context, - context.l10n.oops, - context.l10n.emailNotRegistered, - ), - ); - } else if (e.response != null && e.response!.statusCode == 403) { - unawaited( - showErrorDialog( - context, - context.l10n.oops, - context.l10n.thisEmailIsAlreadyInUse, - ), - ); - } else { - unawaited(showGenericErrorDialog(context: context, error: e)); - } - } catch (e) { - await dialog.hide(); - _logger.severe(e); - unawaited(showGenericErrorDialog(context: context, error: e)); - } - } - - Future sendFeedback( - BuildContext context, - String feedback, { - String type = "SubCancellation", - }) async { - await _dio.post( - "${_config.getHttpEndpoint()}/anonymous/feedback", - data: {"feedback": feedback, "type": "type"}, - ); - } - - Future getUserDetailsV2({ - bool memoryCount = false, - bool shouldCache = true, - }) async { - try { - final response = await _enteDio.get( - "/users/details/v2", - queryParameters: { - "memoryCount": memoryCount, - }, - ); - final userDetails = UserDetails.fromMap(response.data); - if (shouldCache) { - if (userDetails.profileData != null) { - await _preferences.setBool( - kIsEmailMFAEnabled, - userDetails.profileData!.isEmailMFAEnabled, - ); - await _preferences.setBool( - kCanDisableEmailMFA, - userDetails.profileData!.canDisableEmailMFA, - ); - } - // handle email change from different client - if (userDetails.email != _config.getEmail()) { - await setEmail(userDetails.email); - } - } - return userDetails; - } catch (e) { - _logger.warning("Failed to fetch", e); - if (e is DioException && e.response?.statusCode == 401) { - throw UnauthorizedError(); - } else { - rethrow; - } - } - } - - Future getActiveSessions() async { - try { - final response = await _enteDio.get("/users/sessions"); - return Sessions.fromMap(response.data); - } on DioException catch (e) { - _logger.info(e); - rethrow; - } - } - - Future terminateSession(String token) async { - try { - await _enteDio.delete( - "/users/session", - queryParameters: { - "token": token, - }, - ); - } on DioException catch (e) { - _logger.info(e); - rethrow; - } - } - - Future leaveFamilyPlan() async { - try { - await _enteDio.delete("/family/leave"); - } on DioException catch (e) { - _logger.warning('failed to leave family plan', e); - rethrow; - } - } - - Future logout(BuildContext context) async { - try { - final response = await _enteDio.post("/users/logout"); - if (response.statusCode == 200) { - await Configuration.instance.logout(); - Navigator.of(context).popUntil((route) => route.isFirst); - } else { - throw Exception("Log out action failed"); - } - } catch (e) { - _logger.severe(e); - // check if token is already invalid - if (e is DioException && e.response?.statusCode == 401) { - await Configuration.instance.logout(); - Navigator.of(context).popUntil((route) => route.isFirst); - return; - } - //This future is for waiting for the dialog from which logout() is called - //to close and only then to show the error dialog. - Future.delayed( - const Duration(milliseconds: 150), - () => showGenericErrorDialog(context: context, error: e), - ); - rethrow; - } - } - - Future getDeleteChallenge( - BuildContext context, - ) async { - try { - final response = await _enteDio.get("/users/delete-challenge"); - if (response.statusCode == 200) { - return DeleteChallengeResponse( - allowDelete: response.data["allowDelete"] as bool, - encryptedChallenge: response.data["encryptedChallenge"], - ); - } else { - throw Exception("delete action failed"); - } - } catch (e) { - _logger.severe(e); - await showGenericErrorDialog( - context: context, - error: e, - ); - return null; - } - } - - Future deleteAccount( - BuildContext context, - String challengeResponse, - ) async { - try { - final response = await _enteDio.delete( - "/users/delete", - data: { - "challenge": challengeResponse, - }, - ); - if (response.statusCode == 200) { - // clear data - await Configuration.instance.logout(); - } else { - throw Exception("delete action failed"); - } - } catch (e) { - _logger.severe(e); - rethrow; - } - } - - Future getTokenForPasskeySession(String sessionID) async { - try { - final response = await _dio.get( - "${_config.getHttpEndpoint()}/users/two-factor/passkeys/get-token", - queryParameters: { - "sessionID": sessionID, - }, - ); - return response.data; - } on DioException catch (e) { - if (e.response != null) { - if (e.response!.statusCode == 404 || e.response!.statusCode == 410) { - throw PassKeySessionExpiredError(); - } - if (e.response!.statusCode == 400) { - throw PassKeySessionNotVerifiedError(); - } - } - rethrow; - } catch (e, s) { - _logger.severe("unexpected error", e, s); - rethrow; - } - } - - Future onPassKeyVerified(BuildContext context, Map response) async { - final ProgressDialog dialog = - createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - try { - final userPassword = _config.getVolatilePassword(); - await _saveConfiguration(response); - if (userPassword == null) { - await dialog.hide(); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return const PasswordReentryPage(); - }, - ), - (route) => route.isFirst, - ); - } else { - Widget page; - if (_config.getEncryptedToken() != null) { - await _config.decryptSecretsAndGetKeyEncKey( - userPassword, - _config.getKeyAttributes()!, - ); - _config.resetVolatilePassword(); - page = const HomePage(); - } else { - throw Exception("unexpected response during passkey verification"); - } - await dialog.hide(); - - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return page; - }, - ), - (route) => route.isFirst, - ); - } - } catch (e) { - _logger.severe(e); - await dialog.hide(); - rethrow; - } - } - - Future verifyEmail( - BuildContext context, - String ott, { - bool isResettingPasswordScreen = false, - }) async { - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - final verifyData = { - "email": _config.getEmail(), - "ott": ott, - }; - if (!_config.isLoggedIn()) { - verifyData["source"] = 'auth:${_getRefSource()}'; - } - try { - final response = await _dio.post( - "${_config.getHttpEndpoint()}/users/verify-email", - data: verifyData, - ); - await dialog.hide(); - if (response.statusCode == 200) { - Widget page; - final String passkeySessionID = response.data["passkeySessionID"]; - final String accountsUrl = response.data["accountsUrl"] ?? kAccountsUrl; - String twoFASessionID = response.data["twoFactorSessionID"]; - if (twoFASessionID.isEmpty && - response.data["twoFactorSessionIDV2"] != null) { - twoFASessionID = response.data["twoFactorSessionIDV2"]; - } - if (passkeySessionID.isNotEmpty) { - page = PasskeyPage( - Configuration.instance, - passkeySessionID, - totp2FASessionID: twoFASessionID, - accountsUrl: accountsUrl, - ); - } else if (twoFASessionID.isNotEmpty) { - page = TwoFactorAuthenticationPage(twoFASessionID); - } else { - await _saveConfiguration(response); - if (Configuration.instance.getEncryptedToken() != null) { - if (isResettingPasswordScreen) { - page = const RecoveryPage(); - } else { - page = const PasswordReentryPage(); - } - } else { - page = PasswordEntryPage( - Configuration.instance, - PasswordEntryMode.set, - const HomePage(), - ); - } - } - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return page; - }, - ), - (route) => route.isFirst, - ); - } else { - // should never reach here - throw Exception("unexpected response during email verification"); - } - } on DioException catch (e) { - _logger.info(e); - await dialog.hide(); - if (e.response != null && e.response!.statusCode == 410) { - await showErrorDialog( - context, - context.l10n.oops, - context.l10n.yourVerificationCodeHasExpired, - ); - Navigator.of(context).pop(); - } else { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.incorrectCode, - context.l10n.sorryTheCodeYouveEnteredIsIncorrect, - ); - } - } catch (e) { - await dialog.hide(); - _logger.severe(e); - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.verificationFailedPleaseTryAgain, - ); - } - } - - Future setEmail(String email) async { - await _config.setEmail(email); - emailValueNotifier.value = email; - } - - Future changeEmail( - BuildContext context, - String email, - String ott, - ) async { - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - try { - final response = await _enteDio.post( - "/users/change-email", - data: { - "email": email, - "ott": ott, - }, - ); - await dialog.hide(); - if (response.statusCode == 200) { - showShortToast(context, context.l10n.emailChangedTo(email)); - await setEmail(email); - Navigator.of(context).popUntil((route) => route.isFirst); - Bus.instance.fire(UserDetailsChangedEvent()); - return; - } - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.verificationFailedPleaseTryAgain, - ); - } on DioException catch (e) { - await dialog.hide(); - if (e.response != null && e.response!.statusCode == 403) { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.thisEmailIsAlreadyInUse, - ); - } else { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.incorrectCode, - context.l10n.authenticationFailedPleaseTryAgain, - ); - } - } catch (e) { - await dialog.hide(); - _logger.severe(e); - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.verificationFailedPleaseTryAgain, - ); - } - } - - Future setAttributes(KeyGenResult result) async { - try { - await registerOrUpdateSrp(result.loginKey); - await _enteDio.put( - "/users/attributes", - data: { - "keyAttributes": result.keyAttributes.toMap(), - }, - ); - await _config.setKey(result.privateKeyAttributes.key); - await _config.setSecretKey(result.privateKeyAttributes.secretKey); - await _config.setKeyAttributes(result.keyAttributes); - } catch (e) { - _logger.severe(e); - rethrow; - } - } - - Future getSrpAttributes(String email) async { - try { - final response = await _dio.get( - "${_config.getHttpEndpoint()}/users/srp/attributes", - queryParameters: { - "email": email, - }, - ); - if (response.statusCode == 200) { - return SrpAttributes.fromMap(response.data); - } else { - throw Exception("get-srp-attributes action failed"); - } - } on DioException catch (e) { - if (e.response != null && e.response!.statusCode == 404) { - throw SrpSetupNotCompleteError(); - } - rethrow; - } catch (e) { - rethrow; - } - } - - Future registerOrUpdateSrp( - Uint8List loginKey, { - SetKeysRequest? setKeysRequest, - bool logOutOtherDevices = false, - }) async { - try { - final String username = const Uuid().v4().toString(); - final SecureRandom random = _getSecureRandom(); - final Uint8List identity = Uint8List.fromList(utf8.encode(username)); - final Uint8List password = loginKey; - final Uint8List salt = random.nextBytes(16); - final gen = SRP6VerifierGenerator( - group: kDefaultSrpGroup, - digest: Digest('SHA-256'), - ); - final v = gen.generateVerifier(salt, identity, password); - - final client = SRP6Client( - group: kDefaultSrpGroup, - digest: Digest('SHA-256'), - random: random, - ); - - final A = client.generateClientCredentials(salt, identity, password); - final request = SetupSRPRequest( - srpUserID: username, - srpSalt: base64Encode(salt), - srpVerifier: base64Encode(SRP6Util.encodeBigInt(v)), - srpA: base64Encode(SRP6Util.encodeBigInt(A!)), - isUpdate: false, - ); - final response = await _enteDio.post( - "/users/srp/setup", - data: request.toMap(), - ); - if (response.statusCode == 200) { - final SetupSRPResponse setupSRPResponse = - SetupSRPResponse.fromJson(response.data); - final serverB = - SRP6Util.decodeBigInt(base64Decode(setupSRPResponse.srpB)); - // ignore: unused_local_variable - final clientS = client.calculateSecret(serverB); - final clientM = client.calculateClientEvidenceMessage(); - - if (setKeysRequest == null) { - await _enteDio.post( - "/users/srp/complete", - data: { - 'setupID': setupSRPResponse.setupID, - 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)), - }, - ); - } else { - await _enteDio.post( - "/users/srp/update", - data: { - 'setupID': setupSRPResponse.setupID, - 'srpM1': base64Encode(SRP6Util.encodeBigInt(clientM!)), - 'updatedKeyAttr': setKeysRequest.toMap(), - 'logOutOtherDevices': logOutOtherDevices, - }, - ); - } - } else { - throw Exception("register-srp action failed"); - } - } catch (e, s) { - _logger.severe("failed to register srp", e, s); - rethrow; - } - } - - SecureRandom _getSecureRandom() { - final List seeds = []; - final random = Random.secure(); - for (int i = 0; i < 32; i++) { - seeds.add(random.nextInt(255)); - } - final secureRandom = FortunaRandom(); - secureRandom.seed(KeyParameter(Uint8List.fromList(seeds))); - return secureRandom; - } - - Future verifyEmailViaPassword( - BuildContext context, - SrpAttributes srpAttributes, - String userPassword, - ProgressDialog dialog, - ) async { - late Uint8List keyEncryptionKey; - _logger.finest('Start deriving key'); - keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(userPassword), - CryptoUtil.base642bin(srpAttributes.kekSalt), - srpAttributes.memLimit, - srpAttributes.opsLimit, - ); - _logger.finest('keyDerivation done, derive LoginKey'); - final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey); - final Uint8List identity = Uint8List.fromList( - utf8.encode(srpAttributes.srpUserID), - ); - _logger.finest('longinKey derivation done'); - final Uint8List salt = base64Decode(srpAttributes.srpSalt); - final Uint8List password = loginKey; - final SecureRandom random = _getSecureRandom(); - - final client = SRP6Client( - group: kDefaultSrpGroup, - digest: Digest('SHA-256'), - random: random, - ); - - final A = client.generateClientCredentials(salt, identity, password); - final createSessionResponse = await _dio.post( - "${_config.getHttpEndpoint()}/users/srp/create-session", - data: { - "srpUserID": srpAttributes.srpUserID, - "srpA": base64Encode(SRP6Util.getPadded(A!, 512)), - }, - ); - final String sessionID = createSessionResponse.data["sessionID"]; - final String srpB = createSessionResponse.data["srpB"]; - - final serverB = SRP6Util.decodeBigInt(base64Decode(srpB)); - - // ignore: unused_local_variable - final clientS = client.calculateSecret(serverB); - final clientM = client.calculateClientEvidenceMessage(); - final response = await _dio.post( - "${_config.getHttpEndpoint()}/users/srp/verify-session", - data: { - "sessionID": sessionID, - "srpUserID": srpAttributes.srpUserID, - "srpM1": base64Encode(SRP6Util.getPadded(clientM!, 32)), - }, - ); - if (response.statusCode == 200) { - Widget? page; - final String passkeySessionID = response.data["passkeySessionID"]; - final String accountsUrl = response.data["accountsUrl"] ?? kAccountsUrl; - String twoFASessionID = response.data["twoFactorSessionID"]; - if (twoFASessionID.isEmpty && - response.data["twoFactorSessionIDV2"] != null) { - twoFASessionID = response.data["twoFactorSessionIDV2"]; - } - Configuration.instance.setVolatilePassword(userPassword); - if (passkeySessionID.isNotEmpty) { - page = PasskeyPage( - Configuration.instance, - passkeySessionID, - totp2FASessionID: twoFASessionID, - accountsUrl: accountsUrl, - ); - } else if (twoFASessionID.isNotEmpty) { - page = TwoFactorAuthenticationPage(twoFASessionID); - } else { - await _saveConfiguration(response); - if (Configuration.instance.getEncryptedToken() != null) { - await Configuration.instance.decryptSecretsAndGetKeyEncKey( - userPassword, - Configuration.instance.getKeyAttributes()!, - keyEncryptionKey: keyEncryptionKey, - ); - page = const HomePage(); - } else { - throw Exception("unexpected response during email verification"); - } - } - await dialog.hide(); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return page!; - }, - ), - (route) => route.isFirst, - ); - } else { - // should never reach here - throw Exception("unexpected response during email verification"); - } - } - - Future updateKeyAttributes( - KeyAttributes keyAttributes, - Uint8List loginKey, { - required bool logoutOtherDevices, - }) async { - try { - final setKeyRequest = SetKeysRequest( - kekSalt: keyAttributes.kekSalt, - encryptedKey: keyAttributes.encryptedKey, - keyDecryptionNonce: keyAttributes.keyDecryptionNonce, - memLimit: keyAttributes.memLimit, - opsLimit: keyAttributes.opsLimit, - ); - await registerOrUpdateSrp( - loginKey, - setKeysRequest: setKeyRequest, - logOutOtherDevices: logoutOtherDevices, - ); - await _config.setKeyAttributes(keyAttributes); - } catch (e) { - _logger.severe(e); - rethrow; - } - } - - Future setRecoveryKey(KeyAttributes keyAttributes) async { - try { - final setRecoveryKeyRequest = SetRecoveryKeyRequest( - keyAttributes.masterKeyEncryptedWithRecoveryKey, - keyAttributes.masterKeyDecryptionNonce, - keyAttributes.recoveryKeyEncryptedWithMasterKey, - keyAttributes.recoveryKeyDecryptionNonce, - ); - await _enteDio.put( - "/users/recovery-key", - data: setRecoveryKeyRequest.toMap(), - ); - await _config.setKeyAttributes(keyAttributes); - } catch (e) { - _logger.severe(e); - rethrow; - } - } - - Future verifyTwoFactor( - BuildContext context, - String sessionID, - String code, - ) async { - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - try { - final response = await _dio.post( - "${_config.getHttpEndpoint()}/users/two-factor/verify", - data: { - "sessionID": sessionID, - "code": code, - }, - ); - await dialog.hide(); - if (response.statusCode == 200) { - showShortToast(context, context.l10n.authenticationSuccessful); - await _saveConfiguration(response); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return const PasswordReentryPage(); - }, - ), - (route) => route.isFirst, - ); - } - } on DioException catch (e) { - await dialog.hide(); - _logger.severe(e); - if (e.response != null && e.response!.statusCode == 404) { - showToast(context, "Session expired"); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return LoginPage(Configuration.instance); - }, - ), - (route) => route.isFirst, - ); - } else { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.incorrectCode, - context.l10n.authenticationFailedPleaseTryAgain, - ); - } - } catch (e) { - await dialog.hide(); - _logger.severe(e); - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.authenticationFailedPleaseTryAgain, - ); - } - } - - Future recoverTwoFactor( - BuildContext context, - String sessionID, - TwoFactorType type, - ) async { - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - try { - final response = await _dio.get( - "${_config.getHttpEndpoint()}/users/two-factor/recover", - queryParameters: { - "sessionID": sessionID, - "twoFactorType": twoFactorTypeToString(type), - }, - ); - await dialog.hide(); - if (response.statusCode == 200) { - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return TwoFactorRecoveryPage( - type, - sessionID, - response.data["encryptedSecret"], - response.data["secretDecryptionNonce"], - ); - }, - ), - (route) => route.isFirst, - ); - } - } on DioException catch (e) { - await dialog.hide(); - _logger.severe(e); - if (e.response != null && e.response!.statusCode == 404) { - showToast(context, context.l10n.sessionExpired); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return LoginPage(Configuration.instance); - }, - ), - (route) => route.isFirst, - ); - } else { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.somethingWentWrongPleaseTryAgain, - ); - } - } catch (e) { - await dialog.hide(); - _logger.severe(e); - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.somethingWentWrongPleaseTryAgain, - ); - } finally { - await dialog.hide(); - } - } - - Future removeTwoFactor( - BuildContext context, - TwoFactorType type, - String sessionID, - String recoveryKey, - String encryptedSecret, - String secretDecryptionNonce, - ) async { - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - String secret; - try { - if (recoveryKey.contains(' ')) { - if (recoveryKey.split(' ').length != mnemonicKeyWordCount) { - throw AssertionError( - 'recovery code should have $mnemonicKeyWordCount words', - ); - } - recoveryKey = bip39.mnemonicToEntropy(recoveryKey); - } - secret = CryptoUtil.bin2base64( - await CryptoUtil.decrypt( - CryptoUtil.base642bin(encryptedSecret), - CryptoUtil.hex2bin(recoveryKey.trim()), - CryptoUtil.base642bin(secretDecryptionNonce), - ), - ); - } catch (e) { - await dialog.hide(); - await showErrorDialog( - context, - context.l10n.incorrectRecoveryKey, - context.l10n.theRecoveryKeyYouEnteredIsIncorrect, - ); - return; - } - try { - final response = await _dio.post( - "${_config.getHttpEndpoint()}/users/two-factor/remove", - data: { - "sessionID": sessionID, - "secret": secret, - "twoFactorType": twoFactorTypeToString(type), - }, - ); - await dialog.hide(); - if (response.statusCode == 200) { - showShortToast( - context, - context.l10n.twofactorAuthenticationSuccessfullyReset, - ); - await _saveConfiguration(response); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return const PasswordReentryPage(); - }, - ), - (route) => route.isFirst, - ); - } - } on DioException catch (e) { - await dialog.hide(); - _logger.severe(e); - if (e.response != null && e.response!.statusCode == 404) { - showToast(context, "Session expired"); - // ignore: unawaited_futures - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return LoginPage(Configuration.instance); - }, - ), - (route) => route.isFirst, - ); - } else { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.somethingWentWrongPleaseTryAgain, - ); - } - } catch (e) { - await dialog.hide(); - _logger.severe(e); - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.oops, - context.l10n.somethingWentWrongPleaseTryAgain, - ); - } finally { - await dialog.hide(); - } - } - - Future _saveConfiguration(dynamic response) async { - final responseData = response is Map ? response : response.data as Map?; - if (responseData == null) return; - - await Configuration.instance.setUserID(responseData["id"]); - if (responseData["encryptedToken"] != null) { - await Configuration.instance - .setEncryptedToken(responseData["encryptedToken"]); - await Configuration.instance.setKeyAttributes( - KeyAttributes.fromMap(responseData["keyAttributes"]), - ); - } else { - await Configuration.instance.setToken(responseData["token"]); - } - } - - bool? canDisableEmailMFA() { - return _preferences.getBool(kCanDisableEmailMFA); - } - - bool hasEmailMFAEnabled() { - return _preferences.getBool(kIsEmailMFAEnabled) ?? true; - } - - Future updateEmailMFA(bool isEnabled) async { - try { - await _enteDio.put( - "/users/email-mfa", - data: { - "isEnabled": isEnabled, - }, - ); - await _preferences.setBool(kIsEmailMFAEnabled, isEnabled); - } catch (e) { - _logger.severe("Failed to update email mfa", e); - rethrow; - } - } - - Future setRefSource(String refSource) async { - await _preferences.setString(kReferralSource, refSource); - } - - String _getRefSource() { - return _preferences.getString(kReferralSource) ?? ""; - } -} diff --git a/mobile/apps/auth/lib/ui/account/password_reentry_page.dart b/mobile/apps/auth/lib/ui/account/password_reentry_page.dart deleted file mode 100644 index 600d6ada98..0000000000 --- a/mobile/apps/auth/lib/ui/account/password_reentry_page.dart +++ /dev/null @@ -1,326 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/core/errors.dart'; -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/services/user_service.dart'; -import 'package:ente_auth/ui/account/recovery_page.dart'; -import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/components/buttons/button_widget.dart'; -import 'package:ente_auth/ui/home_page.dart'; -import 'package:ente_auth/utils/dialog_util.dart'; -import 'package:ente_auth/utils/email_util.dart'; -import 'package:ente_crypto_dart/ente_crypto_dart.dart'; -import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; - -class PasswordReentryPage extends StatefulWidget { - const PasswordReentryPage({super.key}); - - @override - State createState() => _PasswordReentryPageState(); -} - -class _PasswordReentryPageState extends State { - final _logger = Logger((_PasswordReentryPageState).toString()); - final _passwordController = TextEditingController(); - final FocusNode _passwordFocusNode = FocusNode(); - String? email; - bool _passwordInFocus = false; - bool _passwordVisible = false; - String? _volatilePassword; - - @override - void initState() { - super.initState(); - email = Configuration.instance.getEmail(); - _volatilePassword = Configuration.instance.getVolatilePassword(); - if (_volatilePassword != null) { - _passwordController.text = _volatilePassword!; - Future.delayed( - Duration.zero, - () => verifyPassword(_volatilePassword!, usingVolatilePassword: true), - ); - } - _passwordFocusNode.addListener(() { - setState(() { - _passwordInFocus = _passwordFocusNode.hasFocus; - }); - }); - } - - @override - Widget build(BuildContext context) { - final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; - - FloatingActionButtonLocation? fabLocation() { - if (isKeypadOpen) { - return null; - } else { - return FloatingActionButtonLocation.centerFloat; - } - } - - return Scaffold( - resizeToAvoidBottomInset: isKeypadOpen, - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - body: _getBody(), - floatingActionButton: DynamicFAB( - key: const ValueKey("verifyPasswordButton"), - isKeypadOpen: isKeypadOpen, - isFormValid: _passwordController.text.isNotEmpty, - buttonText: context.l10n.verifyPassword, - onPressedFunction: () async { - FocusScope.of(context).unfocus(); - await verifyPassword(_passwordController.text); - }, - ), - floatingActionButtonLocation: fabLocation(), - floatingActionButtonAnimator: NoScalingAnimation(), - ); - } - - Future verifyPassword( - String password, { - bool usingVolatilePassword = false, - }) async { - FocusScope.of(context).unfocus(); - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - if (usingVolatilePassword) { - _logger.info("Using volatile password"); - } - try { - final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey( - password, - Configuration.instance.getKeyAttributes()!, - ); - _registerSRPForExistingUsers(kek).ignore(); - } on KeyDerivationError catch (e, s) { - _logger.severe("Password verification failed", e, s); - await dialog.hide(); - final dialogChoice = await showChoiceDialog( - context, - title: context.l10n.recreatePasswordTitle, - body: context.l10n.recreatePasswordBody, - firstButtonLabel: context.l10n.useRecoveryKey, - ); - if (dialogChoice!.action == ButtonAction.first) { - // ignore: unawaited_futures - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return const RecoveryPage(); - }, - ), - ); - } - return; - } catch (e, s) { - _logger.severe("Password verification failed", e, s); - await dialog.hide(); - final dialogChoice = await showChoiceDialog( - context, - title: context.l10n.incorrectPasswordTitle, - body: context.l10n.pleaseTryAgain, - firstButtonLabel: context.l10n.contactSupport, - secondButtonLabel: context.l10n.ok, - ); - if (dialogChoice!.action == ButtonAction.first) { - await sendLogs( - context, - context.l10n.contactSupport, - postShare: () {}, - ); - } - return; - } - Configuration.instance.resetVolatilePassword(); - await dialog.hide(); - unawaited( - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (BuildContext context) { - return const HomePage(); - }, - ), - (route) => false, - ), - ); - } - - Future _registerSRPForExistingUsers(Uint8List key) async { - bool shouldSetupSRP = false; - try { - // ignore: unused_local_variable - final attr = await UserService.instance.getSrpAttributes(email!); - } on SrpSetupNotCompleteError { - shouldSetupSRP = true; - } catch (e, s) { - _logger.severe("error while fetching attr", e, s); - } - if (shouldSetupSRP) { - try { - final Uint8List loginKey = await CryptoUtil.deriveLoginKey(key); - await UserService.instance.registerOrUpdateSrp(loginKey); - } catch (e, s) { - _logger.severe("error while setting up srp for existing users", e, s); - } - } - } - - Widget _getBody() { - return Column( - children: [ - Expanded( - child: AutofillGroup( - child: ListView( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - context.l10n.welcomeBack, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - key: const ValueKey("passwordInputField"), - autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - hintText: context.l10n.enterYourPasswordHint, - filled: true, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _passwordInFocus - ? IconButton( - icon: Icon( - _passwordVisible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ) - : null, - ), - style: const TextStyle( - fontSize: 14, - ), - controller: _passwordController, - autofocus: true, - autocorrect: false, - obscureText: !_passwordVisible, - keyboardType: TextInputType.visiblePassword, - focusNode: _passwordFocusNode, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return const RecoveryPage(); - }, - ), - ); - }, - child: Center( - child: Text( - context.l10n.forgotPassword, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), - ), - ), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () async { - final dialog = createProgressDialog( - context, - context.l10n.pleaseWait, - ); - await dialog.show(); - await Configuration.instance.logout(); - await dialog.hide(); - Navigator.of(context) - .popUntil((route) => route.isFirst); - }, - child: Center( - child: Text( - context.l10n.changeEmail, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/mobile/apps/auth/lib/ui/account/recovery_key_page.dart b/mobile/apps/auth/lib/ui/account/recovery_key_page.dart deleted file mode 100644 index 0422a9e2b8..0000000000 --- a/mobile/apps/auth/lib/ui/account/recovery_key_page.dart +++ /dev/null @@ -1,355 +0,0 @@ -import 'dart:convert'; -import 'dart:io' as io; - -import 'package:bip39/bip39.dart' as bip39; -import 'package:dotted_border/dotted_border.dart'; -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/core/constants.dart'; -import 'package:ente_auth/ente_theme_data.dart'; -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/ui/common/gradient_button.dart'; -import 'package:ente_auth/utils/platform_util.dart'; -import 'package:ente_auth/utils/share_utils.dart'; -import 'package:ente_auth/utils/toast_util.dart'; -import 'package:file_saver/file_saver.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:step_progress_indicator/step_progress_indicator.dart'; - -class RecoveryKeyPage extends StatefulWidget { - final bool? showAppBar; - final String recoveryKey; - final String doneText; - final Function()? onDone; - final bool? isDismissible; - final String? title; - final String? text; - final String? subText; - final bool showProgressBar; - - const RecoveryKeyPage( - this.recoveryKey, - this.doneText, { - super.key, - this.showAppBar, - this.onDone, - this.isDismissible, - this.title, - this.text, - this.subText, - this.showProgressBar = false, - }); - - @override - State createState() => _RecoveryKeyPageState(); -} - -class _RecoveryKeyPageState extends State { - bool _hasTriedToSave = false; - final _recoveryKeyFile = io.File( - "${Configuration.instance.getTempDirectory()}ente-recovery-key.txt", - ); - - @override - Widget build(BuildContext context) { - final String recoveryKey = bip39.entropyToMnemonic(widget.recoveryKey); - if (recoveryKey.split(' ').length != mnemonicKeyWordCount) { - throw AssertionError( - 'recovery code should have $mnemonicKeyWordCount words', - ); - } - final double topPadding = widget.showAppBar! - ? 40 - : widget.showProgressBar - ? 32 - : 120; - - Future copy() async { - await Clipboard.setData( - ClipboardData( - text: recoveryKey, - ), - ); - showShortToast( - context, - context.l10n.recoveryKeyCopiedToClipboard, - ); - setState(() { - _hasTriedToSave = true; - }); - } - - return Scaffold( - appBar: widget.showProgressBar - ? AppBar( - automaticallyImplyLeading: false, - elevation: 0, - title: Hero( - tag: "recovery_key", - child: StepProgressIndicator( - totalSteps: 4, - currentStep: 3, - selectedColor: Theme.of(context).colorScheme.alternativeColor, - roundedEdges: const Radius.circular(10), - unselectedColor: - Theme.of(context).colorScheme.stepProgressUnselectedColor, - ), - ), - ) - : widget.showAppBar! - ? AppBar( - elevation: 0, - title: Text(widget.title ?? context.l10n.recoveryKey), - ) - : null, - body: Padding( - padding: EdgeInsets.fromLTRB(20, topPadding, 20, 20), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - child: ConstrainedBox( - constraints: BoxConstraints( - minWidth: constraints.maxWidth, - minHeight: constraints.maxHeight, - ), - child: IntrinsicHeight( - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - widget.showAppBar! - ? const SizedBox.shrink() - : Text( - widget.title ?? context.l10n.recoveryKey, - style: Theme.of(context).textTheme.headlineMedium, - ), - Padding( - padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12), - ), - Text( - widget.text ?? context.l10n.recoveryKeyOnForgotPassword, - style: Theme.of(context).textTheme.titleMedium, - ), - const Padding(padding: EdgeInsets.only(top: 24)), - Container( - padding: const EdgeInsets.all(1), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0x8E9610D6), - Color(0x8E9F4FC6), - ], - stops: [0.0, 0.9753], - ), - ), - child: DottedBorder( - padding: EdgeInsets.zero, - borderType: BorderType.RRect, - strokeWidth: 1, - color: const Color(0xFF6B6B6B), - dashPattern: const [6, 6], - radius: const Radius.circular(8), - child: SizedBox( - width: double.infinity, - child: Stack( - children: [ - Column( - children: [ - Builder( - builder: (context) { - final content = Container( - padding: const EdgeInsets.all(20), - width: double.infinity, - child: Text( - recoveryKey, - textAlign: TextAlign.justify, - style: Theme.of(context) - .textTheme - .bodyLarge, - ), - ); - - if (PlatformUtil.isMobile()) { - return GestureDetector( - onTap: () async => await copy(), - child: content, - ); - } else { - return SelectableRegion( - focusNode: FocusNode(), - selectionControls: - PlatformUtil.selectionControls, - child: content, - ); - } - }, - ), - ], - ), - Positioned( - right: 0, - top: 0, - child: PlatformCopy( - onPressed: copy, - ), - ), - ], - ), - ), - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 20), - child: Text( - widget.subText ?? - context.l10n.recoveryKeySaveDescription, - style: Theme.of(context).textTheme.bodyLarge, - ), - ), - Expanded( - child: Container( - alignment: Alignment.bottomCenter, - width: double.infinity, - padding: const EdgeInsets.fromLTRB(10, 10, 10, 42), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: _saveOptions(context, recoveryKey), - ), - ), - ), - ], - ), - ), - ), - ); - }, - ), - ), - ); - } - - List _saveOptions(BuildContext context, String recoveryKey) { - final List childrens = []; - if (!_hasTriedToSave) { - childrens.add( - SizedBox( - height: 56, - child: ElevatedButton( - style: Theme.of(context).colorScheme.optionalActionButtonStyle, - onPressed: () async { - await _saveKeys(); - }, - child: Text(context.l10n.doThisLater), - ), - ), - ); - childrens.add(const SizedBox(height: 10)); - } - - childrens.add( - GradientButton( - onTap: () async { - await shareDialog( - context, - context.l10n.recoveryKey, - saveAction: () async { - await _saveRecoveryKey(recoveryKey); - }, - sendAction: () async { - await _shareRecoveryKey(recoveryKey); - }, - ); - }, - text: context.l10n.saveKey, - ), - ); - - if (_hasTriedToSave) { - childrens.add(const SizedBox(height: 10)); - childrens.add( - SizedBox( - height: 56, - child: ElevatedButton( - child: Text(widget.doneText), - onPressed: () async { - await _saveKeys(); - }, - ), - ), - ); - } - childrens.add(const SizedBox(height: 12)); - return childrens; - } - - Future _saveRecoveryKey(String recoveryKey) async { - final bytes = utf8.encode(recoveryKey); - final time = DateTime.now().millisecondsSinceEpoch; - - await PlatformUtil.shareFile( - "ente_recovery_key_$time", - "txt", - bytes, - MimeType.text, - ); - - if (mounted) { - showToast( - context, - context.l10n.recoveryKeySaved, - ); - setState(() { - _hasTriedToSave = true; - }); - } - } - - Future _shareRecoveryKey(String recoveryKey) async { - if (_recoveryKeyFile.existsSync()) { - await _recoveryKeyFile.delete(); - } - _recoveryKeyFile.writeAsStringSync(recoveryKey); - await Share.shareXFiles([XFile(_recoveryKeyFile.path)]); - Future.delayed(const Duration(milliseconds: 500), () { - if (mounted) { - setState(() { - _hasTriedToSave = true; - }); - } - }); - } - - Future _saveKeys() async { - Navigator.of(context).pop(); - if (_recoveryKeyFile.existsSync()) { - await _recoveryKeyFile.delete(); - } - widget.onDone!(); - } -} - -class PlatformCopy extends StatelessWidget { - const PlatformCopy({ - super.key, - required this.onPressed, - }); - - final void Function() onPressed; - - @override - Widget build(BuildContext context) { - return IconButton( - onPressed: () => onPressed(), - visualDensity: VisualDensity.compact, - icon: const Icon( - Icons.copy, - size: 16, - ), - ); - } -} diff --git a/mobile/apps/auth/lib/ui/account/recovery_page.dart b/mobile/apps/auth/lib/ui/account/recovery_page.dart deleted file mode 100644 index cb3619344d..0000000000 --- a/mobile/apps/auth/lib/ui/account/recovery_page.dart +++ /dev/null @@ -1,164 +0,0 @@ -import 'package:ente_accounts/pages/password_entry_page.dart'; -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/home_page.dart'; -import 'package:ente_auth/utils/dialog_util.dart'; -import 'package:ente_auth/utils/toast_util.dart'; -import 'package:flutter/material.dart'; - -class RecoveryPage extends StatefulWidget { - const RecoveryPage({super.key}); - - @override - State createState() => _RecoveryPageState(); -} - -class _RecoveryPageState extends State { - final _recoveryKey = TextEditingController(); - - Future onPressed() async { - FocusScope.of(context).unfocus(); - final dialog = createProgressDialog(context, "Decrypting..."); - await dialog.show(); - try { - await Configuration.instance.recover(_recoveryKey.text.trim()); - await dialog.hide(); - showToast(context, "Recovery successful!"); - await Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (BuildContext context) { - return PopScope( - canPop: false, - child: PasswordEntryPage( - Configuration.instance, - PasswordEntryMode.reset, - const HomePage(), - ), - ); - }, - ), - ); - } catch (e) { - await dialog.hide(); - String errMessage = 'The recovery key you entered is incorrect'; - if (e is AssertionError) { - errMessage = '$errMessage : ${e.message}'; - } - await showErrorDialog(context, "Incorrect recovery key", errMessage); - } - } - - @override - Widget build(BuildContext context) { - final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; - FloatingActionButtonLocation? fabLocation() { - if (isKeypadOpen) { - return null; - } else { - return FloatingActionButtonLocation.centerFloat; - } - } - - return Scaffold( - resizeToAvoidBottomInset: isKeypadOpen, - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - floatingActionButton: DynamicFAB( - isKeypadOpen: isKeypadOpen, - isFormValid: _recoveryKey.text.isNotEmpty, - buttonText: 'Recover', - onPressedFunction: onPressed, - ), - floatingActionButtonLocation: fabLocation(), - floatingActionButtonAnimator: NoScalingAnimation(), - body: Column( - children: [ - Expanded( - child: ListView( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - context.l10n.forgotPassword, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - decoration: InputDecoration( - filled: true, - hintText: "Enter your recovery key", - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), - style: const TextStyle( - fontSize: 14, - fontFeatures: [FontFeature.tabularFigures()], - ), - controller: _recoveryKey, - autofocus: false, - autocorrect: false, - keyboardType: TextInputType.multiline, - maxLines: null, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - Row( - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - showErrorDialog( - context, - "Sorry", - "Due to the nature of our end-to-end encryption protocol, your data cannot be decrypted without your password or recovery key", - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Center( - child: Text( - context.l10n.noRecoveryKeyTitle, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/mobile/apps/auth/lib/ui/account/request_pwd_verification_page.dart b/mobile/apps/auth/lib/ui/account/request_pwd_verification_page.dart deleted file mode 100644 index 5901d3bd45..0000000000 --- a/mobile/apps/auth/lib/ui/account/request_pwd_verification_page.dart +++ /dev/null @@ -1,219 +0,0 @@ -import "dart:convert"; -import "dart:typed_data"; - -import 'package:ente_auth/core/configuration.dart'; -import "package:ente_auth/l10n/l10n.dart"; -import "package:ente_auth/theme/ente_theme.dart"; -import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import "package:ente_auth/utils/dialog_util.dart"; -import 'package:ente_crypto_dart/ente_crypto_dart.dart'; -import 'package:flutter/material.dart'; -import "package:logging/logging.dart"; - -typedef OnPasswordVerifiedFn = Future Function(Uint8List bytes); - -class RequestPasswordVerificationPage extends StatefulWidget { - final OnPasswordVerifiedFn onPasswordVerified; - final Function? onPasswordError; - - const RequestPasswordVerificationPage({ - super.key, - required this.onPasswordVerified, - this.onPasswordError, - }); - - @override - State createState() => - _RequestPasswordVerificationPageState(); -} - -class _RequestPasswordVerificationPageState - extends State { - final _logger = Logger((_RequestPasswordVerificationPageState).toString()); - final _passwordController = TextEditingController(); - final FocusNode _passwordFocusNode = FocusNode(); - String? email; - bool _passwordInFocus = false; - bool _passwordVisible = false; - - @override - void initState() { - super.initState(); - email = Configuration.instance.getEmail(); - _passwordFocusNode.addListener(() { - setState(() { - _passwordInFocus = _passwordFocusNode.hasFocus; - }); - }); - } - - @override - Widget build(BuildContext context) { - final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; - - FloatingActionButtonLocation? fabLocation() { - if (isKeypadOpen) { - return null; - } else { - return FloatingActionButtonLocation.centerFloat; - } - } - - return Scaffold( - resizeToAvoidBottomInset: isKeypadOpen, - appBar: AppBar( - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back), - color: Theme.of(context).iconTheme.color, - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ), - body: _getBody(), - floatingActionButton: DynamicFAB( - key: const ValueKey("verifyPasswordButton"), - isKeypadOpen: isKeypadOpen, - isFormValid: _passwordController.text.isNotEmpty, - buttonText: context.l10n.verifyPassword, - onPressedFunction: () async { - FocusScope.of(context).unfocus(); - final dialog = createProgressDialog(context, context.l10n.pleaseWait); - await dialog.show(); - try { - final attributes = Configuration.instance.getKeyAttributes()!; - final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(_passwordController.text), - CryptoUtil.base642bin(attributes.kekSalt), - attributes.memLimit, - attributes.opsLimit, - ); - CryptoUtil.decryptSync( - CryptoUtil.base642bin(attributes.encryptedKey), - keyEncryptionKey, - CryptoUtil.base642bin(attributes.keyDecryptionNonce), - ); - await dialog.show(); - // pop - await widget.onPasswordVerified(keyEncryptionKey); - await dialog.hide(); - Navigator.of(context).pop(true); - } catch (e, s) { - _logger.severe("Error while verifying password", e, s); - await dialog.hide(); - if (widget.onPasswordError != null) { - widget.onPasswordError!(); - } else { - // ignore: unawaited_futures - showErrorDialog( - context, - context.l10n.incorrectPasswordTitle, - context.l10n.pleaseTryAgain, - ); - } - } - }, - ), - floatingActionButtonLocation: fabLocation(), - floatingActionButtonAnimator: NoScalingAnimation(), - ); - } - - Widget _getBody() { - return Column( - children: [ - Expanded( - child: AutofillGroup( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.only(top: 30, left: 20, right: 20), - child: Text( - context.l10n.enterPassword, - style: Theme.of(context).textTheme.headlineMedium, - ), - ), - Padding( - padding: const EdgeInsets.only( - bottom: 30, - left: 22, - right: 20, - ), - child: Text( - email ?? '', - style: getEnteTextTheme(context).smallMuted, - ), - ), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), - child: TextFormField( - key: const ValueKey("passwordInputField"), - autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - hintText: context.l10n.enterYourPasswordHint, - filled: true, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _passwordInFocus - ? IconButton( - icon: Icon( - _passwordVisible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ) - : null, - ), - style: const TextStyle( - fontSize: 14, - ), - controller: _passwordController, - autofocus: true, - autocorrect: false, - obscureText: !_passwordVisible, - keyboardType: TextInputType.visiblePassword, - focusNode: _passwordFocusNode, - onChanged: (_) { - setState(() {}); - }, - ), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 18), - child: Divider( - thickness: 1, - ), - ), - ], - ), - ), - ), - ], - ); - } -} diff --git a/mobile/apps/auth/lib/ui/home_page.dart b/mobile/apps/auth/lib/ui/home_page.dart index f6874882d7..32c8c2683f 100644 --- a/mobile/apps/auth/lib/ui/home_page.dart +++ b/mobile/apps/auth/lib/ui/home_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:app_links/app_links.dart'; import 'package:collection/collection.dart'; +import 'package:ente_accounts/services/user_service.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/events/codes_updated_event.dart'; @@ -14,7 +15,6 @@ import 'package:ente_auth/onboarding/model/tag_enums.dart'; import 'package:ente_auth/onboarding/view/common/tag_chip.dart'; import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart'; import 'package:ente_auth/services/preference_service.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/store/code_display_store.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/theme/ente_theme.dart'; diff --git a/mobile/apps/auth/lib/ui/settings/account_section_widget.dart b/mobile/apps/auth/lib/ui/settings/account_section_widget.dart index e72585e6de..46d284ca47 100644 --- a/mobile/apps/auth/lib/ui/settings/account_section_widget.dart +++ b/mobile/apps/auth/lib/ui/settings/account_section_widget.dart @@ -1,11 +1,11 @@ import 'package:ente_accounts/pages/change_email_dialog.dart'; import 'package:ente_accounts/pages/delete_account_page.dart'; import 'package:ente_accounts/pages/password_entry_page.dart'; +import 'package:ente_accounts/pages/recovery_key_page.dart'; +import 'package:ente_accounts/services/user_service.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:ente_auth/ui/account/recovery_key_page.dart'; import 'package:ente_auth/ui/components/captioned_text_widget.dart'; import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; @@ -124,6 +124,7 @@ class AccountSectionWidget extends StatelessWidget { routeToPage( context, RecoveryKeyPage( + Configuration.instance, recoveryKey, l10n.ok, showAppBar: true, diff --git a/mobile/apps/auth/lib/ui/settings/notification_banner_widget.dart b/mobile/apps/auth/lib/ui/settings/notification_banner_widget.dart index 6c5bcd168d..83162b04f8 100644 --- a/mobile/apps/auth/lib/ui/settings/notification_banner_widget.dart +++ b/mobile/apps/auth/lib/ui/settings/notification_banner_widget.dart @@ -1,10 +1,10 @@ import 'dart:io'; import 'package:ente_accounts/models/user_details.dart'; +import 'package:ente_accounts/services/user_service.dart'; import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/preference_service.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/components/banner_widget.dart'; import 'package:flutter/material.dart'; diff --git a/mobile/apps/auth/lib/ui/settings/security_section_widget.dart b/mobile/apps/auth/lib/ui/settings/security_section_widget.dart index 9fa7d06b54..7406a9fd94 100644 --- a/mobile/apps/auth/lib/ui/settings/security_section_widget.dart +++ b/mobile/apps/auth/lib/ui/settings/security_section_widget.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:ente_accounts/models/user_details.dart'; +import 'package:ente_accounts/pages/request_pwd_verification_page.dart'; import 'package:ente_accounts/pages/sessions_page.dart'; +import 'package:ente_accounts/services/passkey_service.dart'; +import 'package:ente_accounts/services/user_service.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/services/passkey_service.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:ente_auth/ui/account/request_pwd_verification_page.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/captioned_text_widget.dart'; import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; @@ -243,6 +243,7 @@ class _SecuritySectionWidgetState extends State { await routeToPage( context, RequestPasswordVerificationPage( + Configuration.instance, onPasswordVerified: (Uint8List keyEncryptionKey) async { final Uint8List loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey); diff --git a/mobile/apps/auth/lib/ui/settings_page.dart b/mobile/apps/auth/lib/ui/settings_page.dart index d543985b57..d1965eb009 100644 --- a/mobile/apps/auth/lib/ui/settings_page.dart +++ b/mobile/apps/auth/lib/ui/settings_page.dart @@ -1,9 +1,9 @@ import 'dart:io'; +import 'package:ente_accounts/services/user_service.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/onboarding/view/onboarding_page.dart'; -import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/theme/colors.dart'; import 'package:ente_auth/theme/ente_theme.dart';