From f22b0cde8d74d693ebbad6efef70a9d0538450c6 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 21:54:15 +0900 Subject: [PATCH 1/7] Added: Bitwarden option in import screen --- lib/ui/settings/data/import/import_service.dart | 3 +++ lib/ui/settings/data/import_page.dart | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/lib/ui/settings/data/import/import_service.dart b/lib/ui/settings/data/import/import_service.dart index a8a410b21..69706fb5f 100644 --- a/lib/ui/settings/data/import/import_service.dart +++ b/lib/ui/settings/data/import/import_service.dart @@ -31,6 +31,9 @@ class ImportService { case ImportType.aegis: showAegisImportInstruction(context); break; + case ImportType.bitwarden: + showGoogleAuthImageInstruction(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'; } } From 4b66689e0744f26a79e39a76f2886bf723935032 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 22:00:59 +0900 Subject: [PATCH 2/7] Created: Popup for bitwarden import option --- lib/l10n/arb/app_en.arb | 1 + .../data/import/bitwarden_import.dart | 117 ++++++++++++++++++ .../settings/data/import/import_service.dart | 3 +- 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 lib/ui/settings/data/import/bitwarden_import.dart 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; } } From 27c8111e633b292cdbeffd6c20fb053ffd7dec00 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 22:18:35 +0900 Subject: [PATCH 3/7] Created: Bitward import functionality with json file selecting and extracting the data to a particular format --- .../data/import/bitwarden_import.dart | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/ui/settings/data/import/bitwarden_import.dart b/lib/ui/settings/data/import/bitwarden_import.dart index 4b9101d00..ec4637146 100644 --- a/lib/ui/settings/data/import/bitwarden_import.dart +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -40,12 +40,12 @@ Future showBitwardenImportInstruction(BuildContext context) async { ); if (result?.action != null && result!.action != ButtonAction.cancel) { if (result.action == ButtonAction.first) { - await _pickRaivoJsonFile(context); + await _pickBitwardenJsonFile(context); } else {} } } -Future _pickRaivoJsonFile(BuildContext context) async { +Future _pickBitwardenJsonFile(BuildContext context) async { final l10n = context.l10n; FilePickerResult? result = await FilePicker.platform.pickFiles(); if (result == null) { @@ -55,7 +55,7 @@ Future _pickRaivoJsonFile(BuildContext context) async { await progressDialog.show(); try { String path = result.files.single.path!; - int? count = await _processRaivoExportFile(context, path); + int? count = await _processBitwardenExportFile(context, path); await progressDialog.hide(); if (count != null) { await importSuccessDialog(context, count); @@ -70,7 +70,10 @@ Future _pickRaivoJsonFile(BuildContext context) async { } } -Future _processRaivoExportFile(BuildContext context, String path) async { +Future _processBitwardenExportFile( + BuildContext context, + String path, +) async { File file = File(path); if (path.endsWith('.zip')) { await showErrorDialog( @@ -84,28 +87,25 @@ Future _processRaivoExportFile(BuildContext context, String path) async { 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']; + if (item['login']['totp'] != null) { + var issuer = item['name']; + var account = item['login']['username']; + var secret = item['login']['totp']; - // Build the OTP URL - String otpUrl; + // 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'); + 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)); } - parsedCodes.add(Code.fromRawData(otpUrl)); } for (final code in parsedCodes) { From bed3bd96127ce2e7b9ff1d225ef85595f88cc6a7 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 22:31:07 +0900 Subject: [PATCH 4/7] Bug fixed: Using the map values from wrong jsonArray --- lib/ui/settings/data/import/bitwarden_import.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ui/settings/data/import/bitwarden_import.dart b/lib/ui/settings/data/import/bitwarden_import.dart index ec4637146..7b14ceacc 100644 --- a/lib/ui/settings/data/import/bitwarden_import.dart +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -84,7 +84,8 @@ Future _processBitwardenExportFile( return null; } final jsonString = await file.readAsString(); - List jsonArray = jsonDecode(jsonString); + final data = jsonDecode(jsonString); + List jsonArray = data['items']; final parsedCodes = []; for (var item in jsonArray) { if (item['login']['totp'] != null) { From 9b759a02a5bb7f0e107a8c259178a02f43527255 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 22:38:55 +0900 Subject: [PATCH 5/7] Completed: Import from bitwarden functionality --- .../data/import/bitwarden_import.dart | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/ui/settings/data/import/bitwarden_import.dart b/lib/ui/settings/data/import/bitwarden_import.dart index 7b14ceacc..fbf9ed16e 100644 --- a/lib/ui/settings/data/import/bitwarden_import.dart +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -93,19 +93,13 @@ Future _processBitwardenExportFile( var account = item['login']['username']; var secret = item['login']['totp']; - // 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)); + parsedCodes.add( + Code.fromAccountAndSecret( + account, + issuer, + secret, + ), + ); } } From 3b8219020a894f40fe0fa4ac063677c7df082be2 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 23:40:40 +0900 Subject: [PATCH 6/7] Updated: en arb file content for bitwarden data --- lib/l10n/arb/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c9a8b7754..7d516c5b4 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -86,7 +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.", + "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", From adc157fe56d60d0759120c609d7469c7e4111b50 Mon Sep 17 00:00:00 2001 From: Muhammed Ayimen Date: Tue, 14 Nov 2023 23:42:53 +0900 Subject: [PATCH 7/7] Fixes: Based on the review --- lib/ui/settings/data/import/bitwarden_import.dart | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/lib/ui/settings/data/import/bitwarden_import.dart b/lib/ui/settings/data/import/bitwarden_import.dart index fbf9ed16e..ce2dce3f2 100644 --- a/lib/ui/settings/data/import/bitwarden_import.dart +++ b/lib/ui/settings/data/import/bitwarden_import.dart @@ -41,7 +41,7 @@ Future showBitwardenImportInstruction(BuildContext context) async { if (result?.action != null && result!.action != ButtonAction.cancel) { if (result.action == ButtonAction.first) { await _pickBitwardenJsonFile(context); - } else {} + } } } @@ -75,14 +75,6 @@ Future _processBitwardenExportFile( 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(); final data = jsonDecode(jsonString); List jsonArray = data['items']; @@ -107,6 +99,5 @@ Future _processBitwardenExportFile( await CodeStore.instance.addCode(code, shouldSync: false); } unawaited(AuthenticatorService.instance.onlineSync()); - int count = parsedCodes.length; - return count; + return parsedCodes.length; }