diff --git a/lib/core/errors.dart b/lib/core/errors.dart index 634988e0d..a9e175b88 100644 --- a/lib/core/errors.dart +++ b/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 {} diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 8f6c7a6b7..bf5b5991c 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -699,6 +699,7 @@ class FilesDB { Future> getFilesCreatedWithinDurations( List> durations, Set 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 += ")"; diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index f7425cb81..0ba42a0d0 100644 --- a/lib/generated/intl/messages_en.dart +++ b/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!"), diff --git a/lib/generated/intl/messages_fr.dart b/lib/generated/intl/messages_fr.dart index 4a76e7b4e..79391df74 100644 --- a/lib/generated/intl/messages_fr.dart +++ b/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"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index b90353d47..9370da75e 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/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"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 17dea8919..9dbddb101 100644 --- a/lib/generated/l10n.dart +++ b/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 { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index bd14aa9c6..39ff3f697 100644 --- a/lib/l10n/intl_en.arb +++ b/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." } \ No newline at end of file diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 5478e9c8b..948b143c3 100644 --- a/lib/l10n/intl_zh.arb +++ b/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": "请检查您的互联网连接,然后重试。" } \ No newline at end of file diff --git a/lib/services/memories_service.dart b/lib/services/memories_service.dart index 1b6cbf476..2bfefcb2f 100644 --- a/lib/services/memories_service.dart +++ b/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 memories = []; diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 13d085ca2..e52a69984 100644 --- a/lib/services/user_service.dart +++ b/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 _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: () {}, - ); - } - } } diff --git a/lib/ui/account/login_pwd_verification_page.dart b/lib/ui/account/login_pwd_verification_page.dart index f29827a31..a253b1585 100644 --- a/lib/ui/account/login_pwd_verification_page.dart +++ b/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 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 _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: [ diff --git a/lib/ui/tools/lock_screen.dart b/lib/ui/tools/lock_screen.dart index 781700b62..6b34fda4f 100644 --- a/lib/ui/tools/lock_screen.dart +++ b/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 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 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 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 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 with WidgetsBindingObserver { @override void dispose() { + _logger.info('disposing'); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -101,7 +121,7 @@ class _LockScreenState extends State 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 with WidgetsBindingObserver { } } } catch (e, s) { + _isShowingLockScreen = false; _logger.severe(e, s); } } diff --git a/lib/utils/crypto_util.dart b/lib/utils/crypto_util.dart index c2f02b59b..3c6be311b 100644 --- a/lib/utils/crypto_util.dart +++ b/lib/utils/crypto_util.dart @@ -77,7 +77,7 @@ Future cryptoGenericHash(Map args) async { EncryptionResult chachaEncryptData(Map args) { final initPushResult = - Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]); + Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]); final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push( initPushResult.state, args["source"], @@ -102,7 +102,7 @@ Future chachaEncryptFile(Map 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 chachaDecryptFile(Map 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 decrypt(Uint8List cipher, - Uint8List key, - Uint8List nonce,) async { + static Future decrypt( + Uint8List cipher, + Uint8List key, + Uint8List nonce, + ) async { final args = {}; 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 = {}; 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 encryptChaCha(Uint8List source, - Uint8List key,) async { + static Future encryptChaCha( + Uint8List source, + Uint8List key, + ) async { final args = {}; 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 decryptChaCha(Uint8List source, - Uint8List key, - Uint8List header,) async { + static Future decryptChaCha( + Uint8List source, + Uint8List key, + Uint8List header, + ) async { final args = {}; 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 encryptFile( - String sourceFilePath, - String destinationFilePath, { - Uint8List? key, - }) { + String sourceFilePath, + String destinationFilePath, { + Uint8List? key, + }) { final args = {}; 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 decryptFile( - String sourceFilePath, - String destinationFilePath, - Uint8List header, - Uint8List key,) { + String sourceFilePath, + String destinationFilePath, + Uint8List header, + Uint8List key, + ) { final args = {}; 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 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 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 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 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