Ver Fonte

Aegis import + bump version (#207)

Neeraj Gupta há 1 ano atrás
pai
commit
384f37bb07

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

@@ -85,6 +85,7 @@
   "importSelectJsonFile": "Select JSON file",
   "importSelectJsonFile": "Select JSON file",
   "importEnteEncGuide": "Select the encrypted JSON file exported from ente",
   "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.",
   "importRaivoGuide": "Use the \"Export OTPs to Zip archive\" option in Raivo's Settings.\n\nExtract the zip file and import the JSON file.",
+  "importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.",
   "exportCodes": "Export codes",
   "exportCodes": "Export codes",
   "importLabel": "Import",
   "importLabel": "Import",
   "importInstruction": "Please select a file that contains a list of your codes in the following format",
   "importInstruction": "Please select a file that contains a list of your codes in the following format",

+ 236 - 0
lib/ui/settings/data/import/aegis_import.dart

@@ -0,0 +1,236 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+import 'package:convert/convert.dart';
+
+import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/code.dart';
+import 'package:ente_auth/services/authenticator_service.dart';
+import 'package:ente_auth/store/code_store.dart';
+import 'package:ente_auth/ui/common/progress_dialog.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/import_success.dart';
+import 'package:ente_auth/utils/dialog_util.dart';
+import 'package:file_picker/file_picker.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
+import 'package:pointycastle/block/aes.dart';
+import 'package:pointycastle/block/modes/gcm.dart';
+import 'package:pointycastle/key_derivators/scrypt.dart';
+import 'package:pointycastle/pointycastle.dart';
+
+Future<void> showAegisImportInstruction(BuildContext context) async {
+  final l10n = context.l10n;
+  final result = await showDialogWidget(
+    context: context,
+    title: l10n.importFromApp("Aegis Authenticator"),
+    body: l10n.importAegisGuide,
+    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 _pickAegisJsonFile(context);
+    } else {}
+  }
+}
+
+Future<void> _pickAegisJsonFile(BuildContext context) async {
+  final l10n = context.l10n;
+  FilePickerResult? result = await FilePicker.platform
+      .pickFiles(dialogTitle: l10n.importSelectJsonFile);
+  if (result == null) {
+    return;
+  }
+  final ProgressDialog progressDialog =
+      createProgressDialog(context, l10n.pleaseWait);
+  await progressDialog.show();
+  try {
+    String path = result.files.single.path!;
+    int? count = await _processAegisExportFile(context, path, progressDialog);
+    await progressDialog.hide();
+    if (count != null) {
+      await importSuccessDialog(context, count);
+    }
+  } catch (e, s) {
+    Logger('AegisImport').severe('exception while processing for aegis', e, s);
+    await progressDialog.hide();
+    await showErrorDialog(
+      context,
+      context.l10n.sorry,
+      context.l10n.importFailureDesc,
+    );
+  }
+}
+
+Future<int?> _processAegisExportFile(
+  BuildContext context,
+  String path,
+  final ProgressDialog dialog,
+) async {
+  File file = File(path);
+
+  final jsonString = await file.readAsString();
+  final decodedJson = jsonDecode(jsonString);
+  final isEncrypted = decodedJson['header']['slots'] != null;
+  var aegisDB;
+  if (isEncrypted) {
+    String? password;
+    try {
+      await showTextInputDialog(
+        context,
+        title: "Enter password to aegis vault",
+        submitButtonLabel: "Submit",
+        isPasswordInput: true,
+        onSubmit: (value) async {
+          password = value;
+        },
+      );
+      if (password == null) {
+        await dialog.hide();
+        return null;
+      }
+      final content = decryptAegisVault(decodedJson, password: password!);
+      aegisDB = jsonDecode(content);
+    } catch (e, s) {
+      Logger("AegisImport")
+          .warning("exception while decrypting aegis vault", e, s);
+      await dialog.hide();
+      if (password != null) {
+        await showErrorDialog(
+          context,
+          "Failed to decrypt aegis vault",
+          "Please check your password and try again.",
+        );
+      }
+      return null;
+    }
+  } else {
+    aegisDB = decodedJson['db'];
+  }
+  final parsedCodes = [];
+  for (var item in aegisDB['entries']) {
+    var kind = item['type'];
+    var account = item['name'];
+    var issuer = item['issuer'];
+    var algorithm = item['info']['algo'];
+    var secret = item['info']['secret'];
+    var timer = item['info']['period'];
+    var digits = item['info']['digits'];
+
+    var counter = item['info']['counter'];
+
+    // Build the OTP URL
+    String otpUrl;
+
+    if (kind.toLowerCase() == 'totp') {
+      otpUrl =
+          'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
+    } else if (kind.toLowerCase() == 'hotp') {
+      otpUrl =
+          'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter';
+    } else {
+      throw Exception('Invalid OTP type');
+    }
+    parsedCodes.add(Code.fromRawData(otpUrl));
+  }
+
+  for (final code in parsedCodes) {
+    await CodeStore.instance.addCode(code, shouldSync: false);
+  }
+  unawaited(AuthenticatorService.instance.sync());
+  int count = parsedCodes.length;
+  return count;
+}
+
+String decryptAegisVault(dynamic data, {required String password}) {
+  final header = data["header"];
+  final slots =
+      (header["slots"] as List).where((slot) => slot["type"] == 1).toList();
+
+  Uint8List? masterKey;
+  for (final slot in slots) {
+    final salt = Uint8List.fromList(hex.decode(slot["salt"]));
+    final int iterations = slot["n"];
+    final int r = slot["r"];
+    final int p = slot["p"];
+    const int derivedKeyLength = 32;
+    final script = Scrypt()
+      ..init(
+        ScryptParameters(
+          iterations,
+          r,
+          p,
+          derivedKeyLength,
+          salt,
+        ),
+      );
+
+    final key = script.process(Uint8List.fromList(utf8.encode(password)));
+
+    final params = slot["key_params"];
+    final nonce = Uint8List.fromList(hex.decode(params["nonce"]));
+    final encryptedKeyWithTag =
+        Uint8List.fromList(hex.decode(slot["key"]) + hex.decode(params["tag"]));
+
+    final cipher = GCMBlockCipher(AESEngine())
+      ..init(
+        false,
+        AEADParameters(
+          KeyParameter(key),
+          128,
+          nonce,
+          Uint8List.fromList(<int>[]),
+        ),
+      );
+
+    try {
+      masterKey = cipher.process(encryptedKeyWithTag);
+      break;
+    } catch (e) {
+      // Ignore decryption failure and continue to next slot
+    }
+  }
+
+  if (masterKey == null) {
+    throw Exception("Unable to decrypt the master key with the given password");
+  }
+
+  final content = base64.decode(data["db"]);
+  final params = header["params"];
+  final nonce = Uint8List.fromList(hex.decode(params["nonce"]));
+  final tag = Uint8List.fromList(hex.decode(params["tag"]));
+  final cipherTextWithTag = Uint8List.fromList(content + tag);
+
+  final cipher = GCMBlockCipher(AESEngine())
+    ..init(
+      false,
+      AEADParameters(
+        KeyParameter(masterKey),
+        128,
+        nonce,
+        Uint8List.fromList(<int>[]),
+      ),
+    );
+
+  final dbBytes = cipher.process(cipherTextWithTag);
+  return utf8.decode(dbBytes);
+}

+ 0 - 1
lib/ui/settings/data/import/google_auth_import.dart

@@ -66,7 +66,6 @@ List<Code> parseGoogleAuth(String qrCodeData) {
   try {
   try {
     List<Code> codes = <Code>[];
     List<Code> codes = <Code>[];
     final String payload = qrCodeData.substring(kGoogleAuthExportPrefix.length);
     final String payload = qrCodeData.substring(kGoogleAuthExportPrefix.length);
-    debugPrint("GoogleAuthImport: payload: $payload");
     final Uint8List base64Decoded = base64Decode(Uri.decodeComponent(payload));
     final Uint8List base64Decoded = base64Decode(Uri.decodeComponent(payload));
     final MigrationPayload mPayload =
     final MigrationPayload mPayload =
         MigrationPayload.fromBuffer(base64Decoded);
         MigrationPayload.fromBuffer(base64Decoded);

+ 4 - 1
lib/ui/settings/data/import/import_service.dart

@@ -1,3 +1,4 @@
+import 'package:ente_auth/ui/settings/data/import/aegis_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_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';
@@ -25,9 +26,11 @@ class ImportService {
         break;
         break;
       case ImportType.googleAuthenticator:
       case ImportType.googleAuthenticator:
         showGoogleAuthInstruction(context);
         showGoogleAuthInstruction(context);
-
         // showToast(context, 'coming soon');
         // showToast(context, 'coming soon');
         break;
         break;
+      case ImportType.aegis:
+        showAegisImportInstruction(context);
+        break;
     }
     }
   }
   }
 }
 }

+ 4 - 0
lib/ui/settings/data/import_page.dart

@@ -15,6 +15,7 @@ enum ImportType {
   encrypted,
   encrypted,
   ravio,
   ravio,
   googleAuthenticator,
   googleAuthenticator,
+  aegis,
 }
 }
 
 
 class ImportCodePage extends StatelessWidget {
 class ImportCodePage extends StatelessWidget {
@@ -22,6 +23,7 @@ class ImportCodePage extends StatelessWidget {
     ImportType.plainText,
     ImportType.plainText,
     ImportType.encrypted,
     ImportType.encrypted,
     ImportType.ravio,
     ImportType.ravio,
+    ImportType.aegis,
     ImportType.googleAuthenticator,
     ImportType.googleAuthenticator,
   ];
   ];
 
 
@@ -37,6 +39,8 @@ class ImportCodePage extends StatelessWidget {
         return 'Raivo OTP';
         return 'Raivo OTP';
       case ImportType.googleAuthenticator:
       case ImportType.googleAuthenticator:
         return 'Google Authenticator';
         return 'Google Authenticator';
+      case ImportType.aegis:
+        return 'Aegis Authenticator';
     }
     }
   }
   }
 
 

+ 1 - 1
pubspec.yaml

@@ -1,6 +1,6 @@
 name: ente_auth
 name: ente_auth
 description: ente two-factor authenticator
 description: ente two-factor authenticator
-version: 1.0.54+54
+version: 1.0.55+55
 publish_to: none
 publish_to: none
 
 
 environment:
 environment: