diff --git a/assets/auth/ic_2fas.png b/assets/auth/ic_2fas.png new file mode 100644 index 00000000..25e67fdb Binary files /dev/null and b/assets/auth/ic_2fas.png differ diff --git a/assets/auth/ic_aegis.png b/assets/auth/ic_aegis.png new file mode 100644 index 00000000..1d60fa00 Binary files /dev/null and b/assets/auth/ic_aegis.png differ diff --git a/assets/auth/ic_andotp.png b/assets/auth/ic_andotp.png new file mode 100644 index 00000000..f13eba34 Binary files /dev/null and b/assets/auth/ic_andotp.png differ diff --git a/assets/auth/ic_authenticatorplus.png b/assets/auth/ic_authenticatorplus.png new file mode 100644 index 00000000..182e8915 Binary files /dev/null and b/assets/auth/ic_authenticatorplus.png differ diff --git a/assets/auth/ic_authenticatorpro.png b/assets/auth/ic_authenticatorpro.png new file mode 100644 index 00000000..9fd2107a Binary files /dev/null and b/assets/auth/ic_authenticatorpro.png differ diff --git a/assets/auth/ic_authy.png b/assets/auth/ic_authy.png new file mode 100644 index 00000000..d3778e5d Binary files /dev/null and b/assets/auth/ic_authy.png differ diff --git a/assets/auth/ic_bitwarden.png b/assets/auth/ic_bitwarden.png new file mode 100644 index 00000000..93f8d1d1 Binary files /dev/null and b/assets/auth/ic_bitwarden.png differ diff --git a/assets/auth/ic_blizzard.png b/assets/auth/ic_blizzard.png new file mode 100644 index 00000000..6012444e Binary files /dev/null and b/assets/auth/ic_blizzard.png differ diff --git a/assets/auth/ic_enteauth.png b/assets/auth/ic_enteauth.png new file mode 100644 index 00000000..9d6bf368 Binary files /dev/null and b/assets/auth/ic_enteauth.png differ diff --git a/assets/auth/ic_freeotp.png b/assets/auth/ic_freeotp.png new file mode 100644 index 00000000..2a5cebe1 Binary files /dev/null and b/assets/auth/ic_freeotp.png differ diff --git a/assets/auth/ic_freeotpplus.png b/assets/auth/ic_freeotpplus.png new file mode 100644 index 00000000..f9354c33 Binary files /dev/null and b/assets/auth/ic_freeotpplus.png differ diff --git a/assets/auth/ic_googleauthenticator.png b/assets/auth/ic_googleauthenticator.png new file mode 100644 index 00000000..a83ab250 Binary files /dev/null and b/assets/auth/ic_googleauthenticator.png differ diff --git a/assets/auth/ic_lastpass.png b/assets/auth/ic_lastpass.png new file mode 100644 index 00000000..423aced4 Binary files /dev/null and b/assets/auth/ic_lastpass.png differ diff --git a/assets/auth/ic_steam.png b/assets/auth/ic_steam.png new file mode 100644 index 00000000..4e26796f Binary files /dev/null and b/assets/auth/ic_steam.png differ diff --git a/assets/auth/ic_totpauthenticator.png b/assets/auth/ic_totpauthenticator.png new file mode 100644 index 00000000..d5bfc66b Binary files /dev/null and b/assets/auth/ic_totpauthenticator.png differ diff --git a/assets/auth/ic_winauth.png b/assets/auth/ic_winauth.png new file mode 100644 index 00000000..7b9aa123 Binary files /dev/null and b/assets/auth/ic_winauth.png differ diff --git a/assets/auth/run.py b/assets/auth/run.py new file mode 100644 index 00000000..7dfcd9c6 --- /dev/null +++ b/assets/auth/run.py @@ -0,0 +1,36 @@ +#遍历文件夹下的json文件,对于每个json文件生成对应的代码 +#如对于gift_dark.json,对应代码行为static const String giftDark = "assets/lottie/gift_dark.json" +#生成所有json文件对应的代码,并写入到文件中 +import os + +def to_camel_case(snake_str): + """将蛇形命名(下划线分隔)转换为驼峰命名(首字母小写)""" + components = snake_str.split('_') + return components[0].lower() + ''.join(x.title() for x in components[1:]) + +def generate_code_for_json_files(folder_path, output_file): + # 初始化一个列表来存储所有的代码行 + code_lines = [] + + # 遍历文件夹下的所有文件 + for root, _, files in os.walk(folder_path): + for file in files: + if file.endswith(".png"): + # 提取文件名(不包括扩展名) + base_name = os.path.splitext(file)[0] + # 将文件名转换为驼峰命名 + variable_name = to_camel_case(base_name) + # 生成代码行 + code_line = f'static const String {variable_name} = "assets/auth/{file}";' + # 将代码行添加到列表中 + code_lines.append(code_line) + + # 将所有代码行写入到输出文件中 + with open(output_file, 'w') as f: + for line in code_lines: + f.write(line + '\n') + +# 使用示例 +folder_path = './' # 替换为你的文件夹路径 +output_file = './code.txt' # 替换为你的输出文件路径 +generate_code_for_json_files(folder_path, output_file) \ No newline at end of file diff --git a/lib/Screens/Token/import_export_token_screen.dart b/lib/Screens/Token/import_export_token_screen.dart index 5b6aa2df..c4901385 100644 --- a/lib/Screens/Token/import_export_token_screen.dart +++ b/lib/Screens/Token/import_export_token_screen.dart @@ -327,139 +327,6 @@ class _ImportExportTokenScreenState extends State }, ), const SizedBox(height: 10), - ItemBuilder.buildCaptionItem( - context: context, title: S.current.importFromThirdParty), - ItemBuilder.buildEntryItem( - context: context, - title: S.current.importFromGoogleAuthenticator, - description: S.current.importFromGoogleAuthenticatorTip, - onTap: () async { - if (ResponsiveUtil.isMobile()) { - BottomSheetBuilder.showBottomSheet( - rootContext, - enableDrag: false, - responsive: true, - (context) => const AddBottomSheet(onlyShowScanner: true), - ); - } - }, - ), - ItemBuilder.buildEntryItem( - context: context, - title: S.current.importFrom2FAS, - description: S.current.importFrom2FASTip, - onTap: () async { - FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFrom2FASTitle, - type: FileType.custom, - allowedExtensions: ['2fas'], - lockParentWindow: true, - ); - if (result != null) { - TwoFASTokenImporter().importFromPath(result.files.single.path!); - } - }, - ), - ItemBuilder.buildEntryItem( - context: context, - title: S.current.importFromAegis, - description: S.current.importFromAegisTip, - onTap: () async { - FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFromAegisTitle, - type: FileType.custom, - allowedExtensions: ['json'], - lockParentWindow: true, - ); - if (result != null) { - AegisTokenImporter().importFromPath(result.files.single.path!); - } - }, - ), - // ItemBuilder.buildEntryItem( - // context: context, - // title: S.current.importFromFreeOTP, - // description: S.current.importFromFreeOTPTip, - // onTap: () async { - // FilePickerResult? result = await FileUtil.pickFiles( - // dialogTitle: S.current.importFromFreeOTPTitle, - // type: FileType.custom, - // allowedExtensions: ['xml'], - // lockParentWindow: true, - // ); - // if (result != null) { - // FreeOTPTokenImporter().importFromPath(result.files.single.path!); - // } - // }, - // ), - ItemBuilder.buildEntryItem( - context: context, - title: S.current.importFromFreeOTPPlus, - description: S.current.importFromFreeOTPPlusTip, - onTap: () async { - FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFromFreeOTPPlusTitle, - type: FileType.custom, - allowedExtensions: ['json'], - lockParentWindow: true, - ); - if (result != null) { - FreeOTPPlusTokenImporter() - .importFromPath(result.files.single.path!); - } - }, - ), - ItemBuilder.buildEntryItem( - context: context, - title: S.current.importFromEnteAuth, - description: S.current.importFromEnteAuthTip, - onTap: () async { - FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFromEnteAuthTitle, - type: FileType.custom, - allowedExtensions: ['txt'], - lockParentWindow: true, - ); - if (result != null) { - EnteAuthTokenImporter() - .importFromPath(result.files.single.path!); - } - }, - ), - ItemBuilder.buildEntryItem( - context: context, - title: S.current.importFromAndOTP, - description: S.current.importFromAndOTPTip, - onTap: () async { - FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFromAndOTPTitle, - type: FileType.custom, - allowedExtensions: ['json', 'aes'], - lockParentWindow: true, - ); - if (result != null) { - AndOTPTokenImporter().importFromPath(result.files.single.path!); - } - }, - ), - ItemBuilder.buildEntryItem( - context: context, - bottomRadius: true, - title: S.current.importFromBitwarden, - description: S.current.importFromBitwardenTip, - onTap: () async { - FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFromBitwardenTitle, - type: FileType.custom, - allowedExtensions: ['json', 'csv'], - lockParentWindow: true, - ); - if (result != null) { - BitwardenTokenImporter().importFromPath(result.files.single.path!); - } - }, - ), - const SizedBox(height: 10), ItemBuilder.buildCaptionItem( context: context, title: S.current.exportToThirdParty), ItemBuilder.buildEntryItem( diff --git a/lib/Screens/main_screen.dart b/lib/Screens/main_screen.dart index 350bea7d..4e30b978 100644 --- a/lib/Screens/main_screen.dart +++ b/lib/Screens/main_screen.dart @@ -41,6 +41,8 @@ import '../Utils/itoast.dart'; import '../Utils/lottie_util.dart'; import '../Utils/route_util.dart'; import '../Utils/utils.dart'; +import '../Widgets/BottomSheet/bottom_sheet_builder.dart'; +import '../Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart'; import '../Widgets/Custom/loading_icon.dart'; import '../Widgets/Dialog/custom_dialog.dart'; import '../Widgets/General/EasyRefresh/easy_refresh.dart'; @@ -646,6 +648,22 @@ class MainScreenState extends State }, ), const SizedBox(height: 4), + ItemBuilder.buildIconTextButton( + context, + quarterTurns: quarterTurns, + text: S.current.importFromThirdParty, + fontSizeDelta: -2, + showText: false, + direction: Axis.vertical, + icon: const Icon(Icons.apps_rounded), + onTap: () async { + RouteUtil.pushDialogRoute( + context, + const ImportFromThirdPartyBottomSheet(), + ); + }, + ), + const SizedBox(height: 4), if (provider.canShowCloudBackupButton && provider.showCloudBackupButton) ItemBuilder.buildIconTextButton( diff --git a/lib/TokenUtils/ThirdParty/enteauth_importer.dart b/lib/TokenUtils/ThirdParty/enteauth_importer.dart index d338404f..f8f36b8c 100644 --- a/lib/TokenUtils/ThirdParty/enteauth_importer.dart +++ b/lib/TokenUtils/ThirdParty/enteauth_importer.dart @@ -11,7 +11,6 @@ import 'package:cloudotp/Widgets/Dialog/progress_dialog.dart'; import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:pointycastle/export.dart'; import '../../Utils/ilogger.dart'; import '../../Utils/itoast.dart'; @@ -94,16 +93,16 @@ class EnteAuthTokenImporter implements BaseTokenImporter { static const int KeyLength = 32; static const int DerivationParallelism = 1; - static dynamic decrypt(String password, KdfParams param, String data, - String header) async { - final derivedKey = await CryptoUtil.deriveKey( - utf8.encode(password), - CryptoUtil.base642bin(param.salt), - param.memLimit, - param.opsLimit, - ); - Uint8List? decryptedContent; + static dynamic decrypt( + String password, KdfParams param, String data, String header) async { try { + final derivedKey = await CryptoUtil.deriveKey( + utf8.encode(password), + CryptoUtil.base642bin(param.salt), + param.memLimit, + param.opsLimit, + ); + Uint8List? decryptedContent; decryptedContent = await CryptoUtil.decryptData( CryptoUtil.base642bin(data), derivedKey, @@ -120,18 +119,15 @@ class EnteAuthTokenImporter implements BaseTokenImporter { List categories = []; List bindings = []; List uniqueTags = - toImportTokens.expand((element) => element.tags).toSet().toList(); + toImportTokens.expand((element) => element.tags).toSet().toList(); categories.addAll(uniqueTags .map((e) => TokenCategory.title(title: e)) .where((element) => !categories.contains(element))); for (var token in toImportTokens) { - bindings.addAll(token.tags.map((e) => - TokenCategoryBinding( + bindings.addAll(token.tags.map((e) => TokenCategoryBinding( tokenUid: token.uid, categoryUid: - categories - .firstWhere((element) => element.title == e) - .uid, + categories.firstWhere((element) => element.title == e).uid, ))); } await BaseTokenImporter.importResult( @@ -139,7 +135,8 @@ class EnteAuthTokenImporter implements BaseTokenImporter { } @override - Future importFromPath(String path, { + Future importFromPath( + String path, { bool showLoading = true, }) async { late ProgressDialog dialog; @@ -163,7 +160,7 @@ class EnteAuthTokenImporter implements BaseTokenImporter { } if (showLoading) dialog.dismiss(); InputValidateAsyncController validateAsyncController = - InputValidateAsyncController( + InputValidateAsyncController( listen: false, validator: (text) async { if (text.isEmpty) { @@ -172,25 +169,27 @@ class EnteAuthTokenImporter implements BaseTokenImporter { if (showLoading) { dialog.show(msg: S.current.importing, showProgress: false); } - var res = await compute( - (receiveMessage) async { - return await decrypt( - receiveMessage["password"] as String, - KdfParams.fromJson( - receiveMessage["params"] as Map), - receiveMessage["data"] as String, - receiveMessage["header"] as String); - }, - { - 'data': backup.encryptedData, - "params": backup.kdfParams.toJson(), - 'header': backup.encryptedNonce, - 'password': text, - }, - ); + // var res = await compute( + // (receiveMessage) async { + // return await decrypt( + // receiveMessage["password"] as String, + // KdfParams.fromJson( + // receiveMessage["params"] as Map), + // receiveMessage["data"] as String, + // receiveMessage["header"] as String); + // }, + // { + // 'data': backup.encryptedData, + // "params": backup.kdfParams.toJson(), + // 'header': backup.encryptedNonce, + // 'password': text, + // }, + // ); + var res = await decrypt(text, backup.kdfParams, backup.encryptedData, + backup.encryptedNonce); if (res[0] == DecryptResult.success) { List tokens = - await ImportTokenUtil.importText(res[1], showToast: false); + await ImportTokenUtil.importText(res[1], showToast: false); await import(tokens); if (showLoading) { dialog.dismiss(); @@ -209,29 +208,28 @@ class EnteAuthTokenImporter implements BaseTokenImporter { rootContext, responsive: true, useWideLandscape: true, - (context) => - InputBottomSheet( - validator: (value) { - if (value.isEmpty) { - return S.current.autoBackupPasswordCannotBeEmpty; - } - return null; - }, - checkSyncValidator: false, - validateAsyncController: validateAsyncController, - title: S.current.inputImportPasswordTitle, - message: S.current.inputImportPasswordTip, - hint: S.current.inputImportPasswordHint, - inputFormatters: [ - RegexInputFormatter.onlyNumberAndLetterAndSymbol, - ], - tailingType: InputItemTailingType.password, - onValidConfirm: (password) async {}, - ), + (context) => InputBottomSheet( + validator: (value) { + if (value.isEmpty) { + return S.current.autoBackupPasswordCannotBeEmpty; + } + return null; + }, + checkSyncValidator: false, + validateAsyncController: validateAsyncController, + title: S.current.inputImportPasswordTitle, + message: S.current.inputImportPasswordTip, + hint: S.current.inputImportPasswordHint, + inputFormatters: [ + RegexInputFormatter.onlyNumberAndLetterAndSymbol, + ], + tailingType: InputItemTailingType.password, + onValidConfirm: (password) async {}, + ), ); } catch (e) { List tokens = - await ImportTokenUtil.importText(content, showToast: false); + await ImportTokenUtil.importText(content, showToast: false); await import(tokens); } } diff --git a/lib/TokenUtils/ThirdParty/xchacha20poly1305.dart b/lib/TokenUtils/ThirdParty/xchacha20poly1305.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/Utils/asset_util.dart b/lib/Utils/asset_util.dart index 5844e647..235c857b 100644 --- a/lib/Utils/asset_util.dart +++ b/lib/Utils/asset_util.dart @@ -14,6 +14,23 @@ class AssetUtil { static const String emptyIcon = "assets/icon/empty.png"; + static const String ic2Fas = "assets/auth/ic_2fas.png"; + static const String icAegis = "assets/auth/ic_aegis.png"; + static const String icAndotp = "assets/auth/ic_andotp.png"; + static const String icAuthenticatorplus = "assets/auth/ic_authenticatorplus.png"; + static const String icAuthenticatorpro = "assets/auth/ic_authenticatorpro.png"; + static const String icAuthy = "assets/auth/ic_authy.png"; + static const String icBitwarden = "assets/auth/ic_bitwarden.png"; + static const String icBlizzard = "assets/auth/ic_blizzard.png"; + static const String icEnteauth = "assets/auth/ic_enteauth.png"; + static const String icFreeotp = "assets/auth/ic_freeotp.png"; + static const String icFreeotpplus = "assets/auth/ic_freeotpplus.png"; + static const String icGoogleauthenticator = "assets/auth/ic_googleauthenticator.png"; + static const String icLastpass = "assets/auth/ic_lastpass.png"; + static const String icSteam = "assets/auth/ic_steam.png"; + static const String icTotpauthenticator = "assets/auth/ic_totpauthenticator.png"; + static const String icWinauth = "assets/auth/ic_winauth.png"; + static load( String path, { double size = 24, diff --git a/lib/Widgets/BottomSheet/add_bottom_sheet.dart b/lib/Widgets/BottomSheet/add_bottom_sheet.dart index a68451aa..a08aeb76 100644 --- a/lib/Widgets/BottomSheet/add_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/add_bottom_sheet.dart @@ -18,6 +18,7 @@ import '../../Utils/route_util.dart'; import '../../Utils/utils.dart'; import '../../generated/l10n.dart'; import 'bottom_sheet_builder.dart'; +import 'import_from_third_party_bottom_sheet.dart'; class AddBottomSheet extends StatefulWidget { const AddBottomSheet({ @@ -313,6 +314,21 @@ class AddBottomSheetState extends State }, leading: Icons.import_export_rounded, ), + ItemBuilder.buildEntryItem( + context: context, + horizontalPadding: 20, + title: S.current.importFromThirdParty, + showLeading: true, + showTrailing: false, + onTap: () { + Navigator.pop(context); + RouteUtil.pushDialogRoute( + context, + const ImportFromThirdPartyBottomSheet(), + ); + }, + leading: Icons.apps_rounded, + ), ], ); } diff --git a/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart b/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart new file mode 100644 index 00000000..0f6e29b4 --- /dev/null +++ b/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart @@ -0,0 +1,228 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/andotp_importer.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/enteauth_importer.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/freeotpplus_importer.dart'; +import 'package:cloudotp/Utils/asset_util.dart'; +import 'package:cloudotp/Utils/itoast.dart'; +import 'package:cloudotp/Utils/responsive_util.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; + +import '../../TokenUtils/ThirdParty/2fas_importer.dart'; +import '../../TokenUtils/ThirdParty/aegis_importer.dart'; +import '../../TokenUtils/ThirdParty/bitwarden_importer.dart'; +import '../../Utils/app_provider.dart'; +import '../../Utils/file_util.dart'; +import '../../generated/l10n.dart'; +import '../General/EasyRefresh/easy_refresh.dart'; +import '../Item/item_builder.dart'; +import '../Scaffold/my_scaffold.dart'; +import 'add_bottom_sheet.dart'; +import 'bottom_sheet_builder.dart'; + +class ImportFromThirdPartyBottomSheet extends StatefulWidget { + const ImportFromThirdPartyBottomSheet({ + super.key, + }); + + @override + ImportFromThirdPartyBottomSheetState createState() => + ImportFromThirdPartyBottomSheetState(); +} + +class ImportFromThirdPartyBottomSheetState + extends State { + @override + Widget build(BuildContext context) { + return MyScaffold( + appBar: ItemBuilder.buildAppBar( + context: context, + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + leading: ResponsiveUtil.isLandscape() + ? Icons.close_rounded + : Icons.arrow_back_rounded, + onLeadingTap: () { + if (ResponsiveUtil.isLandscape()) { + dialogNavigatorState?.popPage(); + } else { + Navigator.pop(context); + } + }, + title: Text( + S.current.importFromThirdParty, + style: Theme.of(context) + .textTheme + .titleMedium + ?.apply(fontWeightDelta: 2), + ), + center: !ResponsiveUtil.isLandscape(), + actions: ResponsiveUtil.isLandscape() + ? [] + : [ + ItemBuilder.buildBlankIconButton(context), + const SizedBox(width: 5), + ], + ), + body: EasyRefresh( + child: _buildBody(), + ), + ); + } + + _buildBody({ + double spacing = 6, + }) { + return ListView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20), + children: [ + _buildItem( + asset: AssetUtil.icAndotp, + title: S.current.importFromAndOTP, + description: S.current.importFromAndOTPTip, + allowedExtensions: ['json', 'aes'], + onImport: (path) { + AndOTPTokenImporter().importFromPath(path); + }, + ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icAegis, + title: S.current.importFromAegis, + description: S.current.importFromAegisTip, + onImport: (path) { + AegisTokenImporter().importFromPath(path); + }, + ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icBitwarden, + title: S.current.importFromBitwarden, + description: S.current.importFromBitwardenTip, + onImport: (path) { + BitwardenTokenImporter().importFromPath(path); + }, + ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icEnteauth, + title: S.current.importFromEnteAuth, + description: S.current.importFromEnteAuthTip, + allowedExtensions: ['txt'], + onImport: (path) { + EnteAuthTokenImporter().importFromPath(path); + }, + ), + // SizedBox(height: spacing), + // _buildItem( + // asset: AssetUtil.icFreeotp, + // allowedExtensions: ['xml'], + // title: S.current.importFromFreeOTP, + // description: S.current.importFromFreeOTPTip, + // onImport: (path) { + // FreeOTPTokenImporter().importFromPath(path); + // }, + // ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icFreeotpplus, + title: S.current.importFromFreeOTPPlus, + description: S.current.importFromFreeOTPPlusTip, + onImport: (path) { + FreeOTPPlusTokenImporter().importFromPath(path); + }, + ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icGoogleauthenticator, + title: S.current.importFromGoogleAuthenticator, + description: S.current.importFromGoogleAuthenticatorTip, + useImport: false, + onImport: (path) { + if (ResponsiveUtil.isMobile()) { + BottomSheetBuilder.showBottomSheet( + rootContext, + enableDrag: false, + responsive: true, + (context) => const AddBottomSheet(onlyShowScanner: true), + ); + }else{ + IToast.showTop(S.current.importFromGoogleAuthenticatorInMobile); + } + }, + ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.ic2Fas, + title: S.current.importFrom2FAS, + description: S.current.importFrom2FASTip, + allowedExtensions: ['2fas'], + onImport: (path) { + TwoFASTokenImporter().importFromPath(path); + }, + ), + ], + ); + } + + _buildItem({ + required String asset, + required String title, + required String description, + List allowedExtensions = const ['json'], + Function(String)? onImport, + bool useImport = true, + }) { + return Material( + color: Theme.of(context).canvasColor, + borderRadius: BorderRadius.circular(10), + child: InkWell( + onTap: useImport + ? () async { + FilePickerResult? result = await FileUtil.pickFiles( + dialogTitle: S.current.importFromFreeOTPPlusTitle, + type: FileType.custom, + allowedExtensions: allowedExtensions, + lockParentWindow: true, + ); + if (result != null) { + onImport?.call(result.files.single.path!); + } + } + : () { + onImport?.call(""); + }, + borderRadius: BorderRadius.circular(10), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + AssetUtil.load(asset, size: 32), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + title, + maxLines: 1, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 5), + Text( + description, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/Widgets/Item/item_builder.dart b/lib/Widgets/Item/item_builder.dart index 54504233..341fe5b4 100644 --- a/lib/Widgets/Item/item_builder.dart +++ b/lib/Widgets/Item/item_builder.dart @@ -592,7 +592,7 @@ class ItemBuilder { ? Text(description, style: Theme.of(context) .textTheme - .labelSmall + .bodySmall ?.apply(fontSizeDelta: 1)) : emptyWidget, ], @@ -757,7 +757,7 @@ class ItemBuilder { description, style: Theme.of(context) .textTheme - .labelSmall + .bodySmall ?.apply( fontSizeDelta: 1, color: descriptionColor, diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 72279f3b..de7611ce 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -485,6 +485,7 @@ "importFromThirdParty": "Import from third-party software", "importFromGoogleAuthenticator": "Import from Google Authenticator", "importFromGoogleAuthenticatorTip": "Select the sidebar-Migrate Account-Export in Google Authenticator. A QR code will be displayed on the screen. Use CloudOTP to scan the QR code to import", + "importFromGoogleAuthenticatorInMobile": "Please scan the QR code from your mobile device to import", "importFrom2FAS": "importFrom2FAS", "importFrom2FASTitle": "Select the 2fas-backup.2fas file", "importFrom2FASTip": "In 2FAS select Settings - 2FAS Backup - Export to File; then import the 2fas-backup.2fas file", diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index 33be6ce2..6f18cb4f 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -485,6 +485,7 @@ "importFromThirdParty": "从第三方软件导入", "importFromGoogleAuthenticator": "从Google Authenticator导入", "importFromGoogleAuthenticatorTip": "在Google Authenticator中选择侧边栏-迁移账户-导出,屏幕上将会显示二维码,使用CloudOTP扫描该二维码即可导入", + "importFromGoogleAuthenticatorInMobile": "请从移动端设备扫码导入", "importFrom2FAS": "从2FAS导入", "importFrom2FASTitle": "选择2fas-backup.2fas文件", "importFrom2FASTip": "在2FAS中选择设置-2FAS备份-导出到文件;然后导入2fas-backup.2fas文件", diff --git a/lib/main.dart b/lib/main.dart index 66ea24e8..b34b58f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:cloudotp/Utils/hive_util.dart'; import 'package:cloudotp/Utils/request_header_util.dart'; import 'package:cloudotp/Widgets/Item/item_builder.dart'; import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; @@ -94,6 +95,7 @@ Future initApp(WidgetsBinding widgetsBinding) async { HiveUtil.setEncryptDatabaseStatus(EncryptDatabaseStatus.defaultPassword); } } + await initCryptoUtil(); NotificationUtil.init(); await TokenImageUtil.loadBrandLogos(); if (ResponsiveUtil.isMobile()) { diff --git a/pubspec.yaml b/pubspec.yaml index bb5569d9..6f4fb67f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -145,6 +145,7 @@ flutter: uses-material-design: true assets: - assets/lottie/ + - assets/auth/ - assets/icon/ - assets/brand/ - assets/logo.png diff --git a/third-party/ente_crypto/lib/src/crypto_util.dart b/third-party/ente_crypto/lib/src/crypto_util.dart index 3f370fae..17cc6195 100644 --- a/third-party/ente_crypto/lib/src/crypto_util.dart +++ b/third-party/ente_crypto/lib/src/crypto_util.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io' as io; import 'dart:developer'; +import 'dart:io' as io; import 'dart:typed_data'; import 'package:ente_crypto_dart/src/core/errors.dart'; import 'package:ente_crypto_dart/src/models/derived_key_result.dart'; import 'package:ente_crypto_dart/src/models/device_info.dart'; import 'package:ente_crypto_dart/src/models/encryption_result.dart'; +import 'package:flutter/cupertino.dart'; import 'package:logging/logging.dart'; import 'package:sodium/sodium_sumo.dart'; import 'package:sodium_libs/sodium_libs.dart'; @@ -19,7 +20,7 @@ const int hashChunkSize = 4 * 1024 * 1024; const int loginSubKeyLen = 32; const int loginSubKeyId = 1; const String loginSubKeyContext = "loginctx"; -late SodiumSumo sodium; +late final SodiumSumo sodium; // Computes and returns the hash of the source file Future getHash(io.File source) { @@ -500,6 +501,7 @@ class CryptoUtil { cryptoPwHash(password, salt, memLimit, opsLimit, sodium), ); } catch (e, s) { + debugPrint("$e\n$s"); final String errMessage = 'failed to deriveKey memLimit: $memLimit and ' 'opsLimit: $opsLimit'; Logger("CryptoUtilDeriveKey").warning(errMessage, e, s);