-
-
Notifications
You must be signed in to change notification settings - Fork 813
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
246 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters