|
@@ -0,0 +1,166 @@
|
|
|
+import 'dart:async';
|
|
|
+import 'dart:convert';
|
|
|
+import 'dart:io';
|
|
|
+
|
|
|
+import 'package:ente_auth/l10n/l10n.dart';
|
|
|
+import 'package:ente_auth/models/code.dart';
|
|
|
+import 'package:ente_auth/models/export/ente.dart';
|
|
|
+import 'package:ente_auth/services/authenticator_service.dart';
|
|
|
+import 'package:ente_auth/store/code_store.dart';
|
|
|
+import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
|
|
+import 'package:ente_auth/ui/components/dialog_widget.dart';
|
|
|
+import 'package:ente_auth/ui/components/models/button_type.dart';
|
|
|
+import 'package:ente_auth/utils/crypto_util.dart';
|
|
|
+import 'package:ente_auth/utils/dialog_util.dart';
|
|
|
+import 'package:ente_auth/utils/toast_util.dart';
|
|
|
+import 'package:file_picker/file_picker.dart';
|
|
|
+import 'package:flutter/cupertino.dart';
|
|
|
+import 'package:flutter/foundation.dart';
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'package:flutter_sodium/flutter_sodium.dart';
|
|
|
+import 'package:logging/logging.dart';
|
|
|
+
|
|
|
+Future<void> showEncryptedImportInstruction(BuildContext context) async {
|
|
|
+ final l10n = context.l10n;
|
|
|
+ final result = await showDialogWidget(
|
|
|
+ context: context,
|
|
|
+ title: l10n.importFromApp("ente Auth"),
|
|
|
+ body: l10n.importEnteEncGuide,
|
|
|
+ buttons: [
|
|
|
+ ButtonWidget(
|
|
|
+ buttonType: ButtonType.primary,
|
|
|
+ labelText: l10n.importSelectJsonFile,
|
|
|
+ isInAlert: true,
|
|
|
+ buttonSize: ButtonSize.large,
|
|
|
+ buttonAction: ButtonAction.first,
|
|
|
+ ),
|
|
|
+ ButtonWidget(
|
|
|
+ buttonType: ButtonType.secondary,
|
|
|
+ labelText: context.l10n.cancel,
|
|
|
+ buttonSize: ButtonSize.large,
|
|
|
+ isInAlert: true,
|
|
|
+ buttonAction: ButtonAction.second,
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ if (result?.action != null && result!.action != ButtonAction.cancel) {
|
|
|
+ if (result.action == ButtonAction.first) {
|
|
|
+ await _pickEnteJsonFile(context);
|
|
|
+ } else {}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+Future<void> _decryptExportData(
|
|
|
+ BuildContext context,
|
|
|
+ EnteAuthExport enteAuthExport, {
|
|
|
+ String? password,
|
|
|
+}) async {
|
|
|
+ final l10n = context.l10n;
|
|
|
+ bool isPasswordIncorrect = false;
|
|
|
+ int? importedCodeCount;
|
|
|
+ await showTextInputDialog(
|
|
|
+ context,
|
|
|
+ title: l10n.passwordForDecryptingExport,
|
|
|
+ submitButtonLabel: l10n.importLabel,
|
|
|
+ hintText: l10n.enterYourPasswordHint,
|
|
|
+ isPasswordInput: true,
|
|
|
+ alwaysShowSuccessState: false,
|
|
|
+ showOnlyLoadingState: true,
|
|
|
+ onSubmit: (String password) async {
|
|
|
+ if (password.isEmpty) {
|
|
|
+ showToast(context, l10n.passwordEmptyError);
|
|
|
+ Future.delayed(const Duration(seconds: 0), () {
|
|
|
+ _decryptExportData(context, enteAuthExport, password: password);
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (password.isNotEmpty) {
|
|
|
+ final progressDialog = createProgressDialog(context, l10n.pleaseWait);
|
|
|
+ try {
|
|
|
+ await progressDialog.show();
|
|
|
+ final derivedKey = await CryptoUtil.deriveKey(
|
|
|
+ utf8.encode(password) as Uint8List,
|
|
|
+ Sodium.base642bin(enteAuthExport.kdfParams.salt),
|
|
|
+ enteAuthExport.kdfParams.memLimit,
|
|
|
+ enteAuthExport.kdfParams.opsLimit,
|
|
|
+ );
|
|
|
+ Uint8List? decryptedContent;
|
|
|
+ // Encrypt the key with this derived key
|
|
|
+ try {
|
|
|
+ decryptedContent = await CryptoUtil.decryptChaCha(
|
|
|
+ Sodium.base642bin(enteAuthExport.encryptedData),
|
|
|
+ derivedKey,
|
|
|
+ Sodium.base642bin(enteAuthExport.encryptionNonce),
|
|
|
+ );
|
|
|
+ } catch (e) {
|
|
|
+ showToast(context, l10n.incorrectPasswordTitle);
|
|
|
+ isPasswordIncorrect = true;
|
|
|
+ }
|
|
|
+ if (isPasswordIncorrect) {
|
|
|
+ await progressDialog.hide();
|
|
|
+
|
|
|
+ Future.delayed(const Duration(seconds: 0), () {
|
|
|
+ _decryptExportData(context, enteAuthExport, password: password);
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String content = utf8.decode(decryptedContent!);
|
|
|
+ List<String> splitCodes = content.split("\n");
|
|
|
+ final parsedCodes = [];
|
|
|
+ for (final code in splitCodes) {
|
|
|
+ try {
|
|
|
+ parsedCodes.add(Code.fromRawData(code));
|
|
|
+ } catch (e) {
|
|
|
+ Logger('EncryptedText').severe("Could not parse code", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (final code in parsedCodes) {
|
|
|
+ await CodeStore.instance.addCode(code, shouldSync: false);
|
|
|
+ }
|
|
|
+ unawaited(AuthenticatorService.instance.sync());
|
|
|
+ importedCodeCount = parsedCodes.length;
|
|
|
+ await progressDialog.hide();
|
|
|
+ } catch (e, s) {
|
|
|
+ await progressDialog.hide();
|
|
|
+ Logger("ExportWidget").severe(e, s);
|
|
|
+ showToast(context, "Error while exporting codes.");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ );
|
|
|
+ if (importedCodeCount != null) {
|
|
|
+ final DialogWidget dialog = choiceDialog(
|
|
|
+ title: context.l10n.importSuccessTitle,
|
|
|
+ body: context.l10n.importSuccessDesc(importedCodeCount!),
|
|
|
+ firstButtonLabel: l10n.ok,
|
|
|
+ firstButtonType: ButtonType.primary,
|
|
|
+ );
|
|
|
+ await showConfettiDialog(
|
|
|
+ context: context,
|
|
|
+ dialogBuilder: (BuildContext context) {
|
|
|
+ return dialog;
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+Future<void> _pickEnteJsonFile(BuildContext context) async {
|
|
|
+ FilePickerResult? result = await FilePicker.platform.pickFiles();
|
|
|
+ if (result == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ File file = File(result.files.single.path!);
|
|
|
+ final jsonString = await file.readAsString();
|
|
|
+ EnteAuthExport exportedData =
|
|
|
+ EnteAuthExport.fromJson(jsonDecode(jsonString));
|
|
|
+ await _decryptExportData(context, exportedData);
|
|
|
+ } catch (e) {
|
|
|
+ await showErrorDialog(
|
|
|
+ context,
|
|
|
+ context.l10n.sorry,
|
|
|
+ context.l10n.importFailureDesc,
|
|
|
+ );
|
|
|
+ }
|
|
|
+}
|