Merge branch 'main' into show_add_on_validity
This commit is contained in:
commit
f16c7d0aad
13 changed files with 341 additions and 215 deletions
|
@ -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 {}
|
||||
|
|
|
@ -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
lib/generated/intl/messages_en.dart
generated
5
lib/generated/intl/messages_en.dart
generated
|
@ -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!"),
|
||||
|
|
8
lib/generated/intl/messages_fr.dart
generated
8
lib/generated/intl/messages_fr.dart
generated
|
@ -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"),
|
||||
|
|
8
lib/generated/intl/messages_zh.dart
generated
8
lib/generated/intl/messages_zh.dart
generated
|
@ -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"),
|
||||
|
|
21
lib/generated/l10n.dart
generated
21
lib/generated/l10n.dart
generated
|
@ -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> {
|
||||
|
|
|
@ -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."
|
||||
}
|
|
@ -1149,5 +1149,7 @@
|
|||
"@addNew": {
|
||||
"description": "Text to add a new item (location tag, album, caption etc)"
|
||||
},
|
||||
"contacts": "联系人"
|
||||
"contacts": "联系人",
|
||||
"noInternetConnection": "无互联网连接",
|
||||
"pleaseCheckYourInternetConnectionAndTryAgain": "请检查您的互联网连接,然后重试。"
|
||||
}
|
|
@ -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 = [];
|
||||
|
|
|
@ -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: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue