ente/auth/lib/models/code.dart

312 lines
7.5 KiB
Dart

import 'dart:convert';
import 'package:ente_auth/models/code_display.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:logging/logging.dart';
class Code {
static const defaultDigits = 6;
static const steamDigits = 5;
static const defaultPeriod = 30;
int? generatedID;
final String account;
final String issuer;
final int digits;
final int period;
final String secret;
final Algorithm algorithm;
final Type type;
/// otpauth url in the code
final String rawData;
final int counter;
bool? hasSynced;
final CodeDisplay display;
bool get isPinned => display.pinned;
bool get isTrashed => display.trashed;
final Object? err;
bool get hasError => err != null;
Code(
this.account,
this.issuer,
this.digits,
this.period,
this.secret,
this.algorithm,
this.type,
this.counter,
this.rawData, {
this.generatedID,
required this.display,
this.err,
});
factory Code.withError(Object error, String rawData) {
return Code(
"",
"",
0,
0,
"",
Algorithm.sha1,
Type.totp,
0,
rawData,
err: error,
display: CodeDisplay(),
);
}
Code copyWith({
String? account,
String? issuer,
int? digits,
int? period,
String? secret,
Algorithm? algorithm,
Type? type,
int? counter,
CodeDisplay? display,
}) {
final String updateAccount = account ?? this.account;
final String updateIssuer = issuer ?? this.issuer;
final int updatedDigits = digits ?? this.digits;
final int updatePeriod = period ?? this.period;
final String updatedSecret = secret ?? this.secret;
final Algorithm updatedAlgo = algorithm ?? this.algorithm;
final Type updatedType = type ?? this.type;
final int updatedCounter = counter ?? this.counter;
final CodeDisplay updatedDisplay = display ?? this.display;
final String encodedIssuer = Uri.encodeQueryComponent(updateIssuer);
return Code(
updateAccount,
updateIssuer,
updatedDigits,
updatePeriod,
updatedSecret,
updatedAlgo,
updatedType,
updatedCounter,
"otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}"
"&digits=$updatedDigits&issuer=$encodedIssuer"
"&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
generatedID: generatedID,
display: updatedDisplay,
);
}
static Code fromAccountAndSecret(
Type type,
String account,
String issuer,
String secret,
CodeDisplay? display,
int digits,
) {
final String encodedIssuer = Uri.encodeQueryComponent(issuer);
return Code(
account,
issuer,
digits,
defaultPeriod,
secret,
Algorithm.sha1,
type,
0,
"otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret",
display: display ?? CodeDisplay(),
);
}
static Code fromOTPAuthUrl(String rawData, {CodeDisplay? display}) {
Uri uri = Uri.parse(rawData);
final issuer = _getIssuer(uri);
final account = _getAccount(uri);
try {
final code = Code(
account,
issuer,
_getDigits(uri),
_getPeriod(uri),
getSanitizedSecret(uri.queryParameters['secret']!),
_getAlgorithm(uri),
_getType(uri),
_getCounter(uri),
rawData,
display: CodeDisplay.fromUri(uri) ?? CodeDisplay(),
);
return code;
} catch (e) {
// if account name contains # without encoding,
// rest of the url are treated as url fragment
if (rawData.contains("#")) {
return Code.fromOTPAuthUrl(rawData.replaceAll("#", '%23'));
} else {
Logger("Code").warning(
'Error while parsing code for issuer $issuer, $account',
e,
);
rethrow;
}
}
}
static String _getAccount(Uri uri) {
try {
String path = Uri.decodeComponent(uri.path);
if (path.startsWith("/")) {
path = path.substring(1, path.length);
}
// Parse account name from documented auth URI
// otpauth://totp/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
if (uri.queryParameters.containsKey("issuer") && !path.contains(":")) {
return path;
}
return path.split(':')[1];
} catch (e) {
return "";
}
}
static Code fromExportJson(Map rawJson) {
Code resultCode = Code.fromOTPAuthUrl(
rawJson['rawData'],
display: CodeDisplay.fromJson(rawJson['display']),
);
return resultCode;
}
String toOTPAuthUrlFormat() {
final uri = Uri.parse(rawData.replaceAll("#", '%23'));
final query = {...uri.queryParameters};
query["codeDisplay"] = jsonEncode(display.toJson());
final newUri = uri.replace(queryParameters: query);
return jsonEncode(newUri.toString());
}
static String _getIssuer(Uri uri) {
try {
if (uri.queryParameters.containsKey("issuer")) {
String issuerName = uri.queryParameters['issuer']!;
// Handle issuer name with period
// See https://github.com/ente-io/ente/pull/77
if (issuerName.contains("period=")) {
return issuerName.substring(0, issuerName.indexOf("period="));
}
return issuerName;
}
final String path = Uri.decodeComponent(uri.path);
return path.split(':')[0].substring(1);
} catch (e) {
return "";
}
}
static int _getDigits(Uri uri) {
try {
return int.parse(uri.queryParameters['digits']!);
} catch (e) {
if (uri.host == "steam") {
return steamDigits;
}
return defaultDigits;
}
}
static int _getPeriod(Uri uri) {
try {
return int.parse(uri.queryParameters['period']!);
} catch (e) {
return defaultPeriod;
}
}
static int _getCounter(Uri uri) {
try {
final bool hasCounterKey = uri.queryParameters.containsKey('counter');
if (!hasCounterKey) {
return 0;
}
return int.parse(uri.queryParameters['counter']!);
} catch (e) {
return defaultPeriod;
}
}
static Algorithm _getAlgorithm(Uri uri) {
try {
final algorithm =
uri.queryParameters['algorithm'].toString().toLowerCase();
if (algorithm == "sha256") {
return Algorithm.sha256;
} else if (algorithm == "sha512") {
return Algorithm.sha512;
}
} catch (e) {
// nothing
}
return Algorithm.sha1;
}
static Type _getType(Uri uri) {
if (uri.host == "totp") {
return Type.totp;
} else if (uri.host == "steam") {
return Type.steam;
} else if (uri.host == "hotp") {
return Type.hotp;
}
throw UnsupportedError("Unsupported format with host ${uri.host}");
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Code &&
other.account == account &&
other.issuer == issuer &&
other.digits == digits &&
other.period == period &&
other.secret == secret &&
other.counter == counter &&
other.type == type &&
other.rawData == rawData;
}
@override
int get hashCode {
return account.hashCode ^
issuer.hashCode ^
digits.hashCode ^
period.hashCode ^
secret.hashCode ^
type.hashCode ^
counter.hashCode ^
rawData.hashCode;
}
}
enum Type {
totp,
hotp,
steam;
bool get isTOTPCompatible => this == totp || this == steam;
}
enum Algorithm {
sha1,
sha256,
sha512,
}