From 8b4944bc07a16b2b6a5dcafc569c9cd1edbe9f64 Mon Sep 17 00:00:00 2001 From: Robert-Stackflow Date: Thu, 5 Sep 2024 17:10:10 +0800 Subject: [PATCH] Support winauth and totp authenticator --- .../Token/import_export_token_screen.dart | 7 +- lib/Screens/Token/token_layout.dart | 386 +++++++++--------- lib/Screens/home_screen.dart | 7 +- .../authenticatorplus_importer.dart | 252 ++++++++++++ .../ThirdParty/base_token_importer.dart | 1 + .../totpauthenticator_importer.dart | 213 ++++++++++ .../ThirdParty/winauth_importer.dart | 166 ++++++++ lib/TokenUtils/import_token_util.dart | 1 + lib/Utils/constant.dart | 2 +- .../import_from_third_party_bottom_sheet.dart | 75 +++- lib/Widgets/Custom/progress_text.dart | 91 +++++ lib/l10n/intl_en.arb | 14 + lib/l10n/intl_zh_CN.arb | 14 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 10 +- pubspec.yaml | 2 +- 16 files changed, 1023 insertions(+), 220 deletions(-) create mode 100644 lib/TokenUtils/ThirdParty/authenticatorplus_importer.dart create mode 100644 lib/TokenUtils/ThirdParty/totpauthenticator_importer.dart create mode 100644 lib/TokenUtils/ThirdParty/winauth_importer.dart create mode 100644 lib/Widgets/Custom/progress_text.dart diff --git a/lib/Screens/Token/import_export_token_screen.dart b/lib/Screens/Token/import_export_token_screen.dart index c4901385..9585053e 100644 --- a/lib/Screens/Token/import_export_token_screen.dart +++ b/lib/Screens/Token/import_export_token_screen.dart @@ -299,7 +299,6 @@ class _ImportExportTokenScreenState extends State ItemBuilder.buildEntryItem( context: context, title: S.current.exportUriFile, - bottomRadius: true, description: S.current.exportUriFileHint, onTap: () async { DialogBuilder.showConfirmDialog( @@ -326,9 +325,9 @@ class _ImportExportTokenScreenState extends State ); }, ), - const SizedBox(height: 10), - ItemBuilder.buildCaptionItem( - context: context, title: S.current.exportToThirdParty), + // const SizedBox(height: 10), + // ItemBuilder.buildCaptionItem( + // context: context, title: S.current.exportToThirdParty), ItemBuilder.buildEntryItem( context: context, bottomRadius: true, diff --git a/lib/Screens/Token/token_layout.dart b/lib/Screens/Token/token_layout.dart index b2e4a660..c40ad274 100644 --- a/lib/Screens/Token/token_layout.dart +++ b/lib/Screens/Token/token_layout.dart @@ -400,17 +400,17 @@ class TokenLayoutState extends State ); } - _buildLinearProgress() { - return isHOTP - ? const SizedBox.shrink() + _buildLinearProgress([bool hideProgressBar = false]) { + return isHOTP || hideProgressBar + ? const SizedBox(height: 1) : ValueListenableBuilder( valueListenable: progressNotifier, builder: (context, progress, child) { return Container( - margin: const EdgeInsets.only(top: 3, bottom: 13), + constraints: const BoxConstraints(minHeight: 2, maxHeight: 2), child: LinearProgressIndicator( value: progress, - minHeight: 1, + minHeight: 2, color: progress > autoCopyNextCodeProgressThrehold ? Theme.of(context).primaryColor : Colors.red, @@ -443,6 +443,7 @@ class TokenLayoutState extends State ? Theme.of(context).primaryColor : Colors.red, backgroundColor: Colors.grey.withOpacity(0.3), + strokeCap: StrokeCap.round, ); }, ), @@ -457,7 +458,7 @@ class TokenLayoutState extends State autoCopyNextCodeProgressThrehold ? Theme.of(context).primaryColor : Colors.red, - fontSizeDelta: -2, + fontSizeDelta: -3, ), ); }, @@ -510,53 +511,54 @@ class TokenLayoutState extends State child: InkWell( onTap: _processTap, borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - children: [ - const SizedBox(height: 12), - Row( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(12)), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( children: [ - ItemBuilder.buildTokenImage(widget.token, size: 32), - const SizedBox(width: 8), - Expanded( - child: Text( - widget.token.issuer, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.apply(fontWeightDelta: 2), + const SizedBox(height: 12), + Row( + children: [ + ItemBuilder.buildTokenImage(widget.token, size: 32), + const SizedBox(width: 8), + Expanded( + child: Text( + widget.token.issuer, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleMedium + ?.apply(fontWeightDelta: 2), + ), + ), + if (isHOTP) _buildHOTPRefreshButton(padding: 6), + ], + ), + const SizedBox(height: 3), + Container( + constraints: + const BoxConstraints(minHeight: 56, maxHeight: 56), + child: _buildCodeLayout( + letterSpacing: 10, + alignment: Alignment.center, + fontSize: 27, ), ), - if (isHOTP) _buildHOTPRefreshButton(padding: 6), + const SizedBox(height: 5), ], ), - const SizedBox(height: 8), - Selector( - selector: (context, provider) => provider.hideProgressBar, - builder: (context, hideProgressBar, child) => Container( - constraints: BoxConstraints( - minHeight: hideProgressBar ? 42 : 58, - maxHeight: hideProgressBar ? 42 : 58, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildCodeLayout( - letterSpacing: 10, alignment: Alignment.center), - const SizedBox(height: 5), - if (!hideProgressBar) _buildLinearProgress(), - ], - ), - ), - ) - ], - ), + ), + Selector( + selector: (context, provider) => provider.hideProgressBar, + builder: (context, hideProgressBar, child) => + _buildLinearProgress(hideProgressBar), + ), + ], ), ), ), @@ -574,86 +576,82 @@ class TokenLayoutState extends State clipBehavior: Clip.hardEdge, child: InkWell( onTap: _processTap, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - children: [ - const SizedBox(height: 12), - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.token.issuer, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.apply(fontWeightDelta: 2), - ), - Text( - widget.token.account, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - const SizedBox(width: 8), - ItemBuilder.buildTokenImage(widget.token, size: 28), - ], + customBorder: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), ), - const SizedBox(height: 6), - Selector( - selector: (context, provider) => provider.hideProgressBar, - builder: (context, hideProgressBar, child) => Container( - constraints: BoxConstraints( - minHeight: hideProgressBar ? 39 : 53, - maxHeight: hideProgressBar ? 39 : 53, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + children: [ + const SizedBox(height: 12), + Row( children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(child: _buildCodeLayout()), - if (isHOTP) - _buildHOTPRefreshButton( - padding: 4, - color: textTheme.labelSmall?.color, - ), - ItemBuilder.buildIconButton( - context: context, - padding: const EdgeInsets.all(4), - icon: Icon( - Icons.more_vert_rounded, - color: Theme.of(context) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.token.issuer, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) .textTheme - .labelSmall - ?.color, - size: 20, + .titleMedium + ?.apply(fontWeightDelta: 2), ), - onTap: showContextMenu, - ), - ], + Text( + widget.token.account, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), - if (!hideProgressBar) _buildLinearProgress(), - if (hideProgressBar) const SizedBox(height: 2), + const SizedBox(width: 8), + ItemBuilder.buildTokenImage(widget.token, size: 28), ], ), - ), + Container( + constraints: + const BoxConstraints(minHeight: 56, maxHeight: 56), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded(child: _buildCodeLayout(letterSpacing: 8)), + if (isHOTP) + _buildHOTPRefreshButton( + padding: 4, + color: textTheme.labelSmall?.color, + ), + ItemBuilder.buildIconButton( + context: context, + padding: const EdgeInsets.all(4), + icon: Icon( + Icons.more_vert_rounded, + color: + Theme.of(context).textTheme.labelSmall?.color, + size: 20, + ), + onTap: showContextMenu, + ), + ], + ), + ), + ], ), - ], - ), + ), + Selector( + selector: (context, provider) => provider.hideProgressBar, + builder: (context, hideProgressBar, child) => + _buildLinearProgress(hideProgressBar), + ), + ], ), ), ), @@ -671,84 +669,92 @@ class TokenLayoutState extends State child: InkWell( onTap: _processTap, borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - ), - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - children: [ - const SizedBox(height: 12), - Row( + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( children: [ - ItemBuilder.buildTokenImage(widget.token, size: 36), - const SizedBox(width: 8), - Expanded( + const SizedBox(height: 12), + Row( + children: [ + ItemBuilder.buildTokenImage(widget.token, size: 36), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.token.issuer, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleMedium + ?.apply(fontWeightDelta: 2), + ), + Text( + widget.token.account, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + if (isHOTP) _buildHOTPRefreshButton(), + ItemBuilder.buildIconButton( + context: context, + icon: Icon(Icons.edit_rounded, + color: Theme.of(context).iconTheme.color, + size: 20), + onTap: _processEdit), + ItemBuilder.buildIconButton( + context: context, + icon: Icon(Icons.qr_code_rounded, + color: Theme.of(context).iconTheme.color, + size: 20), + onTap: _processViewQrCode), + ItemBuilder.buildIconButton( + context: context, + icon: Icon(Icons.more_vert_rounded, + color: Theme.of(context).iconTheme.color, + size: 20), + onTap: showContextMenu, + ), + ], + ), + const SizedBox(height: 2), + Container( + constraints: + const BoxConstraints(minHeight: 60, maxHeight: 60), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - widget.token.issuer, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium - ?.apply(fontWeightDelta: 2), - ), - Text( - widget.token.account, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall, + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + _buildCodeLayout(fontSize: 28, letterSpacing: 10) + ], ), ], ), ), - if (isHOTP) _buildHOTPRefreshButton(), - ItemBuilder.buildIconButton( - context: context, - icon: Icon(Icons.edit_rounded, - color: Theme.of(context).iconTheme.color, size: 20), - onTap: _processEdit), - ItemBuilder.buildIconButton( - context: context, - icon: Icon(Icons.qr_code_rounded, - color: Theme.of(context).iconTheme.color, size: 20), - onTap: _processViewQrCode), - ItemBuilder.buildIconButton( - context: context, - icon: Icon(Icons.more_vert_rounded, - color: Theme.of(context).iconTheme.color, size: 20), - onTap: showContextMenu, - ), ], ), - const SizedBox(height: 4), - Selector( - selector: (context, provider) => provider.hideProgressBar, - builder: (context, hideProgressBar, child) => Container( - constraints: BoxConstraints( - minHeight: hideProgressBar ? 49 : 62, - maxHeight: hideProgressBar ? 49 : 62, - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [_buildCodeLayout(fontSize: 30)], - ), - if (!hideProgressBar) _buildLinearProgress(), - if (hideProgressBar) const SizedBox(height: 4), - ], - ), - ), - ), - ], - ), + ), + Selector( + selector: (context, provider) => provider.hideProgressBar, + builder: (context, hideProgressBar, child) => + _buildLinearProgress(hideProgressBar), + ), + ], ), ), ), @@ -806,7 +812,9 @@ class TokenLayoutState extends State constraints: const BoxConstraints( maxHeight: 45, minHeight: 45), child: _buildCodeLayout( - fontSize: 30, forceNoType: false), + fontSize: 28, + forceNoType: false, + letterSpacing: 10), ), ], ), diff --git a/lib/Screens/home_screen.dart b/lib/Screens/home_screen.dart index 5a91af16..1c298cf3 100644 --- a/lib/Screens/home_screen.dart +++ b/lib/Screens/home_screen.dart @@ -744,6 +744,7 @@ class HomeScreenState extends State with TickerProviderStateMixin { ); Widget body = tokens.isEmpty ? ListView( + padding: const EdgeInsets.symmetric(vertical: 50), children: [ ItemBuilder.buildEmptyPlaceholder( context: context, @@ -1088,11 +1089,11 @@ enum LayoutType { double getHeight([bool hideProgressBar = false]) { switch (this) { case LayoutType.Simple: - return hideProgressBar ? 94 : 110; + return 108; case LayoutType.Compact: - return hideProgressBar ? 97 : 111; + return 108; case LayoutType.Tile: - return hideProgressBar ? 105 : 118; + return 114; case LayoutType.List: return 60; case LayoutType.Spotlight: diff --git a/lib/TokenUtils/ThirdParty/authenticatorplus_importer.dart b/lib/TokenUtils/ThirdParty/authenticatorplus_importer.dart new file mode 100644 index 00000000..227750c9 --- /dev/null +++ b/lib/TokenUtils/ThirdParty/authenticatorplus_importer.dart @@ -0,0 +1,252 @@ +import 'dart:io'; + +import 'package:cloudotp/Database/database_manager.dart'; +import 'package:cloudotp/Models/opt_token.dart'; +import 'package:cloudotp/Models/token_category.dart'; +import 'package:cloudotp/Models/token_category_binding.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/base_token_importer.dart'; +import 'package:cloudotp/Utils/file_util.dart'; +import 'package:cloudotp/Utils/utils.dart'; +import 'package:cloudotp/Widgets/Dialog/progress_dialog.dart'; +import 'package:convert/convert.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart'; +import 'package:sqflite_sqlcipher/sqflite.dart'; + +import '../../Utils/Base32/base32.dart'; +import '../../Utils/ilogger.dart'; +import '../../Utils/itoast.dart'; +import '../../generated/l10n.dart'; + +enum AuthenticatorType { + Totp, + Hotp, + Blizzard; + + OtpTokenType get tokenType { + switch (this) { + case AuthenticatorType.Totp: + return OtpTokenType.TOTP; + case AuthenticatorType.Hotp: + return OtpTokenType.HOTP; + case AuthenticatorType.Blizzard: + return OtpTokenType.TOTP; + } + } +} + +class AuthenticatorPlusToken { + static const String _blizzardIssuer = "Blizzard"; + static const OtpDigits _blizzardDigits = OtpDigits.D8; + + final String uid; + final String email; + final String secret; + final int counter; + final AuthenticatorType type; + final String issuer; + final String originalName; + final String categoryName; + + AuthenticatorPlusToken({ + required this.email, + required this.secret, + required this.counter, + required this.type, + required this.issuer, + required this.originalName, + required this.categoryName, + }) : uid = Utils.generateUid(); + + factory AuthenticatorPlusToken.fromMap(Map map) { + return AuthenticatorPlusToken( + email: map['email'] as String, + secret: map['secret'] as String, + counter: map['counter'] as int, + type: AuthenticatorType.values[map['type'] as int], + issuer: map['issuer'] as String, + originalName: map['original_name'] as String, + categoryName: map['category'] as String, + ); + } + + String _convertSecret(AuthenticatorType type) { + if (this.type == AuthenticatorType.Blizzard) { + final bytes = hex.decode(secret); + final base32Secret = base32.encode(Uint8List.fromList(bytes)); + return base32Secret; + } + return secret; + } + + OtpToken toOtpToken() { + String issuer = ""; + String? username; + if (issuer.isNotEmpty) { + issuer = + type == AuthenticatorType.Blizzard ? _blizzardIssuer : this.issuer; + if (email.isNotEmpty) { + username = email; + } + } else { + final originalNameParts = originalName.split(':'); + if (originalNameParts.length == 2) { + issuer = originalNameParts[0]; + if (issuer.isEmpty) { + issuer = email; + } else { + username = email; + } + } else { + issuer = email; + } + } + final secret = _convertSecret(type); + OtpToken token = OtpToken.init(); + token.uid = uid; + token.issuer = issuer; + token.account = username ?? ""; + token.secret = secret; + token.tokenType = type.tokenType; + token.counterString = counter > 0 + ? counter.toString() + : token.tokenType.defaultDigits.toString(); + token.digits = type == AuthenticatorType.Blizzard + ? _blizzardDigits + : token.tokenType.defaultDigits; + token.algorithm = OtpAlgorithm.fromString("SHA1"); + token.periodString = token.tokenType.defaultPeriod.toString(); + return token; + } +} + +class AuthenticatorPlusGroup { + String id; + String name; + + AuthenticatorPlusGroup({ + required this.id, + required this.name, + }); + + TokenCategory toTokenCategory() { + return TokenCategory.title( + tUid: id, + title: name, + ); + } + + factory AuthenticatorPlusGroup.fromJson(Map json) { + return AuthenticatorPlusGroup( + id: Utils.generateUid(), + name: json['name'], + ); + } +} + +class AuthenticatorPlusTokenImporter implements BaseTokenImporter { + static const String baseAlgorithm = 'AES'; + static const String mode = 'GCM'; + static const String padding = 'NoPadding'; + static const String algorithmDescription = '$baseAlgorithm/$mode/$padding'; + + static const int iterations = 10000; + static const int keyLength = 32; + + Future _convertFromConnectionAsync(Database database) async { + final sourceAccounts = await database.query('accounts'); + final sourceCategories = await database.query('category'); + + final authenticators = []; + final categories = sourceCategories + .map((row) => AuthenticatorPlusGroup.fromJson(row)) + .toList(); + final bindings = []; + + for (final accountRow in sourceAccounts) { + try { + final account = AuthenticatorPlusToken.fromMap(accountRow); + + if (account.categoryName != "All Accounts") { + late final AuthenticatorPlusGroup? category; + try { + categories.firstWhere((c) => c.name == account.categoryName); + } catch (e) { + category = null; + } + if (category == null) continue; + + final binding = TokenCategoryBinding( + tokenUid: account.uid, + categoryUid: category.id, + ); + + bindings.add(binding); + } + } catch (e, t) { + debugPrint("Failed to convert account: $e\n$t"); + } + } + + final backup = ImporterResult( + authenticators.map((e) => e.toOtpToken()).toList(), + categories.map((e) => e.toTokenCategory()).toList(), + bindings, + ); + + return backup; + } + + @override + Future importFromPath( + String path, { + bool showLoading = true, + }) async { + late ProgressDialog dialog; + if (showLoading) { + dialog = + showProgressDialog(msg: S.current.importing, showProgress: false); + } + try { + File file = File(path); + if (!file.existsSync()) { + IToast.showTop(S.current.fileNotExist); + } else { + try { + final path = join(await FileUtil.getDatabaseDir(), + '${DateTime.now().millisecondsSinceEpoch}.db'); + await file.copy(path); + String password = ""; + final database = await DatabaseManager.cipherDbFactory.openDatabase( + path, + options: OpenDatabaseOptions( + version: 1, + singleInstance: true, + onConfigure: (db) async { + await db.execute('PRAGMA cipher_compatibility = 3'); + await db.rawQuery("PRAGMA KEY='$password'"); + }, + ), + ); + try { + ImporterResult result = await _convertFromConnectionAsync(database); + await BaseTokenImporter.importResult(result); + } catch (e) { + IToast.showTop(S.current.importFailed); + } finally { + await database.close(); + } + } finally { + File(path).deleteSync(); + } + } + } catch (e, t) { + ILogger.error("Failed to import from 2FAS", e, t); + IToast.showTop(S.current.importFailed); + } finally { + if (showLoading) { + dialog.dismiss(); + } + } + } +} diff --git a/lib/TokenUtils/ThirdParty/base_token_importer.dart b/lib/TokenUtils/ThirdParty/base_token_importer.dart index e89483b9..5953a027 100644 --- a/lib/TokenUtils/ThirdParty/base_token_importer.dart +++ b/lib/TokenUtils/ThirdParty/base_token_importer.dart @@ -7,6 +7,7 @@ import '../import_token_util.dart'; enum DecryptResult { success, + noFileInZip, invalidPasswordOrDataCorrupted, } diff --git a/lib/TokenUtils/ThirdParty/totpauthenticator_importer.dart b/lib/TokenUtils/ThirdParty/totpauthenticator_importer.dart new file mode 100644 index 00000000..3c7c9c0c --- /dev/null +++ b/lib/TokenUtils/ThirdParty/totpauthenticator_importer.dart @@ -0,0 +1,213 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cloudotp/Models/opt_token.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/base_token_importer.dart'; +import 'package:cloudotp/Utils/Base32/base32.dart'; +import 'package:cloudotp/Utils/app_provider.dart'; +import 'package:cloudotp/Widgets/Dialog/progress_dialog.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:convert/convert.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/block/aes.dart'; +import 'package:pointycastle/block/modes/cbc.dart'; +import 'package:pointycastle/paddings/pkcs7.dart'; + +import '../../Utils/ilogger.dart'; +import '../../Utils/itoast.dart'; +import '../../Widgets/BottomSheet/bottom_sheet_builder.dart'; +import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; +import '../../Widgets/Item/input_item.dart'; +import '../../generated/l10n.dart'; + +class TotpAuthenticatorAccount { + final String issuer; + final String name; + final String key; + final String digits; + final String period; + final int base; + + TotpAuthenticatorAccount({ + required this.issuer, + required this.name, + required this.key, + required this.digits, + required this.period, + required this.base, + }); + + factory TotpAuthenticatorAccount.fromJson(Map json) { + return TotpAuthenticatorAccount( + issuer: json['issuer'] as String, + name: json['name'] as String, + key: json['key'] as String, + digits: json['digits'] as String, + period: json['period'] as String, + base: json['base'] as int, + ); + } + + OtpToken toOtpToken() { + var secretBytes = hex.decode(key); + var secret = base32.encode(Uint8List.fromList(secretBytes)); + OtpToken token = OtpToken.init(); + token.issuer = issuer; + token.account = name; + token.secret = secret; + token.digits = OtpDigits.fromString(digits); + token.periodString = period; + token.algorithm = OtpAlgorithm.SHA1; + token.tokenType = OtpTokenType.TOTP; + return token; + } + + Map toJson() { + return { + 'issuer': issuer, + 'name': name, + 'key': key, + 'digits': digits, + 'period': period, + 'base': base, + }; + } +} + +class TotpAuthenticatorTokenImporter implements BaseTokenImporter { + static const String Algorithm = "AES/CBC/PKCS7"; + + static List? decrypt(String data, String password) { + if (password.isEmpty) { + return null; + } + + final passwordBytes = utf8.encode(password); + final digest = Digest('SHA-256'); + final key = digest.process(Uint8List.fromList(passwordBytes)); + + final actualBytes = base64.decode(data); + + var keyParameter = ParametersWithIV(KeyParameter(key), Uint8List(16)); + var cipher = CBCBlockCipher(AESEngine())..init(false, keyParameter); + + try { + Uint8List decryptedBytes; + final decrypted = Uint8List(actualBytes.length); + var offset = 0; + while (offset < actualBytes.length) { + offset += cipher.processBlock(actualBytes, offset, decrypted, offset); + } + final padding = PKCS7Padding(); + final padCount = padding.padCount(decrypted); + decryptedBytes = decrypted.sublist(0, decrypted.length - padCount); + + var json = utf8.decode(decryptedBytes); + json = json.substring(2); + json = json.substring(0, json.lastIndexOf(']') + 1); + json = json.replaceAll(r'\"', '"'); + + final decoded = jsonDecode(json) as List; + return decoded + .map((e) => + TotpAuthenticatorAccount.fromJson(e as Map)) + .toList(); + } catch (e, t) { + debugPrint("Failed to decrypt 2FAS data: $e\n$t"); + return null; + } + } + + Future import(List accounts) async { + List tokens = []; + tokens = accounts.map((e) => e.toOtpToken()).toList(); + await BaseTokenImporter.importResult(ImporterResult(tokens, [], [])); + } + + @override + Future importFromPath( + String path, { + bool showLoading = true, + }) async { + late ProgressDialog dialog; + if (showLoading) { + dialog = + showProgressDialog(msg: S.current.importing, showProgress: false); + } + try { + File file = File(path); + if (!file.existsSync()) { + IToast.showTop(S.current.fileNotExist); + } else { + var content = file.readAsStringSync(); + if (showLoading) dialog.dismiss(); + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + listen: false, + validator: (text) async { + if (text.isEmpty) { + return S.current.autoBackupPasswordCannotBeEmpty; + } + if (showLoading) { + dialog.show(msg: S.current.importing, showProgress: false); + } + var res = await compute( + (receiveMessage) { + return decrypt(receiveMessage["data"] as String, + receiveMessage["password"] as String); + }, + { + 'data': content, + 'password': text, + }, + ); + if (res != null) { + await import(res); + if (showLoading) { + dialog.dismiss(); + } + return null; + } else { + if (showLoading) { + dialog.dismiss(); + } + return S.current.invalidPasswordOrDataCorrupted; + } + }, + controller: TextEditingController(), + ); + BottomSheetBuilder.showBottomSheet( + 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 {}, + ), + ); + } + } catch (e, t) { + ILogger.error("Failed to import from 2FAS", e, t); + IToast.showTop(S.current.importFailed); + } finally { + if (showLoading) { + dialog.dismiss(); + } + } + } +} diff --git a/lib/TokenUtils/ThirdParty/winauth_importer.dart b/lib/TokenUtils/ThirdParty/winauth_importer.dart new file mode 100644 index 00000000..8196fe32 --- /dev/null +++ b/lib/TokenUtils/ThirdParty/winauth_importer.dart @@ -0,0 +1,166 @@ +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/base_token_importer.dart'; +import 'package:cloudotp/Utils/app_provider.dart'; +import 'package:cloudotp/Utils/file_util.dart'; +import 'package:cloudotp/Widgets/Dialog/progress_dialog.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; + +import '../../Models/opt_token.dart'; +import '../../Utils/ilogger.dart'; +import '../../Utils/itoast.dart'; +import '../../Widgets/BottomSheet/bottom_sheet_builder.dart'; +import '../../Widgets/BottomSheet/input_bottom_sheet.dart'; +import '../../Widgets/Item/input_item.dart'; +import '../../generated/l10n.dart'; +import '../import_token_util.dart'; + +class WinauthTokenImporter implements BaseTokenImporter { + static dynamic decrypt(Uint8List data, String password) { + final archive = ZipDecoder().decodeBytes(data, password: password); + ArchiveFile? fileEntry; + try { + archive.files.firstWhere((file) => file.isFile); + } catch (e) { + fileEntry = null; + } + + if (fileEntry == null) { + return [ + DecryptResult.noFileInZip, + null, + ]; + } + + final outputData = BytesBuilder(); + + try { + // If password is required and provided + if (password.isNotEmpty) { + fileEntry = _decryptZipFile(fileEntry, password); + } + outputData.add(fileEntry.content as List); + } catch (e) { + return [ + DecryptResult.invalidPasswordOrDataCorrupted, + null, + ]; + } + + return outputData.toBytes(); + } + + static ArchiveFile _decryptZipFile(ArchiveFile fileEntry, String password) { + // Perform decryption logic here, if needed, depending on zip file encryption. + // Use any decryption algorithms supported by the 'archive' or another package. + return fileEntry; + } + + Future import(List toImportTokens) async { + await BaseTokenImporter.importResult( + ImporterResult(toImportTokens, [], [])); + } + + @override + Future importFromPath( + String path, { + bool showLoading = true, + }) async { + late ProgressDialog dialog; + if (showLoading) { + dialog = + showProgressDialog(msg: S.current.importing, showProgress: false); + } + try { + File file = File(path); + if (!file.existsSync()) { + IToast.showTop(S.current.fileNotExist); + } else if (FileUtil.getFileExtension(path) == 'txt') { + var content = file.readAsStringSync(); + List tokens = + await ImportTokenUtil.importText(content, showToast: false); + await import(tokens); + } else if (FileUtil.getFileExtension(path) == 'zip') { + IToast.showTop(S.current.importFromWinauthNotSupportZip); + return; + var bytes = file.readAsBytesSync(); + if (showLoading) dialog.dismiss(); + InputValidateAsyncController validateAsyncController = + InputValidateAsyncController( + listen: false, + validator: (text) async { + if (text.isEmpty) { + return S.current.autoBackupPasswordCannotBeEmpty; + } + if (showLoading) { + dialog.show(msg: S.current.importing, showProgress: false); + } + var res = await compute( + (receiveMessage) { + return decrypt( + Uint8List.fromList(receiveMessage["data"] as List), + receiveMessage["password"] as String); + }, + { + 'data': bytes.toList(), + 'password': text, + }, + ); + if (res[0] == DecryptResult.success) { + List tokens = + await ImportTokenUtil.importText(res[1], showToast: false); + await import(tokens); + if (showLoading) { + dialog.dismiss(); + } + return null; + } else if (res[0] == DecryptResult.noFileInZip) { + if (showLoading) { + dialog.dismiss(); + } + return S.current.noFileInZip; + } else { + if (showLoading) { + dialog.dismiss(); + } + return S.current.invalidPasswordOrDataCorrupted; + } + }, + controller: TextEditingController(), + ); + BottomSheetBuilder.showBottomSheet( + 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 {}, + ), + ); + } + } catch (e, t) { + ILogger.error("Failed to import from Winauth", e, t); + IToast.showTop(S.current.importFailed); + } finally { + if (showLoading) { + dialog.dismiss(); + } + } + } +} diff --git a/lib/TokenUtils/import_token_util.dart b/lib/TokenUtils/import_token_util.dart index 89d3305a..e2e0b27f 100644 --- a/lib/TokenUtils/import_token_util.dart +++ b/lib/TokenUtils/import_token_util.dart @@ -482,6 +482,7 @@ class ImportTokenUtil { List lines = content.split("\n"); List tokens = []; for (String line in lines) { + line = line.trim(); List parsedTokens = OtpTokenParser.parseUri(line); if (parsedTokens.isNotEmpty) { tokens.addAll(parsedTokens); diff --git a/lib/Utils/constant.dart b/lib/Utils/constant.dart index 20025307..3df81fdd 100644 --- a/lib/Utils/constant.dart +++ b/lib/Utils/constant.dart @@ -17,7 +17,7 @@ const Widget emptyWidget = SizedBox.shrink(); const defaultWindowSize = Size(1120, 740); -const minimumSize = Size(620, 540); +const minimumSize = Size(620, 580); const double autoCopyNextCodeProgressThrehold = 0.25; const int defaultHOTPPeriod = 15; diff --git a/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart b/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart index 0f6e29b4..093433c2 100644 --- a/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart +++ b/lib/Widgets/BottomSheet/import_from_third_party_bottom_sheet.dart @@ -2,6 +2,8 @@ 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/TokenUtils/ThirdParty/totpauthenticator_importer.dart'; +import 'package:cloudotp/TokenUtils/ThirdParty/winauth_importer.dart'; import 'package:cloudotp/Utils/asset_util.dart'; import 'package:cloudotp/Utils/itoast.dart'; import 'package:cloudotp/Utils/responsive_util.dart'; @@ -77,27 +79,40 @@ class ImportFromThirdPartyBottomSheetState 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'], + asset: AssetUtil.icAegis, + title: S.current.importFromAegis, + dialogTitle: S.current.importFromAegisTitle, + description: S.current.importFromAegisTip, onImport: (path) { - AndOTPTokenImporter().importFromPath(path); + AegisTokenImporter().importFromPath(path); }, ), SizedBox(height: spacing), _buildItem( - asset: AssetUtil.icAegis, - title: S.current.importFromAegis, - description: S.current.importFromAegisTip, + asset: AssetUtil.icAndotp, + title: S.current.importFromAndOTP, + dialogTitle: S.current.importFromAndOTPTitle, + description: S.current.importFromAndOTPTip, + allowedExtensions: ['json', 'aes'], onImport: (path) { - AegisTokenImporter().importFromPath(path); + AndOTPTokenImporter().importFromPath(path); }, ), + // _buildItem( + // asset: AssetUtil.icAuthenticatorplus, + // title: S.current.importFromAuthenticatorPlus, + // dialogTitle: S.current.importFromAuthenticatorPlusTitle, + // description: S.current.importFromAuthenticatorPlusTip, + // allowedExtensions: ['db'], + // onImport: (path) { + // AuthenticatorPlusTokenImporter().importFromPath(path); + // }, + // ), SizedBox(height: spacing), _buildItem( asset: AssetUtil.icBitwarden, title: S.current.importFromBitwarden, + dialogTitle: S.current.importFromBitwardenTitle, description: S.current.importFromBitwardenTip, onImport: (path) { BitwardenTokenImporter().importFromPath(path); @@ -107,6 +122,7 @@ class ImportFromThirdPartyBottomSheetState _buildItem( asset: AssetUtil.icEnteauth, title: S.current.importFromEnteAuth, + dialogTitle: S.current.importFromEnteAuthTitle, description: S.current.importFromEnteAuthTip, allowedExtensions: ['txt'], onImport: (path) { @@ -118,6 +134,7 @@ class ImportFromThirdPartyBottomSheetState // asset: AssetUtil.icFreeotp, // allowedExtensions: ['xml'], // title: S.current.importFromFreeOTP, + // dialogTitle: S.current.importFromFreeOTPTitle, // description: S.current.importFromFreeOTPTip, // onImport: (path) { // FreeOTPTokenImporter().importFromPath(path); @@ -127,6 +144,7 @@ class ImportFromThirdPartyBottomSheetState _buildItem( asset: AssetUtil.icFreeotpplus, title: S.current.importFromFreeOTPPlus, + dialogTitle: S.current.importFromFreeOTPPlusTitle, description: S.current.importFromFreeOTPPlusTip, onImport: (path) { FreeOTPPlusTokenImporter().importFromPath(path); @@ -146,21 +164,55 @@ class ImportFromThirdPartyBottomSheetState responsive: true, (context) => const AddBottomSheet(onlyShowScanner: true), ); - }else{ + } else { IToast.showTop(S.current.importFromGoogleAuthenticatorInMobile); } }, ), + // SizedBox(height: spacing), + // _buildItem( + // asset: AssetUtil.icLastpass, + // title: S.current.importFromLastPassAuthenticator, + // dialogTitle: S.current.importFromLastPassAuthenticatorTitle, + // description: S.current.importFromLastPassAuthenticatorTip, + // allowedExtensions: ['json'], + // onImport: (path) { + // WinauthTokenImporter().importFromPath(path); + // }, + // ), SizedBox(height: spacing), _buildItem( asset: AssetUtil.ic2Fas, title: S.current.importFrom2FAS, + dialogTitle: S.current.importFrom2FASTitle, description: S.current.importFrom2FASTip, allowedExtensions: ['2fas'], onImport: (path) { TwoFASTokenImporter().importFromPath(path); }, ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icTotpauthenticator, + title: S.current.importFromTOTPAuthenticator, + dialogTitle: S.current.importFromTOTPAuthenticatorTitle, + description: S.current.importFromTOTPAuthenticatorTip, + allowedExtensions: ['encrypt'], + onImport: (path) { + TotpAuthenticatorTokenImporter().importFromPath(path); + }, + ), + SizedBox(height: spacing), + _buildItem( + asset: AssetUtil.icWinauth, + title: S.current.importFromWinauth, + dialogTitle: S.current.importFromWinauthTitle, + description: S.current.importFromWinauthTip, + allowedExtensions: ['zip', 'txt'], + onImport: (path) { + WinauthTokenImporter().importFromPath(path); + }, + ), ], ); } @@ -168,6 +220,7 @@ class ImportFromThirdPartyBottomSheetState _buildItem({ required String asset, required String title, + String? dialogTitle, required String description, List allowedExtensions = const ['json'], Function(String)? onImport, @@ -180,7 +233,7 @@ class ImportFromThirdPartyBottomSheetState onTap: useImport ? () async { FilePickerResult? result = await FileUtil.pickFiles( - dialogTitle: S.current.importFromFreeOTPPlusTitle, + dialogTitle: dialogTitle, type: FileType.custom, allowedExtensions: allowedExtensions, lockParentWindow: true, diff --git a/lib/Widgets/Custom/progress_text.dart b/lib/Widgets/Custom/progress_text.dart new file mode 100644 index 00000000..e44e80ac --- /dev/null +++ b/lib/Widgets/Custom/progress_text.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +class ProgressText extends StatelessWidget { + final String text; + final double progress; + final BuildContext mContext; + final Color? color; + final Color? backgroundColor; + final double? fontSize; + final double? letterSpacing; + final AlignmentGeometry? alignment; + + static const Color defaultColor = Colors.green; + static const Color defaultBackgroundColor = Colors.grey; + + const ProgressText( + this.mContext, + this.text, { + super.key, + required this.progress, + this.color = defaultColor, + this.backgroundColor = defaultBackgroundColor, + this.fontSize = 24, + this.letterSpacing = 5, + this.alignment = Alignment.centerLeft, + }); + + @override + Widget build(BuildContext context) { + return RichText( + text: buildTextSpan(), + ); + } + + TextSpan buildTextSpan() { + final int completedLength = (text.length * progress).floor(); + final double partialProgress = text.length * progress - completedLength; + + List spans = []; + + if (completedLength > 0) { + spans.add( + TextSpan( + text: text.substring(0, completedLength), + style: TextStyle(color: color), + ), + ); + } + + if (completedLength < text.length) { + final String partialChar = text[completedLength]; + spans.add( + TextSpan( + text: partialChar, + style: TextStyle( + foreground: Paint()..shader = _createPartialShader(partialProgress), + ), + ), + ); + } + + if (completedLength + 1 < text.length) { + spans.add( + TextSpan( + text: text.substring(completedLength + 1), + style: TextStyle(color: backgroundColor), + ), + ); + } + + return TextSpan( + children: spans, + style: Theme.of(mContext).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + fontSize: fontSize!+30.0, + // letterSpacing: 5, + ), + ); + } + + Shader _createPartialShader(double progress) { + return LinearGradient( + colors: [ + color ?? defaultColor, + backgroundColor ?? defaultBackgroundColor + ], + stops: [progress, progress], + ).createShader(const Rect.fromLTWH(0, 0, 50, 0)); + } +} + diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index de7611ce..124722ad 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -486,6 +486,20 @@ "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", + "importFromTOTPAuthenticator": "Import from TOTP Authenticator", + "importFromTOTPAuthenticatorTitle": "Select the TOTP_Backup.encrypt file", + "importFromTOTPAuthenticatorTip": "Select the sidebar-Backup/Restore-Backup Data in TOTP Authenticator; then import the TOTP_Backup.encrypt file", + "importFromWinauth": "Import from Winauth", + "importFromWinauthTitle": "Select the winauth.zip or winauth.txt file", + "importFromWinauthTip": "Select More-Export in Winauth; then import the winauth.zip or winauth.txt file. Importing PGP encrypted files is not supported yet.", + "importFromWinauthNotSupportZip": "Importing zip files is not supported yet", + "importFromAuthenticatorPlus": "Import from Authenticator Plus", + "importFromAuthenticatorPlusTitle": "Select the authplus.db file", + "importFromAuthenticatorPlusTip": "Select More-Backup in Authenticator Plus; then import the authplus.db file", + "importFromLastPassAuthenticator": "Import from LastPass Authenticator", + "importFromLastPassAuthenticatorTitle": "Select the LastPassAuthenticator.json file", + "importFromLastPassAuthenticatorTip": "Export the backup in LastPass Authenticator; then import the LastPassAuthenticator.json file", + "noFileInZip": "No file in the zip file", "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 6f18cb4f..1c78e2a3 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -486,6 +486,9 @@ "importFromGoogleAuthenticator": "从Google Authenticator导入", "importFromGoogleAuthenticatorTip": "在Google Authenticator中选择侧边栏-迁移账户-导出,屏幕上将会显示二维码,使用CloudOTP扫描该二维码即可导入", "importFromGoogleAuthenticatorInMobile": "请从移动端设备扫码导入", + "importFromTOTPAuthenticator": "从TOTP Authenticator导入", + "importFromTOTPAuthenticatorTitle": "选择TOTP_Backup.encrypt文件", + "importFromTOTPAuthenticatorTip": "在TOTP Authenticator中选择侧边栏-备份/恢复-备份数据;然后导入TOTP_Backup.encrypt文件", "importFrom2FAS": "从2FAS导入", "importFrom2FASTitle": "选择2fas-backup.2fas文件", "importFrom2FASTip": "在2FAS中选择设置-2FAS备份-导出到文件;然后导入2fas-backup.2fas文件", @@ -495,6 +498,17 @@ "importFromFreeOTP": "从FreeOTP导入", "importFromFreeOTPTitle": "选择externalBackup.xml文件", "importFromFreeOTPTip": "在FreeOTP中选择更多-备份;然后导入externalBackup.xml文件", + "importFromWinauth": "从Winauth导入", + "importFromWinauthTitle": "选择winauth.zip或winauth.txt文件", + "importFromWinauthTip": "在Winauth中选择更多-导出;然后导入winauth.zip或winauth.txt文件,暂未支持导入PGP加密后的文件", + "importFromWinauthNotSupportZip": "暂未支持导入zip文件", + "noFileInZip": "压缩包中无文件", + "importFromAuthenticatorPlus": "从Authenticator Plus导入", + "importFromAuthenticatorPlusTitle": "选择authplus.db文件", + "importFromAuthenticatorPlusTip": "在Authenticator Plus中选择更多-备份;然后导入authplus.db文件", + "importFromLastPassAuthenticator": "从LastPass Authenticator导入", + "importFromLastPassAuthenticatorTitle": "选择LastPassAuthenticator.json文件", + "importFromLastPassAuthenticatorTip": "在LastPass Authenticator中导出备份;然后导入LastPassAuthenticator.json文件", "importFromFreeOTPPlus": "从FreeOTP+导入", "importFromFreeOTPPlusTitle": "选择freeotp-backup.json文件", "importFromFreeOTPPlusTip": "在FreeOTP+中选择更多-导出-导出JSON格式;然后导入freeotp-backup.json文件", diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a9e2fe8e..083f6317 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import audio_session import desktop_webview_window import device_info_plus -import flutter_archive import flutter_inappwebview_macos import flutter_local_notifications import flutter_secure_storage_macos @@ -39,7 +38,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) - FlutterArchivePlugin.register(with: registry.registrar(forPlugin: "FlutterArchivePlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 6ff8a264..42b0ec29 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: source: hosted version: "2.0.2" archive: - dependency: transitive + dependency: "direct main" description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d @@ -436,14 +436,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_archive: - dependency: "direct main" - description: - name: flutter_archive - sha256: "5ca235f304c12bf468979235f400f79846d204169d715939e39197106f5fc970" - url: "https://pub.flutter-io.cn" - source: hosted - version: "6.0.3" flutter_cache_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f4fb67f..93e1fdb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,7 +38,7 @@ dependencies: path_provider: ^2.0.12 file_picker: ^8.0.6 # 文件选择 hive: ^4.0.0-dev.2 # 轻量存储 - flutter_archive: ^6.0.3 # 压缩 + archive: ^3.6.1 # 压缩 isar_flutter_libs: ^4.0.0-dev.13 sqflite_sqlcipher: ^3.1.0+1 # SQLite加密 # 网络