Browse Source

Merge branch 'main' into show_add_on_validity

Neeraj Gupta 1 năm trước cách đây
mục cha
commit
f16c7d0aad

+ 3 - 1
lib/core/errors.dart

@@ -7,8 +7,8 @@ enum InvalidReason {
   livePhotoVideoMissing,
   thumbnailMissing,
   unknown,
-
 }
+
 extension InvalidReasonExn on InvalidReason {
   bool get isLivePhotoErr =>
       this == InvalidReason.livePhotoToImageTypeChanged ||
@@ -73,6 +73,8 @@ class InvalidStateError extends AssertionError {
 
 class KeyDerivationError extends Error {}
 
+class LoginKeyDerivationError extends Error {}
+
 class SrpSetupNotCompleteError extends Error {}
 
 class SharingNotPermittedForFreeAccountsError extends Error {}

+ 3 - 0
lib/db/files_db.dart

@@ -699,6 +699,7 @@ class FilesDB {
   Future<List<EnteFile>> getFilesCreatedWithinDurations(
     List<List<int>> durations,
     Set<int> ignoredCollectionIDs, {
+    int? visibility,
     String order = 'ASC',
   }) async {
     if (durations.isEmpty) {
@@ -714,6 +715,8 @@ class FilesDB {
           ")";
       if (index != durations.length - 1) {
         whereClause += " OR ";
+      } else if (visibility != null) {
+        whereClause += ' AND $columnMMdVisibility = $visibility';
       }
     }
     whereClause += ")";

+ 5 - 0
lib/generated/intl/messages_en.dart

@@ -884,6 +884,8 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("No hidden photos or videos"),
         "noImagesWithLocation":
             MessageLookupByLibrary.simpleMessage("No images with location"),
+        "noInternetConnection":
+            MessageLookupByLibrary.simpleMessage("No internet connection"),
         "noPhotosAreBeingBackedUpRightNow":
             MessageLookupByLibrary.simpleMessage(
                 "No photos are being backed up right now"),
@@ -954,6 +956,9 @@ class MessageLookup extends MessageLookupByLibrary {
         "playStoreFreeTrialValidTill": m35,
         "playstoreSubscription":
             MessageLookupByLibrary.simpleMessage("PlayStore subscription"),
+        "pleaseCheckYourInternetConnectionAndTryAgain":
+            MessageLookupByLibrary.simpleMessage(
+                "Please check your internet connection and try again."),
         "pleaseContactSupportAndWeWillBeHappyToHelp":
             MessageLookupByLibrary.simpleMessage(
                 "Please contact support@ente.io and we will be happy to help!"),

+ 4 - 4
lib/generated/intl/messages_fr.dart

@@ -140,7 +140,7 @@ class MessageLookup extends MessageLookupByLibrary {
 
   static String m41(endDate) => "Renouvellement le ${endDate}";
 
-  static String m65(count) =>
+  static String m64(count) =>
       "${Intl.plural(count, one: '${count} résultat trouvé', other: '${count} résultats trouvés')}";
 
   static String m42(count) => "${count} sélectionné(s)";
@@ -192,7 +192,7 @@ class MessageLookup extends MessageLookupByLibrary {
   static String m59(count) =>
       "${Intl.plural(count, zero: '0 jour', one: '1 jour', other: '${count} jours')}";
 
-  static String m66(endDate) => "Valable jusqu\'au ${endDate}";
+  static String m65(endDate) => "Valable jusqu\'au ${endDate}";
 
   static String m60(email) => "Vérifier ${email}";
 
@@ -1193,7 +1193,7 @@ class MessageLookup extends MessageLookupByLibrary {
             "Grouper les photos qui sont prises dans un certain angle d\'une photo"),
         "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage(
             "Invitez des gens, et vous verrez ici toutes les photos qu\'ils partagent"),
-        "searchResultCount": m65,
+        "searchResultCount": m64,
         "security": MessageLookupByLibrary.simpleMessage("Sécurité"),
         "selectAlbum":
             MessageLookupByLibrary.simpleMessage("Sélectionner album"),
@@ -1463,7 +1463,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "useSelectedPhoto": MessageLookupByLibrary.simpleMessage(
             "Utiliser la photo sélectionnée"),
         "usedSpace": MessageLookupByLibrary.simpleMessage("Mémoire utilisée"),
-        "validTill": m66,
+        "validTill": m65,
         "verificationFailedPleaseTryAgain":
             MessageLookupByLibrary.simpleMessage(
                 "La vérification a échouée, veuillez réessayer"),

+ 4 - 4
lib/generated/intl/messages_zh.dart

@@ -127,7 +127,7 @@ class MessageLookup extends MessageLookupByLibrary {
 
   static String m41(endDate) => "在 ${endDate} 前续费";
 
-  static String m65(count) =>
+  static String m64(count) =>
       "${Intl.plural(count, other: '已找到 ${count} 个结果')}";
 
   static String m42(count) => "已选择 ${count} 个";
@@ -173,7 +173,7 @@ class MessageLookup extends MessageLookupByLibrary {
   static String m59(count) =>
       "${Intl.plural(count, zero: '', one: '1天', other: '${count} 天')}";
 
-  static String m66(endDate) => "有效期至 ${endDate}";
+  static String m65(endDate) => "有效期至 ${endDate}";
 
   static String m60(email) => "验证 ${email}";
 
@@ -944,7 +944,7 @@ class MessageLookup extends MessageLookupByLibrary {
             MessageLookupByLibrary.simpleMessage("在照片的一定半径内拍摄的几组照片"),
         "searchPeopleEmptySection":
             MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"),
-        "searchResultCount": m65,
+        "searchResultCount": m64,
         "security": MessageLookupByLibrary.simpleMessage("安全"),
         "selectAlbum": MessageLookupByLibrary.simpleMessage("选择相册"),
         "selectAll": MessageLookupByLibrary.simpleMessage("全选"),
@@ -1155,7 +1155,7 @@ class MessageLookup extends MessageLookupByLibrary {
         "useRecoveryKey": MessageLookupByLibrary.simpleMessage("使用恢复密钥"),
         "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("使用所选照片"),
         "usedSpace": MessageLookupByLibrary.simpleMessage("已用空间"),
-        "validTill": m66,
+        "validTill": m65,
         "verificationFailedPleaseTryAgain":
             MessageLookupByLibrary.simpleMessage("验证失败,请重试"),
         "verificationId": MessageLookupByLibrary.simpleMessage("验证 ID"),

+ 20 - 1
lib/generated/l10n.dart

@@ -1,7 +1,6 @@
 // GENERATED CODE - DO NOT MODIFY BY HAND
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
-
 import 'intl/messages_all.dart';
 
 // **************************************************************************
@@ -8078,6 +8077,26 @@ class S {
       args: [],
     );
   }
+
+  /// `No internet connection`
+  String get noInternetConnection {
+    return Intl.message(
+      'No internet connection',
+      name: 'noInternetConnection',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Please check your internet connection and try again.`
+  String get pleaseCheckYourInternetConnectionAndTryAgain {
+    return Intl.message(
+      'Please check your internet connection and try again.',
+      name: 'pleaseCheckYourInternetConnectionAndTryAgain',
+      desc: '',
+      args: [],
+    );
+  }
 }
 
 class AppLocalizationDelegate extends LocalizationsDelegate<S> {

+ 3 - 1
lib/l10n/intl_en.arb

@@ -1150,5 +1150,7 @@
   "@addNew": {
     "description": "Text to add a new item (location tag, album, caption etc)"
   },
-  "contacts": "Contacts"
+  "contacts": "Contacts",
+  "noInternetConnection": "No internet connection",
+  "pleaseCheckYourInternetConnectionAndTryAgain": "Please check your internet connection and try again."
 }

+ 3 - 1
lib/l10n/intl_zh.arb

@@ -1149,5 +1149,7 @@
   "@addNew": {
     "description": "Text to add a new item (location tag, album, caption etc)"
   },
-  "contacts": "联系人"
+  "contacts": "联系人",
+  "noInternetConnection": "无互联网连接",
+  "pleaseCheckYourInternetConnectionAndTryAgain": "请检查您的互联网连接,然后重试。"
 }

+ 2 - 0
lib/services/memories_service.dart

@@ -8,6 +8,7 @@ import "package:photos/events/files_updated_event.dart";
 import "package:photos/events/memories_setting_changed.dart";
 import 'package:photos/models/filters/important_items_filter.dart';
 import 'package:photos/models/memory.dart';
+import "package:photos/models/metadata/common_keys.dart";
 import 'package:photos/services/collections_service.dart';
 import "package:shared_preferences/shared_preferences.dart";
 
@@ -108,6 +109,7 @@ class MemoriesService extends ChangeNotifier {
     final files = await _filesDB.getFilesCreatedWithinDurations(
       durations,
       ignoredCollections,
+      visibility: visibleVisibility,
     );
     final seenTimes = await _memoriesDB.getSeenTimes();
     final List<Memory> memories = [];

+ 75 - 126
lib/services/user_service.dart

@@ -34,11 +34,10 @@ import "package:photos/ui/account/recovery_page.dart";
 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/common/progress_dialog.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";
 import 'package:photos/utils/navigation_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import "package:pointycastle/export.dart";
@@ -586,120 +585,92 @@ class UserService {
     BuildContext context,
     SrpAttributes srpAttributes,
     String userPassword,
+    ProgressDialog dialog,
   ) async {
-    final dialog = createProgressDialog(
-      context,
-      S.of(context).pleaseWait,
-      isDismissible: true,
-    );
-    await dialog.show();
     late Uint8List keyEncryptionKey;
-    try {
-      keyEncryptionKey = await CryptoUtil.deriveKey(
-        utf8.encode(userPassword) as Uint8List,
-        CryptoUtil.base642bin(srpAttributes.kekSalt),
-        srpAttributes.memLimit,
-        srpAttributes.opsLimit,
-      );
-      final loginKey = await CryptoUtil.deriveLoginKey(keyEncryptionKey);
-      final Uint8List identity = Uint8List.fromList(
-        utf8.encode(srpAttributes.srpUserID),
-      );
-      final Uint8List salt = base64Decode(srpAttributes.srpSalt);
-      final Uint8List password = loginKey;
-      final SecureRandom random = _getSecureRandom();
+    _logger.finest('Start deriving key');
+    keyEncryptionKey = await CryptoUtil.deriveKey(
+      utf8.encode(userPassword) as Uint8List,
+      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 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.encodeBigInt(A!)),
-        },
-      );
-      final String sessionID = createSessionResponse.data["sessionID"];
-      final String srpB = createSessionResponse.data["srpB"];
+    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.encodeBigInt(A!)),
+      },
+    );
+    final String sessionID = createSessionResponse.data["sessionID"];
+    final String srpB = createSessionResponse.data["srpB"];
 
-      final serverB = SRP6Util.decodeBigInt(base64Decode(srpB));
-      // ignore: need to calculate secret to get M1, 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.encodeBigInt(clientM!)),
-        },
-      );
-      if (response.statusCode == 200) {
-        Widget page;
-        final String twoFASessionID = response.data["twoFactorSessionID"];
-        Configuration.instance.setVolatilePassword(userPassword);
-        if (twoFASessionID.isNotEmpty) {
-          setTwoFactor(value: true);
-          page = TwoFactorAuthenticationPage(twoFASessionID);
-        } else {
-          await _saveConfiguration(response);
-          if (Configuration.instance.getEncryptedToken() != null) {
-            await Configuration.instance.decryptSecretsAndGetKeyEncKey(
-              userPassword,
-              Configuration.instance.getKeyAttributes()!,
-              keyEncryptionKey: keyEncryptionKey,
-            );
-            page = const HomeWidget();
-          } else {
-            throw Exception("unexpected response during email verification");
-          }
-        }
-        await dialog.hide();
-        if (page is HomeWidget) {
-          Navigator.of(context).popUntil((route) => route.isFirst);
-          Bus.instance.fire(AccountConfiguredEvent());
-        } else {
-          Navigator.of(context).pushAndRemoveUntil(
-            MaterialPageRoute(
-              builder: (BuildContext context) {
-                return page;
-              },
-            ),
-            (route) => route.isFirst,
+    final serverB = SRP6Util.decodeBigInt(base64Decode(srpB));
+    // ignore: need to calculate secret to get M1, 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.encodeBigInt(clientM!)),
+      },
+    );
+    if (response.statusCode == 200) {
+      Widget page;
+      final String twoFASessionID = response.data["twoFactorSessionID"];
+      Configuration.instance.setVolatilePassword(userPassword);
+      if (twoFASessionID.isNotEmpty) {
+        setTwoFactor(value: true);
+        page = TwoFactorAuthenticationPage(twoFASessionID);
+      } else {
+        await _saveConfiguration(response);
+        if (Configuration.instance.getEncryptedToken() != null) {
+          await Configuration.instance.decryptSecretsAndGetKeyEncKey(
+            userPassword,
+            Configuration.instance.getKeyAttributes()!,
+            keyEncryptionKey: keyEncryptionKey,
           );
+          page = const HomeWidget();
+        } else {
+          throw Exception("unexpected response during email verification");
         }
-      } else {
-        // should never reach here
-        throw Exception("unexpected response during email verification");
       }
-    } on DioError catch (e, s) {
       await dialog.hide();
-      if (e.response != null && e.response!.statusCode == 401) {
-        await _showContactSupportDialog(
-          context,
-          S.of(context).incorrectPasswordTitle,
-          S.of(context).pleaseTryAgain,
-        );
+      if (page is HomeWidget) {
+        Navigator.of(context).popUntil((route) => route.isFirst);
+        Bus.instance.fire(AccountConfiguredEvent());
       } else {
-        _logger.severe('failed to verify password', e, s);
-        await _showContactSupportDialog(
-          context,
-          S.of(context).oops,
-          S.of(context).verificationFailedPleaseTryAgain,
+        Navigator.of(context).pushAndRemoveUntil(
+          MaterialPageRoute(
+            builder: (BuildContext context) {
+              return page;
+            },
+          ),
+          (route) => route.isFirst,
         );
       }
-    } catch (e, s) {
-      _logger.severe('failed to verify password', e, s);
-      await dialog.hide();
-      await _showContactSupportDialog(
-        context,
-        S.of(context).oops,
-        S.of(context).verificationFailedPleaseTryAgain,
-      );
+    } else {
+      // should never reach here
+      throw Exception("unexpected response during email verification");
     }
   }
 
@@ -1164,26 +1135,4 @@ class UserService {
       rethrow;
     }
   }
-
-  Future<void> _showContactSupportDialog(
-    BuildContext context,
-    String title,
-    String message,
-  ) async {
-    final dialogChoice = await showChoiceDialog(
-      context,
-      title: title,
-      body: message,
-      firstButtonLabel: S.of(context).contactSupport,
-      secondButtonLabel: S.of(context).ok,
-    );
-    if (dialogChoice!.action == ButtonAction.first) {
-      await sendLogs(
-        context,
-        S.of(context).contactSupport,
-        "support@ente.io",
-        postShare: () {},
-      );
-    }
-  }
 }

+ 107 - 5
lib/ui/account/login_pwd_verification_page.dart

@@ -1,12 +1,17 @@
+import "package:dio/dio.dart";
 import "package:flutter/foundation.dart";
 import 'package:flutter/material.dart';
+import "package:logging/logging.dart";
 import 'package:photos/core/configuration.dart';
+import "package:photos/core/errors.dart";
 import "package:photos/generated/l10n.dart";
 import "package:photos/models/api/user/srp.dart";
 import "package:photos/services/user_service.dart";
 import "package:photos/theme/ente_theme.dart";
 import 'package:photos/ui/common/dynamic_fab.dart';
+import "package:photos/ui/components/buttons/button_widget.dart";
 import "package:photos/utils/dialog_util.dart";
+import "package:photos/utils/email_util.dart";
 
 // LoginPasswordVerificationPage is a page that allows the user to enter their password to verify their identity.
 // If the password is correct, then the user is either directed to
@@ -31,6 +36,7 @@ class _LoginPasswordVerificationPageState
   String? email;
   bool _passwordInFocus = false;
   bool _passwordVisible = false;
+  final Logger _logger = Logger("LoginPasswordVerificationPage");
 
   @override
   void initState() {
@@ -85,11 +91,7 @@ class _LoginPasswordVerificationPageState
         buttonText: S.of(context).logInLabel,
         onPressedFunction: () async {
           FocusScope.of(context).unfocus();
-          await UserService.instance.verifyEmailViaPassword(
-            context,
-            widget.srpAttributes,
-            _passwordController.text,
-          );
+          await verifyPassword(context, _passwordController.text);
         },
       ),
       floatingActionButtonLocation: fabLocation(),
@@ -97,6 +99,106 @@ class _LoginPasswordVerificationPageState
     );
   }
 
+  Future<void> verifyPassword(BuildContext context, String password) async {
+    final dialog = createProgressDialog(
+      context,
+      S.of(context).pleaseWait,
+      isDismissible: true,
+    );
+    await dialog.show();
+    try {
+      await UserService.instance.verifyEmailViaPassword(
+        context,
+        widget.srpAttributes,
+        password,
+        dialog,
+      );
+    } on DioError catch (e, s) {
+      await dialog.hide();
+      if (e.response != null && e.response!.statusCode == 401) {
+        _logger.severe('server reject, failed verify SRP login', e, s);
+        await _showContactSupportDialog(
+          context,
+          S.of(context).incorrectPasswordTitle,
+          S.of(context).pleaseTryAgain,
+        );
+      } else {
+        _logger.severe('API failure during SRP login', e, s);
+        if (e.type == DioErrorType.other) {
+          await _showContactSupportDialog(
+            context,
+            S.of(context).noInternetConnection,
+            S.of(context).pleaseCheckYourInternetConnectionAndTryAgain,
+          );
+        } else {
+          await _showContactSupportDialog(
+            context,
+            S.of(context).somethingWentWrong,
+            S.of(context).verificationFailedPleaseTryAgain,
+          );
+        }
+      }
+    } catch (e, s) {
+      _logger.info('error during loginViaPassword', e);
+      await dialog.hide();
+      if (e is LoginKeyDerivationError) {
+        _logger.severe('loginKey derivation error', e, s);
+        // LoginKey err, perform regular login via ott verification
+        await UserService.instance.sendOtt(
+          context,
+          email!,
+          isCreateAccountScreen: true,
+        );
+        return;
+      } else if (e is KeyDerivationError) {
+        // device is not powerful enough to perform derive key
+        final dialogChoice = await showChoiceDialog(
+          context,
+          title: S.of(context).recreatePasswordTitle,
+          body: S.of(context).recreatePasswordBody,
+          firstButtonLabel: S.of(context).useRecoveryKey,
+        );
+        if (dialogChoice!.action == ButtonAction.first) {
+          await UserService.instance.sendOtt(
+            context,
+            email!,
+            isResetPasswordScreen: true,
+          );
+        }
+        return;
+      } else {
+        _logger.severe('unexpected error while verifying password', e, s);
+        await _showContactSupportDialog(
+          context,
+          S.of(context).oops,
+          S.of(context).verificationFailedPleaseTryAgain,
+        );
+      }
+    }
+  }
+
+  Future<void> _showContactSupportDialog(
+    BuildContext context,
+    String title,
+    String message,
+  ) async {
+    final dialogChoice = await showChoiceDialog(
+      context,
+      title: title,
+      body: message,
+      firstButtonLabel: S.of(context).contactSupport,
+      secondButtonLabel: S.of(context).ok,
+    );
+    if (dialogChoice!.action == ButtonAction.first) {
+      await sendLogs(
+        context,
+        S.of(context).contactSupport,
+        "support@ente.io",
+        postShare: () {},
+      );
+    }
+  }
+
   Widget _getBody() {
     return Column(
       children: [

+ 27 - 6
lib/ui/tools/lock_screen.dart

@@ -1,6 +1,8 @@
+import "dart:io";
+
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
-import "package:photos/generated/l10n.dart";
+import "package:photos/l10n/l10n.dart";
 import 'package:photos/ui/common/gradient_button.dart';
 import 'package:photos/ui/tools/app_lock.dart';
 import 'package:photos/utils/auth_util.dart';
@@ -21,10 +23,16 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
 
   @override
   void initState() {
-    _logger.info("initState");
+    _logger.info("initiatingState");
     super.initState();
-    _showLockScreen(source: "initState");
     WidgetsBinding.instance.addObserver(this);
+    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+      if (isNonMobileIOSDevice()) {
+        _logger.info('ignore init for non mobile iOS device');
+        return;
+      }
+      _showLockScreen(source: "postFrameInit");
+    });
   }
 
   @override
@@ -45,7 +53,7 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
                 SizedBox(
                   width: 180,
                   child: GradientButton(
-                    text: S.of(context).unlock,
+                    text: context.l10n.unlock,
                     iconData: Icons.lock_open_outlined,
                     onTap: () async {
                       _showLockScreen(source: "tapUnlock");
@@ -60,6 +68,14 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
     );
   }
 
+  bool isNonMobileIOSDevice() {
+    if (Platform.isAndroid) {
+      return false;
+    }
+    var shortestSide = MediaQuery.of(context).size.shortestSide;
+    return shortestSide > 600 ? true : false;
+  }
+
   @override
   void didChangeAppLifecycleState(AppLifecycleState state) {
     _logger.info(state.toString());
@@ -73,7 +89,10 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
       if (!_hasAuthenticationFailed && !didAuthInLast5Seconds) {
         // Show the lock screen again only if the app is resuming from the
         // background, and not when the lock screen was explicitly dismissed
-        _showLockScreen(source: "lifeCycle");
+        Future.delayed(
+          Duration.zero,
+          () => _showLockScreen(source: "lifeCycle"),
+        );
       } else {
         _hasAuthenticationFailed = false; // Reset failure state
       }
@@ -90,6 +109,7 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
 
   @override
   void dispose() {
+    _logger.info('disposing');
     WidgetsBinding.instance.removeObserver(this);
     super.dispose();
   }
@@ -101,7 +121,7 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
       _isShowingLockScreen = true;
       final result = await requestAuthentication(
         context,
-        S.of(context).authToViewYourMemories,
+        context.l10n.authToViewYourMemories,
       );
       _logger.finest("LockScreen Result $result $id");
       _isShowingLockScreen = false;
@@ -117,6 +137,7 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
         }
       }
     } catch (e, s) {
+      _isShowingLockScreen = false;
       _logger.severe(e, s);
     }
   }

+ 85 - 66
lib/utils/crypto_util.dart

@@ -77,7 +77,7 @@ Future<Uint8List> cryptoGenericHash(Map<String, dynamic> args) async {
 
 EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
   final initPushResult =
-  Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
+      Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
   final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
     initPushResult.state,
     args["source"],
@@ -102,7 +102,7 @@ Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
   final inputFile = sourceFile.openSync(mode: FileMode.read);
   final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen();
   final initPushResult =
-  Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
+      Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
   var bytesRead = 0;
   var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
   while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
@@ -156,7 +156,7 @@ Future<void> chachaDecryptFile(Map<String, dynamic> args) async {
     final buffer = await inputFile.read(chunkSize);
     bytesRead += chunkSize;
     final pullResult =
-    Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
+        Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
     await destinationFile.writeAsBytes(pullResult.m, mode: FileMode.append);
     tag = pullResult.tag;
   }
@@ -190,20 +190,22 @@ class CryptoUtil {
     Sodium.init();
   }
 
-  static Uint8List base642bin(String b64, {
+  static Uint8List base642bin(
+    String b64, {
     String? ignore,
     int variant = Sodium.base64VariantOriginal,
   }) {
     return Sodium.base642bin(b64, ignore: ignore, variant: variant);
   }
 
-  static String bin2base64(Uint8List bin, {
+  static String bin2base64(
+    Uint8List bin, {
     bool urlSafe = false,
   }) {
     return Sodium.bin2base64(
       bin,
       variant:
-      urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
+          urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
     );
   }
 
@@ -237,9 +239,11 @@ class CryptoUtil {
 
   // Decrypts the given cipher, with the given key and nonce using XSalsa20
   // (w Poly1305 MAC).
-  static Future<Uint8List> decrypt(Uint8List cipher,
-      Uint8List key,
-      Uint8List nonce,) async {
+  static Future<Uint8List> decrypt(
+    Uint8List cipher,
+    Uint8List key,
+    Uint8List nonce,
+  ) async {
     final args = <String, dynamic>{};
     args["cipher"] = cipher;
     args["nonce"] = nonce;
@@ -256,9 +260,11 @@ class CryptoUtil {
   // This function runs on the same thread as the caller, so should be used only
   // for small amounts of data where thread switching can result in a degraded
   // user experience
-  static Uint8List decryptSync(Uint8List cipher,
-      Uint8List key,
-      Uint8List nonce,) {
+  static Uint8List decryptSync(
+    Uint8List cipher,
+    Uint8List key,
+    Uint8List nonce,
+  ) {
     final args = <String, dynamic>{};
     args["cipher"] = cipher;
     args["nonce"] = nonce;
@@ -270,8 +276,10 @@ class CryptoUtil {
   // nonce, using XChaCha20 (w Poly1305 MAC).
   // This function runs on the isolate pool held by `_computer`.
   // TODO: Remove "ChaCha", an implementation detail from the function name
-  static Future<EncryptionResult> encryptChaCha(Uint8List source,
-      Uint8List key,) async {
+  static Future<EncryptionResult> encryptChaCha(
+    Uint8List source,
+    Uint8List key,
+  ) async {
     final args = <String, dynamic>{};
     args["source"] = source;
     args["key"] = key;
@@ -285,9 +293,11 @@ class CryptoUtil {
   // Decrypts the given source, with the given key and header using XChaCha20
   // (w Poly1305 MAC).
   // TODO: Remove "ChaCha", an implementation detail from the function name
-  static Future<Uint8List> decryptChaCha(Uint8List source,
-      Uint8List key,
-      Uint8List header,) async {
+  static Future<Uint8List> decryptChaCha(
+    Uint8List source,
+    Uint8List key,
+    Uint8List header,
+  ) async {
     final args = <String, dynamic>{};
     args["source"] = source;
     args["key"] = key;
@@ -304,10 +314,10 @@ class CryptoUtil {
   // to the destinationFilePath.
   // If a key is not provided, one is generated and returned.
   static Future<EncryptionResult> encryptFile(
-      String sourceFilePath,
-      String destinationFilePath, {
-      Uint8List? key,
-      }) {
+    String sourceFilePath,
+    String destinationFilePath, {
+    Uint8List? key,
+  }) {
     final args = <String, dynamic>{};
     args["sourceFilePath"] = sourceFilePath;
     args["destinationFilePath"] = destinationFilePath;
@@ -322,10 +332,11 @@ class CryptoUtil {
   // Decrypts the file at sourceFilePath, with the given key and header using
   // XChaCha20 (w Poly1305 MAC), and writes it to the destinationFilePath.
   static Future<void> decryptFile(
-      String sourceFilePath,
-      String destinationFilePath,
-      Uint8List header,
-      Uint8List key,) {
+    String sourceFilePath,
+    String destinationFilePath,
+    Uint8List header,
+    Uint8List key,
+  ) {
     final args = <String, dynamic>{};
     args["sourceFilePath"] = sourceFilePath;
     args["destinationFilePath"] = destinationFilePath;
@@ -356,10 +367,10 @@ class CryptoUtil {
 
   // Decrypts the input using the given publicKey-secretKey pair
   static Uint8List openSealSync(
-      Uint8List input,
-      Uint8List publicKey,
-      Uint8List secretKey,
-      ) {
+    Uint8List input,
+    Uint8List publicKey,
+    Uint8List secretKey,
+  ) {
     return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
   }
 
@@ -377,9 +388,9 @@ class CryptoUtil {
   // At all points, we ensure that the product of these two variables (the area
   // under the graph that determines the amount of work required) is a constant.
   static Future<DerivedKeyResult> deriveSensitiveKey(
-      Uint8List password,
-      Uint8List salt,
-      ) async {
+    Uint8List password,
+    Uint8List salt,
+  ) async {
     final logger = Logger("pwhash");
     int memLimit = Sodium.cryptoPwhashMemlimitSensitive;
     int opsLimit = Sodium.cryptoPwhashOpslimitSensitive;
@@ -407,7 +418,10 @@ class CryptoUtil {
         return DerivedKeyResult(key, memLimit, opsLimit);
       } catch (e, s) {
         logger.warning(
-          "failed to deriveKey mem: $memLimit, ops: $opsLimit", e, s,);
+          "failed to deriveKey mem: $memLimit, ops: $opsLimit",
+          e,
+          s,
+        );
       }
       memLimit = (memLimit / 2).round();
       opsLimit = opsLimit * 2;
@@ -421,9 +435,9 @@ class CryptoUtil {
   // extra layer of authentication (atop the access token and collection key).
   // More details @ https://ente.io/blog/building-shareable-links/
   static Future<DerivedKeyResult> deriveInteractiveKey(
-      Uint8List password,
-      Uint8List salt,
-      ) async {
+    Uint8List password,
+    Uint8List salt,
+  ) async {
     final int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
     final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
     final key = await deriveKey(password, salt, memLimit, opsLimit);
@@ -433,23 +447,23 @@ class CryptoUtil {
   // Derives a key for a given password, salt, memLimit and opsLimit using
   // Argon2id, v1.3.
   static Future<Uint8List> deriveKey(
-      Uint8List password,
-      Uint8List salt,
-      int memLimit,
-      int opsLimit,
-      ) {
+    Uint8List password,
+    Uint8List salt,
+    int memLimit,
+    int opsLimit,
+  ) {
     try {
-    return _computer.compute(
-      cryptoPwHash,
-      param: {
-        "password": password,
-        "salt": salt,
-        "memLimit": memLimit,
-        "opsLimit": opsLimit,
-      },
-      taskName: "deriveKey",
-    );
-    } catch(e,s) {
+      return _computer.compute(
+        cryptoPwHash,
+        param: {
+          "password": password,
+          "salt": salt,
+          "memLimit": memLimit,
+          "opsLimit": opsLimit,
+        },
+        taskName: "deriveKey",
+      );
+    } catch (e, s) {
       final String errMessage = 'failed to deriveKey memLimit: $memLimit and '
           'opsLimit: $opsLimit';
       Logger("CryptoUtilDeriveKey").warning(errMessage, e, s);
@@ -461,20 +475,25 @@ class CryptoUtil {
   // (Key Derivation Function) with the `loginSubKeyId` and
   // `loginSubKeyLen` and `loginSubKeyContext` as context
   static Future<Uint8List> deriveLoginKey(
-      Uint8List key,
-      ) async {
-    final Uint8List derivedKey = await  _computer.compute(
-      cryptoKdfDeriveFromKey,
-      param: {
-        "key": key,
-        "subkeyId": loginSubKeyId,
-        "subkeyLen": loginSubKeyLen,
-        "context": utf8.encode(loginSubKeyContext),
-      },
-      taskName: "deriveLoginKey",
-    );
-    // return the first 16 bytes of the derived key
-    return derivedKey.sublist(0, 16);
+    Uint8List key,
+  ) async {
+    try {
+      final Uint8List derivedKey = await _computer.compute(
+        cryptoKdfDeriveFromKey,
+        param: {
+          "key": key,
+          "subkeyId": loginSubKeyId,
+          "subkeyLen": loginSubKeyLen,
+          "context": utf8.encode(loginSubKeyContext),
+        },
+        taskName: "deriveLoginKey",
+      );
+      // return the first 16 bytes of the derived key
+      return derivedKey.sublist(0, 16);
+    } catch (e, s) {
+      Logger("deriveLoginKey").severe("loginKeyDerivation failed", e, s);
+      throw LoginKeyDerivationError();
+    }
   }
 
   // Computes and returns the hash of the source file