Merge branch 'main' into show_add_on_validity

This commit is contained in:
Neeraj Gupta 2023-11-20 19:35:15 +05:30
commit f16c7d0aad
13 changed files with 341 additions and 215 deletions

View file

@ -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 {}

View file

@ -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 += ")";

View file

@ -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!"),

View file

@ -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"),

View file

@ -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"),

View file

@ -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> {

View file

@ -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."
}

View file

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

View file

@ -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 = [];

View file

@ -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: () {},
);
}
}
}

View file

@ -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: [

View file

@ -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);
}
}

View file

@ -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