Add support for importing encrypt aegis vault
This commit is contained in:
parent
815059f11e
commit
111c28d076
2 changed files with 131 additions and 17 deletions
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue