Skip to content

Commit

Permalink
Aegis import + bump version (#207)
Browse files Browse the repository at this point in the history
  • Loading branch information
ua741 authored Aug 18, 2023
2 parents 16dbf5b + 7cee6ad commit 384f37b
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/l10n/arb/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"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.",
"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",
"importLabel": "Import",
"importInstruction": "Please select a file that contains a list of your codes in the following format",
Expand Down
236 changes: 236 additions & 0 deletions lib/ui/settings/data/import/aegis_import.dart
Original file line number Diff line number Diff line change
@@ -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);
}
1 change: 0 additions & 1 deletion lib/ui/settings/data/import/google_auth_import.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ List<Code> parseGoogleAuth(String qrCodeData) {
try {
List<Code> codes = <Code>[];
final String payload = qrCodeData.substring(kGoogleAuthExportPrefix.length);
debugPrint("GoogleAuthImport: payload: $payload");
final Uint8List base64Decoded = base64Decode(Uri.decodeComponent(payload));
final MigrationPayload mPayload =
MigrationPayload.fromBuffer(base64Decoded);
Expand Down
5 changes: 4 additions & 1 deletion lib/ui/settings/data/import/import_service.dart
Original file line number Diff line number Diff line change
@@ -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/google_auth_import.dart';
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
Expand Down Expand Up @@ -25,9 +26,11 @@ class ImportService {
break;
case ImportType.googleAuthenticator:
showGoogleAuthInstruction(context);

// showToast(context, 'coming soon');
break;
case ImportType.aegis:
showAegisImportInstruction(context);
break;
}
}
}
4 changes: 4 additions & 0 deletions lib/ui/settings/data/import_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ enum ImportType {
encrypted,
ravio,
googleAuthenticator,
aegis,
}

class ImportCodePage extends StatelessWidget {
late List<ImportType> importOptions = [
ImportType.plainText,
ImportType.encrypted,
ImportType.ravio,
ImportType.aegis,
ImportType.googleAuthenticator,
];

Expand All @@ -37,6 +39,8 @@ class ImportCodePage extends StatelessWidget {
return 'Raivo OTP';
case ImportType.googleAuthenticator:
return 'Google Authenticator';
case ImportType.aegis:
return 'Aegis Authenticator';
}
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: ente_auth
description: ente two-factor authenticator
version: 1.0.54+54
version: 1.0.55+55
publish_to: none

environment:
Expand Down

0 comments on commit 384f37b

Please sign in to comment.