Forráskód Böngészése

Merge conflict: Resolved-2

Muhammed Ayimen 1 éve
szülő
commit
a4df578665

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

@@ -340,6 +340,13 @@
   "showQRAuthMessage": "Authenticate to show QR code",
   "confirmAccountDeleteTitle": "Confirm account deletion",
   "confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
+  "reminderText": "Reminder",
+  "reminderPopupBody": "Please delete the screenshot before resuming any photo cloud sync",
+  "invalidQrCodeText": "Invalid QR code",
+  "googleAuthImagePopupBody": "Please turn off all photo cloud sync from all apps, including iCloud, Google Photo, OneDrive, etc. \nAlso if you have a second smartphone, it is safer to import by scanning QR code.",
+  "importGoogleAuthImageButtonText": "Import from image",
+  "unableToRecognizeQrCodeText": "Unable to recognize a valid code from the uploaded image",
+  "qrCodeImageNotSelectedText": "Qr code image not selected",
   "androidBiometricHint": "Verify identity",
   "@androidBiometricHint": {
     "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."

+ 52 - 87
lib/ui/settings/data/import/analyze_qr_code.dart

@@ -1,9 +1,11 @@
 import 'dart:io';
 
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/code.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/ui/settings/data/import/google_auth_image_import.dart';
 import 'package:ente_auth/ui/settings/data/import/qr_scanner_overlay.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
@@ -21,6 +23,7 @@ class QrScanner extends StatefulWidget {
 
 class _QrScannerState extends State<QrScanner> {
   bool isNavigationPerformed = false;
+  bool isScannedByImage = false;
 
   //Scanner Initialization
   MobileScannerController scannerController = MobileScannerController(
@@ -40,85 +43,40 @@ class _QrScannerState extends State<QrScanner> {
               children: [
                 MobileScanner(
                   controller: scannerController,
-                  onDetect: (capture) {
+                  onDetect: (capture) async {
                     if (!isNavigationPerformed) {
                       isNavigationPerformed = true;
-                      HapticFeedback.vibrate();
-                      showDialog(
-                        barrierDismissible: false,
-                        context: context,
-                        builder: (BuildContext context) {
-                          return AlertDialog(
-                            backgroundColor: Colors.white,
-                            buttonPadding: const EdgeInsets.all(0),
-                            actionsAlignment: MainAxisAlignment.center,
-                            alignment: Alignment.center,
-                            insetPadding: const EdgeInsets.symmetric(
-                              vertical: 24,
-                              horizontal: 24,
-                            ),
-                            shape: RoundedRectangleBorder(
-                              borderRadius: BorderRadius.circular(12),
-                            ),
-                            title: const Text(
-                              'Scan result',
-                              style: TextStyle(
-                                letterSpacing: 0.5,
-                                fontWeight: FontWeight.w600,
-                                fontSize: 18,
-                                color: Colors.black,
-                              ),
-                              textAlign: TextAlign.center,
-                            ),
-                            content: Text(
-                              ' ${capture.barcodes[0].rawValue!}',
-                              style: const TextStyle(
-                                letterSpacing: 0.5,
-                                fontWeight: FontWeight.w600,
-                                fontSize: 15,
-                                color: Colors.black,
-                              ),
-                              textAlign: TextAlign.center,
-                            ),
-                            actions: [
-                              Column(
-                                children: [
-                                  GestureDetector(
-                                    onTap: () {
-                                      Navigator.pop(context);
-                                      isNavigationPerformed = false;
-                                    },
-                                    child: Container(
-                                      decoration: BoxDecoration(
-                                        color: Colors.black,
-                                        borderRadius: BorderRadius.circular(24),
-                                      ),
-                                      child: const Padding(
-                                        padding: EdgeInsets.symmetric(
-                                          horizontal: 20,
-                                          vertical: 8,
-                                        ),
-                                        child: Text(
-                                          'OK',
-                                          style: TextStyle(
-                                            letterSpacing: 0.5,
-                                            fontWeight: FontWeight.w500,
-                                            fontSize: 16,
-                                            color: Colors.white,
-                                          ),
-                                        ),
-                                      ),
-                                    ),
-                                  ),
-                                  const SizedBox(
-                                    height: 30,
-                                  ),
-                                ],
+                      if (capture.barcodes[0].rawValue!
+                          .startsWith(kGoogleAuthExportPrefix)) {
+                        if (isScannedByImage) {
+                          final result = await showDialogWidget(
+                            context: context,
+                            title: l10n.reminderText,
+                            body: l10n.reminderPopupBody,
+                            buttons: [
+                              ButtonWidget(
+                                buttonType: ButtonType.primary,
+                                labelText: l10n.ok,
+                                isInAlert: true,
+                                buttonSize: ButtonSize.large,
+                                buttonAction: ButtonAction.first,
                               ),
                             ],
                           );
-                        },
-                      );
+                          if (result?.action != null &&
+                              result!.action == ButtonAction.first) {
+                            isScannedByImage = false;
+                          }
+                        }
+                        HapticFeedback.vibrate();
+                        List<Code> codes =
+                            parseGoogleAuth(capture.barcodes[0].rawValue!);
+                        scannerController.dispose();
+                        Navigator.of(context).pop(codes);
+                      } else {
+                        showToast(context, l10n.invalidQrCodeText);
+                        isNavigationPerformed = false;
+                      }
                     }
                   },
                 ),
@@ -163,13 +121,14 @@ class _QrScannerState extends State<QrScanner> {
                         onPressed: () async {
                           final result = await showDialogWidget(
                             context: context,
-                            title: l10n.importFromApp("Google Authenticator (saved image)"),
-                            body:
-                                'Please turn off all photo cloud sync from all apps, including iCloud, Google Photo, OneDrive, etc. \nAlso if you have a second smartphone, it is safer to import by scanning QR code.',
+                            title: l10n.importFromApp(
+                              "Google Authenticator (saved image)",
+                            ),
+                            body: l10n.googleAuthImagePopupBody,
                             buttons: [
-                              const ButtonWidget(
+                              ButtonWidget(
                                 buttonType: ButtonType.primary,
-                                labelText: 'Import from image',
+                                labelText: l10n.importGoogleAuthImageButtonText,
                                 isInAlert: true,
                                 buttonSize: ButtonSize.large,
                                 buttonAction: ButtonAction.first,
@@ -191,6 +150,7 @@ class _QrScannerState extends State<QrScanner> {
                                 context,
                                 pickerConfig: const AssetPickerConfig(
                                   maxAssets: 1,
+                                  requestType: RequestType.image,
                                 ),
                               );
 
@@ -201,14 +161,22 @@ class _QrScannerState extends State<QrScanner> {
 
                                 if (await scannerController
                                     .analyzeImage(path)) {
+                                  isScannedByImage = true;
                                   if (!mounted) return;
                                 } else {
                                   if (!mounted) return;
-                                  showToast(context, "Failed to scan image");
+                                  isScannedByImage = false;
+                                  showToast(
+                                    context,
+                                    l10n.unableToRecognizeQrCodeText,
+                                  );
                                 }
                               } else {
                                 if (!mounted) return;
-                                showToast(context, "Image not selected");
+                                showToast(
+                                  context,
+                                  l10n.qrCodeImageNotSelectedText,
+                                );
                               }
                             }
                           }
@@ -250,13 +218,10 @@ class _QrScannerState extends State<QrScanner> {
                           const SizedBox(
                             height: 25,
                           ),
-                          const Text(
-                            'Scan QR code',
+                          Text(
+                            l10n.scanACode,
                             textAlign: TextAlign.center,
-                            style: TextStyle(
-                              letterSpacing: 0.5,
-                              fontWeight: FontWeight.w600,
-                              fontSize: 14,
+                            style: const TextStyle(
                               color: Colors.black,
                             ),
                           ),

+ 133 - 0
lib/ui/settings/data/import/google_auth_image_import.dart

@@ -0,0 +1,133 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:base32/base32.dart';
+import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/code.dart';
+import 'package:ente_auth/models/protos/googleauth.pb.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/ui/settings/data/import/analyze_qr_code.dart';
+import 'package:ente_auth/ui/settings/data/import/import_success.dart';
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+
+const kGoogleAuthExportPrefix = 'otpauth-migration://offline?data=';
+
+Future<void> showGoogleAuthImageInstruction(BuildContext context) async {
+  final l10n = context.l10n;
+  final result = await showDialogWidget(
+    context: context,
+    title: l10n.importFromApp("Google Authenticator"),
+    body: l10n.importGoogleAuthGuide,
+    buttons: [
+      ButtonWidget(
+        buttonType: ButtonType.primary,
+        labelText: l10n.scanAQrCode,
+        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) {
+      final List<Code>? codes = await Navigator.of(context).push(
+        MaterialPageRoute(
+          builder: (BuildContext context) {
+            return const QrScanner();
+          },
+        ),
+      );
+      if (codes == null || codes.isEmpty) {
+        return;
+      }
+      for (final code in codes) {
+        await CodeStore.instance.addCode(code, shouldSync: false);
+      }
+      unawaited(AuthenticatorService.instance.onlineSync());
+      importSuccessDialog(context, codes.length);
+    }
+  }
+}
+
+List<Code> parseGoogleAuth(String qrCodeData) {
+  try {
+    List<Code> codes = <Code>[];
+    final String payload = qrCodeData.substring(kGoogleAuthExportPrefix.length);
+    final Uint8List base64Decoded = base64Decode(Uri.decodeComponent(payload));
+    final MigrationPayload mPayload =
+        MigrationPayload.fromBuffer(base64Decoded);
+    for (var otpParameter in mPayload.otpParameters) {
+      // Build the OTP URL
+      String otpUrl;
+      String issuer = otpParameter.issuer;
+      String account = otpParameter.name;
+      var counter = otpParameter.counter;
+      // Create a list of bytes from the list of integers.
+      Uint8List bytes = Uint8List.fromList(otpParameter.secret);
+
+      // Encode the bytes to base 32.
+      String base32String = base32.encode(bytes);
+      String secret = base32String;
+      // identify digit count
+      int digits = 6;
+      int timer = 30; // default timer, no field in Google Auth
+      Algorithm algorithm = Algorithm.sha1;
+      switch (otpParameter.algorithm) {
+        case MigrationPayload_Algorithm.ALGORITHM_MD5:
+          throw Exception('GoogleAuthImport: MD5 is not supported');
+        case MigrationPayload_Algorithm.ALGORITHM_SHA1:
+          algorithm = Algorithm.sha1;
+          break;
+        case MigrationPayload_Algorithm.ALGORITHM_SHA256:
+          algorithm = Algorithm.sha256;
+          break;
+        case MigrationPayload_Algorithm.ALGORITHM_SHA512:
+          algorithm = Algorithm.sha512;
+          break;
+        case MigrationPayload_Algorithm.ALGORITHM_UNSPECIFIED:
+          algorithm = Algorithm.sha1;
+          break;
+      }
+      switch (otpParameter.digits) {
+        case MigrationPayload_DigitCount.DIGIT_COUNT_EIGHT:
+          digits = 8;
+          break;
+        case MigrationPayload_DigitCount.DIGIT_COUNT_SIX:
+          digits = 6;
+          break;
+        case MigrationPayload_DigitCount.DIGIT_COUNT_UNSPECIFIED:
+          digits = 6;
+      }
+
+      if (otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_TOTP ||
+          otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_UNSPECIFIED) {
+        otpUrl =
+            'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
+      } else if (otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_HOTP) {
+        otpUrl =
+            'otpauth://hotp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter';
+      } else {
+        throw Exception('Invalid OTP type');
+      }
+      codes.add(Code.fromRawData(otpUrl));
+    }
+    return codes;
+  } catch (e, s) {
+    Logger("GoogleAuthImport")
+        .severe("Error while parsing Google Auth QR code", e, s);
+    throw Exception('Failed to parse Google Auth QR code \n ${e.toString()}');
+  }
+}

+ 2 - 0
lib/ui/settings/data/import/import_service.dart

@@ -2,6 +2,7 @@ import 'package:ente_auth/ui/settings/data/import/aegis_import.dart';
 import 'package:ente_auth/ui/settings/data/import/analyze_qr_code.dart';
 import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart';
 import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
+import 'package:ente_auth/ui/settings/data/import/google_auth_image_import.dart';
 import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart';
 import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
 import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart';
@@ -43,6 +44,7 @@ class ImportService {
         );
       case ImportType.bitwarden:
         showBitwardenImportInstruction(context);
+        showGoogleAuthImageInstruction(context);
         break;
     }
   }