diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index 075264564..a71fa37e0 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -271,8 +271,9 @@ class Configuration { // SRP setup for existing users. Future decryptSecretsAndGetKeyEncKey( String password, - KeyAttributes attributes, - ) async { + KeyAttributes attributes, { + Uint8List? keyEncryptionKey, + }) async { validatePreVerificationStateCheck( attributes, password, @@ -280,7 +281,7 @@ class Configuration { ); // Derive key-encryption-key from the entered password and existing // mem and ops limits - final keyEncryptionKey = await CryptoUtil.deriveKey( + keyEncryptionKey ??= await CryptoUtil.deriveKey( utf8.encode(password) as Uint8List, CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit!, diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 07162f896..f25fca88b 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -34,6 +34,7 @@ import 'package:photos/ui/account/two_factor_authentication_page.dart'; import 'package:photos/ui/account/two_factor_recovery_page.dart'; import 'package:photos/ui/account/two_factor_setup_page.dart'; import "package:photos/ui/components/buttons/button_widget.dart"; +import "package:photos/ui/tabs/home_widget.dart"; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; import "package:photos/utils/email_util.dart"; @@ -569,14 +570,15 @@ class UserService { isDismissible: true, ); await dialog.show(); + late Uint8List keyEncryptionKey; try { - final kek = await CryptoUtil.deriveKey( + keyEncryptionKey = await CryptoUtil.deriveKey( utf8.encode(userPassword) as Uint8List, CryptoUtil.base642bin(srpAttributes.kekSalt), srpAttributes.memLimit, srpAttributes.opsLimit, ); - final loginKey = await CryptoUtil.deriveLoginKey(kek); + final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey); final Uint8List identity = Uint8List.fromList( utf8.encode(srpAttributes.srpUserID), ); @@ -614,7 +616,6 @@ class UserService { }, ); if (response.statusCode == 200) { - await dialog.hide(); Widget page; final String twoFASessionID = response.data["twoFactorSessionID"]; Configuration.instance.setVolatilePassword(userPassword); @@ -624,11 +625,17 @@ class UserService { } else { await _saveConfiguration(response); if (Configuration.instance.getEncryptedToken() != null) { - page = const PasswordReentryPage(); + await Configuration.instance.decryptSecretsAndGetKeyEncKey( + userPassword, + Configuration.instance.getKeyAttributes()!, + keyEncryptionKey: keyEncryptionKey, + ); + page = const HomeWidget(); } else { throw Exception("unexpected response during email verification"); } } + await dialog.hide(); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { diff --git a/lib/ui/account/request_pwd_verification_page.dart b/lib/ui/account/request_pwd_verification_page.dart new file mode 100644 index 000000000..0c6a6cec6 --- /dev/null +++ b/lib/ui/account/request_pwd_verification_page.dart @@ -0,0 +1,223 @@ +import "dart:convert"; +import "dart:typed_data"; + +import 'package:flutter/material.dart'; +import "package:flutter_sodium/flutter_sodium.dart"; +import "package:logging/logging.dart"; +import 'package:photos/core/configuration.dart'; +import "package:photos/l10n/l10n.dart"; +import "package:photos/theme/ente_theme.dart"; +import 'package:photos/ui/common/dynamic_fab.dart'; +import "package:photos/utils/crypto_util.dart"; +import "package:photos/utils/dialog_util.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 + void dispose() { + _passwordController.dispose(); + _passwordFocusNode.dispose(); + super.dispose(); + } + + @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); + dialog.show(); + try { + final attributes = Configuration.instance.getKeyAttributes()!; + final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( + utf8.encode(_passwordController.text) as Uint8List, + Sodium.base642bin(attributes.kekSalt), + attributes.memLimit!, + attributes.opsLimit!, + ); + CryptoUtil.decryptSync( + Sodium.base642bin(attributes.encryptedKey), + keyEncryptionKey, + Sodium.base642bin(attributes.keyDecryptionNonce), + ); + dialog.show(); + // pop + await widget.onPasswordVerified(keyEncryptionKey); + dialog.hide(); + Navigator.of(context).pop(true); + } catch (e, s) { + _logger.severe("Error while verifying password", e, s); + dialog.hide(); + if (widget.onPasswordError != null) { + widget.onPasswordError!(); + } else { + 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.enterYourPassword, + 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/lib/ui/settings/security_section_widget.dart b/lib/ui/settings/security_section_widget.dart index 272c46467..970cd3c80 100644 --- a/lib/ui/settings/security_section_widget.dart +++ b/lib/ui/settings/security_section_widget.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import "dart:typed_data"; import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; @@ -6,10 +7,12 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/events/two_factor_status_change_event.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/models/user_details.dart"; import 'package:photos/services/local_authentication_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/account/recovery_key_page.dart"; +import "package:photos/ui/account/request_pwd_verification_page.dart"; import 'package:photos/ui/account/sessions_page.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; @@ -61,7 +64,6 @@ class _SecuritySectionWidgetState extends State { } Widget _getSectionOptions(BuildContext context) { - final bool canDisableMFA = UserService.instance.getCachedUserDetails()?.profileData?.canDisableEmailMFA ?? false; final Completer completer = Completer(); final List children = []; if (_config.hasConfiguredAccount()) { @@ -133,31 +135,28 @@ class _SecuritySectionWidgetState extends State { }, ), ), - if (canDisableMFA) sectionOptionSpacing, - if (canDisableMFA) - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: S.of(context).emailVerificationToggle, - ), - trailingWidget: ToggleSwitchWidget( - value: () => UserService.instance.hasEmailMFAEnabled(), - onChanged: () async { - final hasAuthenticated = await LocalAuthenticationService.instance - .requestLocalAuthentication( - context, - S.of(context).authToChangeEmailVerificationSetting, - ); - final isEmailMFAEnabled = - UserService.instance.hasEmailMFAEnabled(); - if (hasAuthenticated) { - await updateEmailMFA(!isEmailMFAEnabled); - /*if (mounted) { - setState(() {}); - }*/ - } - }, - ), + sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: CaptionedTextWidget( + title: S.of(context).emailVerificationToggle, + ), + trailingWidget: ToggleSwitchWidget( + value: () => UserService.instance.hasEmailMFAEnabled(), + onChanged: () async { + final hasAuthenticated = await LocalAuthenticationService + .instance + .requestLocalAuthentication( + context, + S.of(context).authToChangeEmailVerificationSetting, + ); + final isEmailMFAEnabled = + UserService.instance.hasEmailMFAEnabled(); + if (hasAuthenticated) { + await updateEmailMFA(!isEmailMFAEnabled); + } + }, ), + ), sectionOptionSpacing, ], ); @@ -263,6 +262,13 @@ class _SecuritySectionWidgetState extends State { } Future updateEmailMFA(bool isEnabled) async { try { + final UserDetails details = await UserService.instance.getUserDetailsV2(memoryCount: false); + if((details.profileData?.canDisableEmailMFA ?? false) == false) { + await routeToPage(context, RequestPasswordVerificationPage(onPasswordVerified: (Uint8List keyEncryptionKey) async { + final Uint8List loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey); + await UserService.instance.registerOrUpdateSrp(loginKey); + },),); + } await UserService.instance.updateEmailMFA(isEnabled); } catch (e) { showToast(context, S.of(context).somethingWentWrong);