Add support for importing encrypt aegis vault

This commit is contained in:
Neeraj Gupta 2023-08-18 15:59:10 +05:30
parent 815059f11e
commit 111c28d076
2 changed files with 131 additions and 17 deletions

View file

@ -85,7 +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\nDeselect 'Encrypt the vault` option while exporting from Aegis",
"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",

View file

@ -1,20 +1,28 @@
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:ente_auth/utils/toast_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;
@ -41,22 +49,24 @@ Future<void> showAegisImportInstruction(BuildContext context) async {
);
if (result?.action != null && result!.action != ButtonAction.cancel) {
if (result.action == ButtonAction.first) {
await _pickRaivoJsonFile(context);
await _pickAegisJsonFile(context);
} else {}
}
}
Future<void> _pickRaivoJsonFile(BuildContext context) async {
Future<void> _pickAegisJsonFile(BuildContext context) async {
final l10n = context.l10n;
FilePickerResult? result = await FilePicker.platform.pickFiles();
FilePickerResult? result = await FilePicker.platform
.pickFiles(dialogTitle: l10n.importSelectJsonFile);
if (result == null) {
return;
}
final progressDialog = createProgressDialog(context, l10n.pleaseWait);
final ProgressDialog progressDialog =
createProgressDialog(context, l10n.pleaseWait);
await progressDialog.show();
try {
String path = result.files.single.path!;
int? count = await _processRaivoExportFile(context, path);
int? count = await _processAegisExportFile(context, path, progressDialog);
await progressDialog.hide();
if (count != null) {
await importSuccessDialog(context, count);
@ -72,21 +82,51 @@ Future<void> _pickRaivoJsonFile(BuildContext context) async {
}
}
Future<int?> _processRaivoExportFile(BuildContext context, String path) async {
Future<int?> _processAegisExportFile(
BuildContext context, String path, final ProgressDialog dialog) async {
File file = File(path);
if (path.endsWith('.zip')) {
await showErrorDialog(
context,
context.l10n.sorry,
"We don't support zip files yet. Please unzip the file and try again.",
);
return null;
}
final jsonString = await file.readAsString();
final decodedJson = jsonDecode(jsonString);
int version = decodedJson['db']['version'];
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'];
}
int dbVersion = aegisDB['version'];
final parsedCodes = [];
for (var item in decodedJson['db']['entries']) {
for (var item in aegisDB['entries']) {
var kind = item['type'];
var account = item['name'];
var issuer = item['issuer'];
@ -119,3 +159,77 @@ Future<int?> _processRaivoExportFile(BuildContext context, String path) async {
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);
}