Ver código fonte

Merge conflict: Resolved-2

Muhammed Ayimen 1 ano atrás
pai
commit
a4df578665

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

@@ -340,6 +340,13 @@
   "showQRAuthMessage": "Authenticate to show QR code",
   "showQRAuthMessage": "Authenticate to show QR code",
   "confirmAccountDeleteTitle": "Confirm account deletion",
   "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.",
   "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": "Verify identity",
   "@androidBiometricHint": {
   "@androidBiometricHint": {
     "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
     "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 'dart:io';
 
 
 import 'package:ente_auth/l10n/l10n.dart';
 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/buttons/button_widget.dart';
 import 'package:ente_auth/ui/components/dialog_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/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/ui/settings/data/import/qr_scanner_overlay.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -21,6 +23,7 @@ class QrScanner extends StatefulWidget {
 
 
 class _QrScannerState extends State<QrScanner> {
 class _QrScannerState extends State<QrScanner> {
   bool isNavigationPerformed = false;
   bool isNavigationPerformed = false;
+  bool isScannedByImage = false;
 
 
   //Scanner Initialization
   //Scanner Initialization
   MobileScannerController scannerController = MobileScannerController(
   MobileScannerController scannerController = MobileScannerController(
@@ -40,85 +43,40 @@ class _QrScannerState extends State<QrScanner> {
               children: [
               children: [
                 MobileScanner(
                 MobileScanner(
                   controller: scannerController,
                   controller: scannerController,
-                  onDetect: (capture) {
+                  onDetect: (capture) async {
                     if (!isNavigationPerformed) {
                     if (!isNavigationPerformed) {
                       isNavigationPerformed = true;
                       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 {
                         onPressed: () async {
                           final result = await showDialogWidget(
                           final result = await showDialogWidget(
                             context: context,
                             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: [
                             buttons: [
-                              const ButtonWidget(
+                              ButtonWidget(
                                 buttonType: ButtonType.primary,
                                 buttonType: ButtonType.primary,
-                                labelText: 'Import from image',
+                                labelText: l10n.importGoogleAuthImageButtonText,
                                 isInAlert: true,
                                 isInAlert: true,
                                 buttonSize: ButtonSize.large,
                                 buttonSize: ButtonSize.large,
                                 buttonAction: ButtonAction.first,
                                 buttonAction: ButtonAction.first,
@@ -191,6 +150,7 @@ class _QrScannerState extends State<QrScanner> {
                                 context,
                                 context,
                                 pickerConfig: const AssetPickerConfig(
                                 pickerConfig: const AssetPickerConfig(
                                   maxAssets: 1,
                                   maxAssets: 1,
+                                  requestType: RequestType.image,
                                 ),
                                 ),
                               );
                               );
 
 
@@ -201,14 +161,22 @@ class _QrScannerState extends State<QrScanner> {
 
 
                                 if (await scannerController
                                 if (await scannerController
                                     .analyzeImage(path)) {
                                     .analyzeImage(path)) {
+                                  isScannedByImage = true;
                                   if (!mounted) return;
                                   if (!mounted) return;
                                 } else {
                                 } else {
                                   if (!mounted) return;
                                   if (!mounted) return;
-                                  showToast(context, "Failed to scan image");
+                                  isScannedByImage = false;
+                                  showToast(
+                                    context,
+                                    l10n.unableToRecognizeQrCodeText,
+                                  );
                                 }
                                 }
                               } else {
                               } else {
                                 if (!mounted) return;
                                 if (!mounted) return;
-                                showToast(context, "Image not selected");
+                                showToast(
+                                  context,
+                                  l10n.qrCodeImageNotSelectedText,
+                                );
                               }
                               }
                             }
                             }
                           }
                           }
@@ -250,13 +218,10 @@ class _QrScannerState extends State<QrScanner> {
                           const SizedBox(
                           const SizedBox(
                             height: 25,
                             height: 25,
                           ),
                           ),
-                          const Text(
-                            'Scan QR code',
+                          Text(
+                            l10n.scanACode,
                             textAlign: TextAlign.center,
                             textAlign: TextAlign.center,
-                            style: TextStyle(
-                              letterSpacing: 0.5,
-                              fontWeight: FontWeight.w600,
-                              fontSize: 14,
+                            style: const TextStyle(
                               color: Colors.black,
                               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/analyze_qr_code.dart';
 import 'package:ente_auth/ui/settings/data/import/bitwarden_import.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/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/google_auth_import.dart';
 import 'package:ente_auth/ui/settings/data/import/plain_text_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';
 import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart';
@@ -43,6 +44,7 @@ class ImportService {
         );
         );
       case ImportType.bitwarden:
       case ImportType.bitwarden:
         showBitwardenImportInstruction(context);
         showBitwardenImportInstruction(context);
+        showGoogleAuthImageInstruction(context);
         break;
         break;
     }
     }
   }
   }