浏览代码

Support for importing ente encrypted export

Neeraj Gupta 1 年之前
父节点
当前提交
46d96ef779

+ 4 - 0
lib/l10n/arb/app_en.arb

@@ -75,10 +75,14 @@
   "importCodes": "Import codes",
   "importTypePlainText": "Plain text",
   "importTypeEnteEncrypted": "ente Encrypted export",
+  "passwordForDecryptingExport" : "Password to decrypt export",
+  "passwordEmptyError": "Password can not be empty",
   "importFromApp": "Import codes from {appName}",
   "importSelectJsonFile": "Select JSON file",
+  "importEnteEncGuide": "Select the encrypted JSON file exported from ente",
   "importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.",
   "exportCodes": "Export codes",
+  "importLabel": "Import",
   "importInstruction": "Please select a file that contains a list of your codes in the following format",
   "importCodeDelimiterInfo": "The codes can be separated by a comma or a new line",
   "selectFile": "Select file",

+ 166 - 0
lib/ui/settings/data/import/encrypted_ente_import.dart

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

+ 3 - 3
lib/ui/settings/data/import/import_service.dart

@@ -1,8 +1,8 @@
 
+import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
 import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
-import 'package:ente_auth/ui/settings/data/import/ravio_plain_text_import.dart';
+import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart';
 import 'package:ente_auth/ui/settings/data/import_page.dart';
-import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/cupertino.dart';
 
 class ImportService {
@@ -17,7 +17,7 @@ class ImportService {
     } else if(type == ImportType.ravio) {
       showRaivoImportInstruction(context);
     } else {
-      showToast(context, 'Coming soon!');
+      showEncryptedImportInstruction(context);
     }
   }
 }

+ 1 - 1
lib/ui/settings/data/import/ravio_plain_text_import.dart → lib/ui/settings/data/import/raivo_plain_text_import.dart

@@ -21,7 +21,7 @@ Future<void> showRaivoImportInstruction(BuildContext context) async {
     title: l10n.importFromApp("Raivo OTP"),
     body: l10n.importRaivoGuide,
     buttons: [
-       ButtonWidget(
+      ButtonWidget(
         buttonType: ButtonType.primary,
         labelText: l10n.importSelectJsonFile,
         isInAlert: true,

+ 61 - 58
lib/ui/settings/data/import_page.dart

@@ -38,68 +38,71 @@ class ImportCodePage extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Scaffold(
-      body: CustomScrollView(
-        primary: false,
-        slivers: <Widget>[
-          TitleBarWidget(
-            flexibleSpaceTitle: TitleBarTitleWidget(
-              title: context.l10n.importCodes,
+    return Material(
+      color: Colors.transparent,
+      child: Scaffold(
+        body: CustomScrollView(
+          primary: false,
+          slivers: <Widget>[
+            TitleBarWidget(
+              flexibleSpaceTitle: TitleBarTitleWidget(
+                title: context.l10n.importCodes,
+              ),
+              flexibleSpaceCaption: "Import source",
+              actionIcons: [
+                IconButtonWidget(
+                  icon: Icons.close_outlined,
+                  iconButtonType: IconButtonType.secondary,
+                  onTap: () {
+                    Navigator.pop(context);
+                    Navigator.pop(context);
+                  },
+                ),
+              ],
             ),
-            flexibleSpaceCaption: "Import source",
-            actionIcons: [
-              IconButtonWidget(
-                icon: Icons.close_outlined,
-                iconButtonType: IconButtonType.secondary,
-                onTap: () {
-                  Navigator.pop(context);
-                  Navigator.pop(context);
+            SliverList(
+              delegate: SliverChildBuilderDelegate(
+                (delegateBuildContext, index) {
+                  final type = importOptions[index];
+                  return Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
+                    child: Column(
+                      children: [
+                        if (index == 0)
+                          const SizedBox(
+                            height: 24,
+                          ),
+                        MenuItemWidget(
+                          captionedTextWidget: CaptionedTextWidget(
+                            title: getTitle(context, type),
+                          ),
+                          alignCaptionedTextToLeft: true,
+                          menuItemColor: getEnteColorScheme(context).fillFaint,
+                          pressedColor: getEnteColorScheme(context).fillFaint,
+                          trailingIcon: Icons.chevron_right_outlined,
+                          isBottomBorderRadiusRemoved:
+                              index != importOptions.length - 1,
+                          isTopBorderRadiusRemoved: index != 0,
+                          onTap: () async {
+                            ImportService().initiateImport(context, type);
+                            // routeToPage(context, ImportCodePage());
+                            // _showImportInstructionDialog(context);
+                          },
+                        ),
+                        if (index != importOptions.length - 1)
+                          DividerWidget(
+                            dividerType: DividerType.menu,
+                            bgColor: getEnteColorScheme(context).fillFaint,
+                          ),
+                      ],
+                    ),
+                  );
                 },
+                childCount: importOptions.length,
               ),
-            ],
-          ),
-          SliverList(
-            delegate: SliverChildBuilderDelegate(
-              (delegateBuildContext, index) {
-                final type = importOptions[index];
-                return Padding(
-                  padding: const EdgeInsets.symmetric(horizontal: 16.0),
-                  child: Column(
-                    children: [
-                      if (index == 0)
-                        const SizedBox(
-                          height: 24,
-                        ),
-                      MenuItemWidget(
-                        captionedTextWidget: CaptionedTextWidget(
-                          title: getTitle(context, type),
-                        ),
-                        alignCaptionedTextToLeft: true,
-                        menuItemColor: getEnteColorScheme(context).fillFaint,
-                        pressedColor: getEnteColorScheme(context).fillFaint,
-                        trailingIcon: Icons.chevron_right_outlined,
-                        isBottomBorderRadiusRemoved:
-                            index != importOptions.length - 1,
-                        isTopBorderRadiusRemoved: index != 0,
-                        onTap: () async {
-                          ImportService().initiateImport(context, type);
-                          // routeToPage(context, ImportCodePage());
-                          // _showImportInstructionDialog(context);
-                        },
-                      ),
-                      if (index != importOptions.length - 1)
-                        DividerWidget(
-                          dividerType: DividerType.menu,
-                          bgColor: getEnteColorScheme(context).fillFaint,
-                        ),
-                    ],
-                  ),
-                );
-              },
-              childCount: importOptions.length,
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }

+ 23 - 20
lib/utils/dialog_util.dart

@@ -310,26 +310,29 @@ Future<dynamic> showTextInputDialog(
     builder: (context) {
       final bottomInset = MediaQuery.of(context).viewInsets.bottom;
       final isKeyboardUp = bottomInset > 100;
-      return Center(
-        child: Padding(
-          padding: EdgeInsets.only(bottom: isKeyboardUp ? bottomInset : 0),
-          child: TextInputDialog(
-            title: title,
-            message: message,
-            label: label,
-            body: body,
-            icon: icon,
-            submitButtonLabel: submitButtonLabel,
-            onSubmit: onSubmit,
-            hintText: hintText,
-            prefixIcon: prefixIcon,
-            initialValue: initialValue,
-            alignMessage: alignMessage,
-            maxLength: maxLength,
-            showOnlyLoadingState: showOnlyLoadingState,
-            textCapitalization: textCapitalization,
-            alwaysShowSuccessState: alwaysShowSuccessState,
-            isPasswordInput: isPasswordInput,
+      return Material(
+        color: Colors.transparent,
+        child: Center(
+          child: Padding(
+            padding: EdgeInsets.only(bottom: isKeyboardUp ? bottomInset : 0),
+            child: TextInputDialog(
+              title: title,
+              message: message,
+              label: label,
+              body: body,
+              icon: icon,
+              submitButtonLabel: submitButtonLabel,
+              onSubmit: onSubmit,
+              hintText: hintText,
+              prefixIcon: prefixIcon,
+              initialValue: initialValue,
+              alignMessage: alignMessage,
+              maxLength: maxLength,
+              showOnlyLoadingState: showOnlyLoadingState,
+              textCapitalization: textCapitalization,
+              alwaysShowSuccessState: alwaysShowSuccessState,
+              isPasswordInput: isPasswordInput,
+            ),
           ),
         ),
       );