-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support winauth and totp authenticator
- Loading branch information
1 parent
087ff0e
commit 8b4944b
Showing
16 changed files
with
1,023 additions
and
220 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
252 changes: 252 additions & 0 deletions
252
lib/TokenUtils/ThirdParty/authenticatorplus_importer.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<String, dynamic> 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<String, dynamic> 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<ImporterResult> _convertFromConnectionAsync(Database database) async { | ||
final sourceAccounts = await database.query('accounts'); | ||
final sourceCategories = await database.query('category'); | ||
|
||
final authenticators = <AuthenticatorPlusToken>[]; | ||
final categories = sourceCategories | ||
.map((row) => AuthenticatorPlusGroup.fromJson(row)) | ||
.toList(); | ||
final bindings = <TokenCategoryBinding>[]; | ||
|
||
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<void> 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(); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.