diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f81b9db84..c9a8b7754 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -86,6 +86,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.", + "importBitwardenGuide": "Use the \"Export\" option in Bitwarden 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", diff --git a/lib/ui/settings/data/import/bitwarden_import.dart b/lib/ui/settings/data/import/bitwarden_import.dart new file mode 100644 index 000000000..4b9101d00 --- /dev/null +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -0,0 +1,117 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +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/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'; + +Future showBitwardenImportInstruction(BuildContext context) async { + final l10n = context.l10n; + final result = await showDialogWidget( + context: context, + title: l10n.importFromApp("Bitwarden"), + body: l10n.importBitwardenGuide, + 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 _pickRaivoJsonFile(context); + } else {} + } +} + +Future _pickRaivoJsonFile(BuildContext context) async { + final l10n = context.l10n; + FilePickerResult? result = await FilePicker.platform.pickFiles(); + if (result == null) { + return; + } + final progressDialog = createProgressDialog(context, l10n.pleaseWait); + await progressDialog.show(); + try { + String path = result.files.single.path!; + int? count = await _processRaivoExportFile(context, path); + await progressDialog.hide(); + if (count != null) { + await importSuccessDialog(context, count); + } + } catch (e) { + await progressDialog.hide(); + await showErrorDialog( + context, + context.l10n.sorry, + context.l10n.importFailureDesc, + ); + } +} + +Future _processRaivoExportFile(BuildContext context, String path) 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(); + List jsonArray = jsonDecode(jsonString); + final parsedCodes = []; + for (var item in jsonArray) { + var kind = item['kind']; + var algorithm = item['algorithm']; + var timer = item['timer']; + var digits = item['digits']; + var issuer = item['issuer']; + var secret = item['secret']; + var account = item['account']; + var counter = item['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.onlineSync()); + int count = parsedCodes.length; + return count; +} diff --git a/lib/ui/settings/data/import/import_service.dart b/lib/ui/settings/data/import/import_service.dart index 69706fb5f..0be1f572e 100644 --- a/lib/ui/settings/data/import/import_service.dart +++ b/lib/ui/settings/data/import/import_service.dart @@ -1,4 +1,5 @@ import 'package:ente_auth/ui/settings/data/import/aegis_import.dart'; +import 'package:ente_auth/ui/settings/data/import/bitwarden_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'; @@ -32,7 +33,7 @@ class ImportService { showAegisImportInstruction(context); break; case ImportType.bitwarden: - showGoogleAuthImageInstruction(context); + showBitwardenImportInstruction(context); break; } }