diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f81b9db84..7d516c5b4 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 vault\" option within Bitwarden Tools and import the unencrypted 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..ce2dce3f2 --- /dev/null +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -0,0 +1,103 @@ +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 _pickBitwardenJsonFile(context); + } + } +} + +Future _pickBitwardenJsonFile(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 _processBitwardenExportFile(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 _processBitwardenExportFile( + BuildContext context, + String path, +) async { + File file = File(path); + final jsonString = await file.readAsString(); + final data = jsonDecode(jsonString); + List jsonArray = data['items']; + final parsedCodes = []; + for (var item in jsonArray) { + if (item['login']['totp'] != null) { + var issuer = item['name']; + var account = item['login']['username']; + var secret = item['login']['totp']; + + parsedCodes.add( + Code.fromAccountAndSecret( + account, + issuer, + secret, + ), + ); + } + } + + for (final code in parsedCodes) { + await CodeStore.instance.addCode(code, shouldSync: false); + } + unawaited(AuthenticatorService.instance.onlineSync()); + return parsedCodes.length; +} diff --git a/lib/ui/settings/data/import/import_service.dart b/lib/ui/settings/data/import/import_service.dart index a8a410b21..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'; @@ -31,6 +32,9 @@ class ImportService { case ImportType.aegis: showAegisImportInstruction(context); break; + case ImportType.bitwarden: + showBitwardenImportInstruction(context); + break; } } } diff --git a/lib/ui/settings/data/import_page.dart b/lib/ui/settings/data/import_page.dart index 9c570b66e..fb8c55d1c 100644 --- a/lib/ui/settings/data/import_page.dart +++ b/lib/ui/settings/data/import_page.dart @@ -15,6 +15,7 @@ enum ImportType { ravio, googleAuthenticator, aegis, + bitwarden, } class ImportCodePage extends StatelessWidget { @@ -24,6 +25,7 @@ class ImportCodePage extends StatelessWidget { ImportType.ravio, ImportType.aegis, ImportType.googleAuthenticator, + ImportType.bitwarden, ]; ImportCodePage({super.key}); @@ -40,6 +42,8 @@ class ImportCodePage extends StatelessWidget { return 'Google Authenticator'; case ImportType.aegis: return 'Aegis Authenticator'; + case ImportType.bitwarden: + return 'Bitwarden'; } }