From 41d093acf4d953053372bc0c5ad85d7befa44254 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sun, 14 Jul 2024 23:11:21 +1000 Subject: [PATCH 01/10] wip: refactor of otp [no ci] --- lib/pages/add.dart | 8 +- lib/state/app_state.dart | 2 +- .../lib/legacy/legacy_authenticator_item.dart | 20 +-- state/lib/legacy/legacy_repository.dart | 2 +- state/lib/repository/repository.dart | 2 +- state/lib/repository/repository_base.dart | 2 +- state/test/legacy_repository_test.dart | 8 +- totp/lib/models/otp_algorithm.dart | 36 +++++ totp/lib/models/otp_item.dart | 148 ++++++++++++++++++ totp/lib/models/otp_type.dart | 13 ++ totp/lib/{ => otp}/base32.dart | 0 totp/lib/otp/code_generator_base.dart | 3 + totp/lib/{ => otp}/totp_algorithm.dart | 43 +---- totp/lib/otp/totp_code_generator.dart | 18 +++ totp/lib/otp_uri.dart | 65 -------- totp/lib/parser/otp_uri_parser.dart | 94 +++++++++++ totp/lib/parser/uri_parser_base.dart | 11 ++ totp/lib/totp.dart | 2 +- totp/lib/totp_item.dart | 110 ------------- totp/test/totp_test.dart | 19 +-- 20 files changed, 357 insertions(+), 249 deletions(-) create mode 100644 totp/lib/models/otp_algorithm.dart create mode 100644 totp/lib/models/otp_item.dart create mode 100644 totp/lib/models/otp_type.dart rename totp/lib/{ => otp}/base32.dart (100%) create mode 100644 totp/lib/otp/code_generator_base.dart rename totp/lib/{ => otp}/totp_algorithm.dart (65%) create mode 100644 totp/lib/otp/totp_code_generator.dart delete mode 100644 totp/lib/otp_uri.dart create mode 100644 totp/lib/parser/otp_uri_parser.dart create mode 100644 totp/lib/parser/uri_parser_base.dart delete mode 100644 totp/lib/totp_item.dart diff --git a/lib/pages/add.dart b/lib/pages/add.dart index 42d6581..d6bd0c8 100644 --- a/lib/pages/add.dart +++ b/lib/pages/add.dart @@ -1,8 +1,8 @@ import 'package:another_authenticator/state/app_state.dart'; import 'package:another_authenticator/ui/adaptive.dart' show AdaptiveDialogAction, AppScaffold, isPlatformAndroid; -import 'package:another_authenticator_totp/totp_algorithm.dart'; -import 'package:another_authenticator_totp/totp.dart' show Base32, TotpItem; +import 'package:another_authenticator_totp/models/otp_algorithm.dart'; +import 'package:another_authenticator_totp/totp.dart' show Base32, OtpItem; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -56,7 +56,7 @@ class _AddPageState extends State { } // Adds item or display errors - void addItem(TotpItem item) { + void addItem(OtpItem item) { try { Provider.of(context, listen: false).addItem(item).then((_) { Navigator.pop(context); @@ -118,7 +118,7 @@ class _AddPageState extends State { } // Initialise and add TOTP item - var item = TotpItem( + var item = OtpItem( _secretController.text, _digits, _period, diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index 8275b10..f0aaed8 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -29,7 +29,7 @@ class AppState extends ChangeNotifier { } /// Adds a TOTP item to the list. - Future addItem(TotpItem item) async { + Future addItem(OtpItem item) async { await _repository.addItem(item); await loadItems(); } diff --git a/state/lib/legacy/legacy_authenticator_item.dart b/state/lib/legacy/legacy_authenticator_item.dart index 08ba516..1567732 100644 --- a/state/lib/legacy/legacy_authenticator_item.dart +++ b/state/lib/legacy/legacy_authenticator_item.dart @@ -5,16 +5,16 @@ class LegacyAuthenticatorItem { /// Legacy GUID id final String id; - final TotpItem totp; + final OtpItem totp; LegacyAuthenticatorItem(this.id, this.totp); static LegacyAuthenticatorItem newAuthenticatorItemFromUri(String uri) { var id = Uuid().v4(); - return LegacyAuthenticatorItem(id, TotpItem.fromUri(uri)); + return LegacyAuthenticatorItem(id, OtpItem.fromUri(uri)); } - static LegacyAuthenticatorItem newAuthenticatorItem(TotpItem item) { + static LegacyAuthenticatorItem newAuthenticatorItem(OtpItem item) { var id = Uuid().v4(); return LegacyAuthenticatorItem(id, item); } @@ -33,18 +33,12 @@ class LegacyAuthenticatorItem { /// Legacy decode. LegacyAuthenticatorItem.fromMap(Map json) : id = json['id'], - totp = TotpItem.fromJSON(json); + totp = OtpItem.fromJSON(json); /// Legacy encode. - Map toMap() => { - 'id': id, - 'accountName': totp.accountName, - 'issuer': totp.issuer, - 'secret': totp.secret, - 'digits': totp.digits, - 'period': totp.period, - 'algorithm': totp.algorithm.name - }; + Map toMap() { + return {'id': id, ...totp.toJSON()}; + } @override bool operator ==(Object other) => diff --git a/state/lib/legacy/legacy_repository.dart b/state/lib/legacy/legacy_repository.dart index 0011d01..5376afc 100644 --- a/state/lib/legacy/legacy_repository.dart +++ b/state/lib/legacy/legacy_repository.dart @@ -64,7 +64,7 @@ class LegacyRepository implements RepositoryBase { } @override - Future addItem(TotpItem item) async { + Future addItem(OtpItem item) async { var legacyAuthenticatorItem = LegacyAuthenticatorItem.newAuthenticatorItem(item); _items.add(legacyAuthenticatorItem); diff --git a/state/lib/repository/repository.dart b/state/lib/repository/repository.dart index d99a22f..312ddff 100644 --- a/state/lib/repository/repository.dart +++ b/state/lib/repository/repository.dart @@ -32,7 +32,7 @@ class Repository implements RepositoryBase { } @override - Future addItem(TotpItem item) async { + Future addItem(OtpItem item) async { return _legacyRepository.addItem(item); } } diff --git a/state/lib/repository/repository_base.dart b/state/lib/repository/repository_base.dart index 2287c47..5e6d6ec 100644 --- a/state/lib/repository/repository_base.dart +++ b/state/lib/repository/repository_base.dart @@ -11,5 +11,5 @@ abstract class RepositoryBase { Future replaceItems(List state); /// Add an item to the list. - Future addItem(TotpItem item); + Future addItem(OtpItem item); } diff --git a/state/test/legacy_repository_test.dart b/state/test/legacy_repository_test.dart index 5c2d580..7142ca2 100644 --- a/state/test/legacy_repository_test.dart +++ b/state/test/legacy_repository_test.dart @@ -1,5 +1,6 @@ import 'package:another_authenticator_state/legacy/legacy_repository.dart'; import 'package:another_authenticator_state/state.dart'; +import 'package:another_authenticator_totp/models/otp_type.dart'; import 'package:another_authenticator_totp/totp.dart'; import 'package:test/test.dart'; @@ -19,7 +20,7 @@ void main() { test('Add Item', () async { var repository = LegacyRepository(TestFileStorage()); - var item = TotpItem("ABCDEF"); + var item = OtpItem(OtpType.totp, "ABCDEF", "Test"); await repository.addItem(item); var items = await repository.loadItems(); @@ -29,10 +30,11 @@ void main() { test('Replace items', () async { var repository = LegacyRepository(TestFileStorage()); - var item = TotpItem("A"); + var item = OtpItem(OtpType.totp, "A", "Test"); await repository.addItem(item); - var item2 = LegacyAuthenticatorItem("id", TotpItem("A")); + var item2 = + LegacyAuthenticatorItem("id", OtpItem(OtpType.totp, "A", "Test")); await repository.replaceItems([item2]); var newItems = await repository.loadItems(); diff --git a/totp/lib/models/otp_algorithm.dart b/totp/lib/models/otp_algorithm.dart new file mode 100644 index 0000000..cc97f74 --- /dev/null +++ b/totp/lib/models/otp_algorithm.dart @@ -0,0 +1,36 @@ +import 'package:crypto/crypto.dart'; + +/// Hash algorithm to OTP +enum OtpHashAlgorithm { + sha1, + sha256, + sha512; + + static OtpHashAlgorithm fromString(String str) { + if (str.toLowerCase() == "sha1") { + return OtpHashAlgorithm.sha1; + } else if (str.toLowerCase() == "sha256") { + return OtpHashAlgorithm.sha256; + } else if (str.toLowerCase() == "sha512") { + return OtpHashAlgorithm.sha512; + } + throw Exception("Unsupported algorithm"); + } + + static bool isValid(String str) { + return OtpHashAlgorithm.values.any((v) => v.name == str.toLowerCase()); + } +} + +extension StringOperations on OtpHashAlgorithm { + Hash toHashFunction() { + if (this == OtpHashAlgorithm.sha1) { + return sha1; + } else if (this == OtpHashAlgorithm.sha256) { + return sha256; + } else if (this == OtpHashAlgorithm.sha512) { + return sha512; + } + throw Exception("Unsupported algorithm"); + } +} diff --git a/totp/lib/models/otp_item.dart b/totp/lib/models/otp_item.dart new file mode 100644 index 0000000..c04cbb9 --- /dev/null +++ b/totp/lib/models/otp_item.dart @@ -0,0 +1,148 @@ +import 'package:another_authenticator_totp/models/otp_type.dart'; +import 'package:another_authenticator_totp/otp/code_generator_base.dart'; +import 'package:another_authenticator_totp/otp/totp_code_generator.dart'; +import 'package:another_authenticator_totp/parser/uri_parser_base.dart'; + +import './otp_algorithm.dart'; +import '../parser/otp_uri_parser.dart' show OtpAuthUriParser; + +/// Represents a TOTP item and associated information. +/// +/// Has properties of accountName, issuer, secret, digits, period and algorithm, +/// as well as an id which is randomly assigned on generation. +class OtpItem { + OtpItem(this.type, this.secret, this.label, + [this.digits, this.period, this.algorithm, this.issuer, this.counter]); + // TODO: Checks on construction? + + /// Type of item (Currently only supports TOTP) + final OtpType type; + + /// Label + final String? label; + + /// Secret key (in base32) + final String secret; + + /// Issuer + final String? issuer; + + /// Algorithm (sha1/sha256/sha512) + final OtpHashAlgorithm? algorithm; + + /// Number of digits (of code) + final int? digits; + + /// Time period + final int? period; + + /// Counter + final int? counter; + + // /// Creates a new TOTP item. + // static OtpItem newTotpItem(String secret, + // [int digits = 6, + // int period = 60, + // OtpHashAlgorithm algorithm = OtpHashAlgorithm.sha1, + // String issuer = "", + // String accountName = ""]) { + // return OtpItem(secret, digits, period, algorithm, issuer, accountName); + // } + + /// Parses a TOTP key URI and returns a TOTPItem. + static final List _parsers = [OtpAuthUriParser()]; + static OtpItem fromUri(String uri) { + for (final parser in _parsers) { + if (parser.canParse(uri)) { + return parser.parse(uri); + } + } + throw FormatException("Unrecognized URI scheme, cannot parse..."); + } + + /// Generates a formatted TOTP value for the given [time]. + String getPrettyCode(int time) { + return _prettyValue(getCode(time)); + } + + /// Generates a TOTP value for the given [time]. + String getCode(int time) { + OtpCodeGeneratorBase gen = + TotpCodeGenerator(secret, digits, period, algorithm); + return gen.generateCode(time); + } + + /// Returns a placeholder representation of the generated code. + String get placeholder { + if (digits == 8) { + return '···· ····'; + } + return '··· ···'; + } + + /// Formats a generated [code] to make it look nice + static String _prettyValue(String code) { + // Length at which to split at + int splitLength = code.length == 8 ? 4 : 3; + // Combine 2 halves + return '${code.substring(0, splitLength)} ${code.substring(splitLength)}'; + } + + /// Decode item from JSON. + OtpItem.fromJSON(Map json) + : type = json.containsKey('type') + ? OtpType.fromString(json['type']) + : OtpType.totp, + secret = json['secret'], + label = json.containsKey('label') ? json['label'] : null, + digits = json['digits'], + period = json['period'], + algorithm = OtpHashAlgorithm.fromString(json['algorithm']), + issuer = json['issuer'], + counter = json.containsKey('counter') ? json['counter'] : null; + + // TODO: Handle accountName + // OtpItem.fromJSON(Map json) + // : accountName = json['accountName'], + // issuer = json['issuer'], + // period = json['period'], + // digits = json['digits'], + // algorithm = OtpHashAlgorithm.values.byName(json['algorithm']), + // secret = json['secret']; + + /// Encode item to JSON. + Map toJSON() => { + 'type': type.name, + 'secret': secret, + 'label': label, + 'digits': digits, + 'period': period, + 'algorithm': algorithm?.name, + 'issuer': issuer, + 'counter': counter + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OtpItem && + type == other.type && + secret == other.secret && + label == other.label && + digits == other.digits && + period == other.period && + algorithm == other.algorithm && + issuer == other.issuer && + counter == other.counter; + + @override + int get hashCode => + type.hashCode ^ + secret.hashCode ^ + label.hashCode ^ + digits.hashCode ^ + period.hashCode ^ + algorithm.hashCode ^ + issuer.hashCode ^ + counter.hashCode; +} diff --git a/totp/lib/models/otp_type.dart b/totp/lib/models/otp_type.dart new file mode 100644 index 0000000..e9b1c67 --- /dev/null +++ b/totp/lib/models/otp_type.dart @@ -0,0 +1,13 @@ +enum OtpType { + hotp, + totp; + + static OtpType fromString(String str) { + if (str.toLowerCase() == "totp") { + return OtpType.totp; + } else if (str.toLowerCase() == "hotp") { + return OtpType.hotp; + } + throw Exception("Unsupported type"); + } +} diff --git a/totp/lib/base32.dart b/totp/lib/otp/base32.dart similarity index 100% rename from totp/lib/base32.dart rename to totp/lib/otp/base32.dart diff --git a/totp/lib/otp/code_generator_base.dart b/totp/lib/otp/code_generator_base.dart new file mode 100644 index 0000000..a533ad5 --- /dev/null +++ b/totp/lib/otp/code_generator_base.dart @@ -0,0 +1,3 @@ +abstract interface class OtpCodeGeneratorBase { + String generateCode(int time); +} diff --git a/totp/lib/totp_algorithm.dart b/totp/lib/otp/totp_algorithm.dart similarity index 65% rename from totp/lib/totp_algorithm.dart rename to totp/lib/otp/totp_algorithm.dart index 67eaafc..6310a83 100644 --- a/totp/lib/totp_algorithm.dart +++ b/totp/lib/otp/totp_algorithm.dart @@ -1,38 +1,9 @@ import 'dart:math' show pow; import 'dart:typed_data' show Uint8List, Endian; -import 'package:crypto/crypto.dart' show Hash, Hmac, sha1, sha256, sha512; -import 'base32.dart' show Base32; - -/// Hash algorithm to OTP -enum OtpHashAlgorithm { - sha1, - sha256, - sha512; - - static OtpHashAlgorithm fromString(String str) { - if (str == "sha1") { - return OtpHashAlgorithm.sha1; - } else if (str == "sha256") { - return OtpHashAlgorithm.sha256; - } else if (str == "sha512") { - return OtpHashAlgorithm.sha512; - } - throw Exception("Unknown algorithm"); - } -} +import 'package:crypto/crypto.dart'; -extension _StringOperations on OtpHashAlgorithm { - Hash toHashFunction() { - if (this == OtpHashAlgorithm.sha1) { - return sha1; - } else if (this == OtpHashAlgorithm.sha256) { - return sha256; - } else if (this == OtpHashAlgorithm.sha512) { - return sha512; - } - throw Exception("Unknown algorithm"); - } -} +import '../models/otp_algorithm.dart'; +import 'base32.dart' show Base32; /// Static class for generating TOTP codes. /// @@ -41,14 +12,6 @@ extension _StringOperations on OtpHashAlgorithm { /// * https://github.com/LanceGin/dotp/blob/master/lib/src/otp.dart /// * https://stackoverflow.com/questions/49398437 class Totp { - /// Formats a generated [code] to make it look nice - static String prettyValue(String code) { - // Length at which to split at - int splitLength = code.length == 8 ? 4 : 3; - // Combine 2 halves - return '${code.substring(0, splitLength)} ${code.substring(splitLength)}'; - } - /// Generates a TOTP value for the given attributes. /// /// Note: diff --git a/totp/lib/otp/totp_code_generator.dart b/totp/lib/otp/totp_code_generator.dart new file mode 100644 index 0000000..30ae09a --- /dev/null +++ b/totp/lib/otp/totp_code_generator.dart @@ -0,0 +1,18 @@ +import 'package:another_authenticator_totp/models/otp_algorithm.dart'; +import 'package:another_authenticator_totp/otp/code_generator_base.dart'; +import 'package:another_authenticator_totp/otp/totp_algorithm.dart'; + +class TotpCodeGenerator implements OtpCodeGeneratorBase { + final String secret; + final int? digits; + final int? period; + final OtpHashAlgorithm? algorithm; + + TotpCodeGenerator(this.secret, [this.digits, this.period, this.algorithm]); + + @override + String generateCode(int time) { + return Totp.generateCode(time, secret, digits ?? 6, period ?? 30, + algorithm ?? OtpHashAlgorithm.sha1); + } +} diff --git a/totp/lib/otp_uri.dart b/totp/lib/otp_uri.dart deleted file mode 100644 index b322148..0000000 --- a/totp/lib/otp_uri.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'totp_algorithm.dart'; -import 'totp_item.dart' show TotpItem; - -/// Parses TOTP key URI into TOTPItems. -/// -/// Reference: -/// * https://github.com/google/google-authenticator/wiki/Key-Uri-Format -class OtpUri { - /// Parses a TOTP key URI and returns a TOTP object - static TotpItem fromUri(String uri) { - // Use dart-core/Uri to parse - var parsed = Uri.parse(uri); - - // Check scheme - if (parsed.scheme != "otpauth") { - throw FormatException("Not OTP URI"); - } - if (parsed.authority != "totp" && parsed.authority != "hotp") { - throw FormatException("Unsupported authority"); - } - - // Extract issuer/account name - if (parsed.pathSegments.length > 1) { - throw FormatException("Should have more than 1 path segment"); - } - var pathSplit = parsed.pathSegments[0].split(':'); - var issuer = pathSplit[0]; - var accountName = pathSplit.length > 1 ? pathSplit[1] : ''; - - // Extract algorithm, digits, period and secret - var algorithm = "sha1"; - var digits = 6; - var period = 30; - var secret = ""; - - if (!parsed.queryParameters.containsKey("secret")) { - throw FormatException("Query parameter does not contain secret"); - } - secret = parsed.queryParameters["secret"]!; - - if (parsed.queryParameters.containsKey("algorithm")) { - algorithm = parsed.queryParameters["algorithm"]!.toLowerCase(); - if (algorithm != "sha1" && - algorithm != "sha256" && - algorithm != "sha512") { - throw FormatException("Unrecognised algorithm"); - } - } - - if (parsed.queryParameters.containsKey("digits")) { - digits = int.parse(parsed.queryParameters["digits"]!); - } - - if (parsed.queryParameters.containsKey("period")) { - period = int.parse(parsed.queryParameters["period"]!); - } - - try { - return TotpItem.newTotpItem(secret, digits, period, - OtpHashAlgorithm.fromString(algorithm), issuer, accountName); - } catch (error) { - throw FormatException("Incorrect parameters"); - } - } -} diff --git a/totp/lib/parser/otp_uri_parser.dart b/totp/lib/parser/otp_uri_parser.dart new file mode 100644 index 0000000..f520f17 --- /dev/null +++ b/totp/lib/parser/otp_uri_parser.dart @@ -0,0 +1,94 @@ +import "./uri_parser_base.dart"; +import '../models/otp_type.dart'; +import '../models/otp_algorithm.dart'; +import '../models/otp_item.dart'; + +/// Parses TOTP key URI into TOTPItems. +/// +/// Reference: +/// * https://github.com/google/google-authenticator/wiki/Key-Uri-Format +class OtpAuthUriParser implements UriParserBase { + /// Whether URI can be parsed by current parser + @override + bool canParse(String uri) { + try { + var parsed = Uri.parse(uri); + return parsed.scheme == "otpauth"; + } on FormatException { + return false; + } + } + + /// Parses a Key URI and returns an OTP object + @override + OtpItem parse(String uri) { + var parsed = Uri.parse(uri); + + if (parsed.scheme != "otpauth") { + throw UriParseException("Not OTP URI"); + } + + var typeValue = parsed.authority.toLowerCase(); + if (typeValue != "totp" && typeValue != "hotp") { + throw UriParseException("Unsupported type"); + } + var type = OtpType.fromString(typeValue); + + if (parsed.pathSegments.length != 1) { + throw UriParseException("Should have 1 path segment"); + } + var label = parsed.pathSegments[0]; + + // secret: Mandatory, key value encoded in Base32 + if (!parsed.queryParameters.containsKey("secret")) { + throw UriParseException("URI does not contain secret"); + } + var secret = parsed.queryParameters["secret"]!; + + // issuer: Recommended, if absent, issuer from label may be taken + String? issuer; + if (parsed.queryParameters.containsKey("issuer")) { + issuer = parsed.queryParameters["issuer"]!; + } + + // algorithm: Optional, SHA1 (Default), SHA256, SHA512 + OtpHashAlgorithm? algorithm; + if (parsed.queryParameters.containsKey("algorithm")) { + var algorithmValue = parsed.queryParameters["algorithm"]!; + if (!OtpHashAlgorithm.isValid(algorithmValue)) { + throw UriParseException("Invalid algorithm"); + } + algorithm = OtpHashAlgorithm.fromString(algorithmValue); + } + + // digits: Optional, 6 (Default), number of digits in output + int? digits; + if (parsed.queryParameters.containsKey("digits")) { + digits = int.parse(parsed.queryParameters["digits"]!); + if (digits != 6 && digits != 8) { + throw UriParseException("Unsupported number of digits"); + } + } + + // period: Optional, 30 (Default), time period in seconds + int? period; + if (parsed.queryParameters.containsKey("period")) { + period = int.parse(parsed.queryParameters["period"]!); + if (period <= 0) { + throw UriParseException("Invalid period"); + } + } + + // counter: Optional, Initial counter value for hotp + int? counter; + if (parsed.queryParameters.containsKey("counter")) { + counter = int.parse(parsed.queryParameters["counter"]!); + if (counter <= 0) { + throw UriParseException("Invalid counter value"); + } + } + + return OtpItem( + type, secret, label, digits, period, algorithm, issuer, counter); + } +} diff --git a/totp/lib/parser/uri_parser_base.dart b/totp/lib/parser/uri_parser_base.dart new file mode 100644 index 0000000..7a31a5a --- /dev/null +++ b/totp/lib/parser/uri_parser_base.dart @@ -0,0 +1,11 @@ +import 'package:another_authenticator_totp/totp.dart'; + +abstract interface class UriParserBase { + bool canParse(String uri); + OtpItem parse(String uri); +} + +class UriParseException implements Exception { + String cause; + UriParseException(this.cause); +} diff --git a/totp/lib/totp.dart b/totp/lib/totp.dart index 0fd84bf..de89bc7 100644 --- a/totp/lib/totp.dart +++ b/totp/lib/totp.dart @@ -1,4 +1,4 @@ library another_authenticator_totp; -export 'totp_item.dart'; +export 'models/otp_item.dart'; export 'base32.dart'; diff --git a/totp/lib/totp_item.dart b/totp/lib/totp_item.dart deleted file mode 100644 index 8f4c3e0..0000000 --- a/totp/lib/totp_item.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'base32.dart' show Base32; -import 'totp_algorithm.dart' show OtpHashAlgorithm, Totp; -import 'otp_uri.dart' show OtpUri; - -/// Represents a TOTP item and associated information. -/// -/// Has properties of accountName, issuer, secret, digits, period and algorithm, -/// as well as an id which is randomly assigned on generation. -class TotpItem { - TotpItem(this.secret, - [this.digits = 6, - this.period = 30, - this.algorithm = OtpHashAlgorithm.sha1, - this.issuer = "", - this.accountName = ""]) - : assert(Base32.isBase32(secret) && secret != ''), - assert(digits == 6 || digits == 8), - assert(period > 0); - - /// Account name - final String accountName; - - /// Issuer - final String issuer; - - /// Secret key (in base32) - final String secret; - - /// Number of digits (of code) - final int digits; - - /// Time period - final int period; - - /// Algorithm (sha1/sha256/sha512) - final OtpHashAlgorithm algorithm; - - /// Creates a new TOTP item. - static TotpItem newTotpItem(String secret, - [int digits = 6, - int period = 60, - OtpHashAlgorithm algorithm = OtpHashAlgorithm.sha1, - String issuer = "", - String accountName = ""]) { - return TotpItem(secret, digits, period, algorithm, issuer, accountName); - } - - /// Parses a TOTP key URI and returns a TOTPItem. - static TotpItem fromUri(String uri) { - return OtpUri.fromUri(uri); - } - - /// Generates a formatted TOTP value for the given [time]. - String getPrettyCode(int time) { - return Totp.prettyValue( - Totp.generateCode(time, secret, digits, period, algorithm)); - } - - /// Generates a TOTP value for the given [time]. - String getCode(int time) { - return Totp.generateCode(time, secret, digits, period, algorithm); - } - - /// Returns a placeholder representation of the generated code. - String get placeholder { - if (digits == 8) { - return '···· ····'; - } - return '··· ···'; - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is TotpItem && - accountName == other.accountName && - issuer == other.issuer && - secret == other.secret && - digits == other.digits && - period == other.period && - algorithm == other.algorithm; - - @override - int get hashCode => - accountName.hashCode ^ - issuer.hashCode ^ - period.hashCode ^ - digits.hashCode ^ - algorithm.hashCode ^ - secret.hashCode; - - /// Decode item from JSON. - TotpItem.fromJSON(Map json) - : accountName = json['accountName'], - issuer = json['issuer'], - period = json['period'], - digits = json['digits'], - algorithm = OtpHashAlgorithm.values.byName(json['algorithm']), - secret = json['secret']; - - /// Encode item to JSON. - Map toJSON() => { - 'accountName': accountName, - 'issuer': issuer, - 'secret': secret, - 'digits': digits, - 'period': period, - 'algorithm': algorithm.name - }; -} diff --git a/totp/test/totp_test.dart b/totp/test/totp_test.dart index 7981ff1..2aada25 100644 --- a/totp/test/totp_test.dart +++ b/totp/test/totp_test.dart @@ -1,7 +1,8 @@ +import 'package:another_authenticator_totp/models/otp_algorithm.dart'; import 'package:test/test.dart'; import 'package:another_authenticator_totp/totp.dart'; import 'package:another_authenticator_totp/totp_algorithm.dart'; -import 'package:another_authenticator_totp/otp_uri.dart'; +import 'package:another_authenticator_totp/parser/otp_uri_parser.dart'; void main() { // https://datatracker.ietf.org/doc/html/rfc4648#section-10 @@ -19,7 +20,7 @@ void main() { }); test('TOTP - Key URI 1', () { - var parsed = OtpUri.fromUri( + var parsed = OtpAuthUriParser.parse( "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"); expect(parsed.accountName, "alice@example.com"); expect(parsed.algorithm, OtpHashAlgorithm.sha1); @@ -30,7 +31,7 @@ void main() { }); test('TOTP - Key URI Complete', () { - var parsed = OtpUri.fromUri( + var parsed = OtpAuthUriParser.parse( "otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"); expect(parsed.accountName, "john.doe@email.com"); expect(parsed.algorithm, OtpHashAlgorithm.sha1); @@ -42,7 +43,7 @@ void main() { test('TOTP - Bad Key URI', () { expect( - () => OtpUri.fromUri( + () => OtpAuthUriParser.parse( "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=12"), throwsA(allOf( isFormatException, @@ -52,7 +53,7 @@ void main() { test('TOTP - Bad Algorithm', () { expect( - () => OtpUri.fromUri( + () => OtpAuthUriParser.parse( "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=12&algorithm=sha??"), throwsA(allOf( isFormatException, @@ -90,7 +91,7 @@ void main() { var algorithm = OtpHashAlgorithm.sha256; var issuer = "bar"; var accountName = "foo@bar"; - var item = TotpItem(secret, digits, period, algorithm, issuer, accountName); + var item = OtpItem(secret, digits, period, algorithm, issuer, accountName); expect(item.secret, secret); expect(item.digits, digits); expect(item.period, period); @@ -100,9 +101,9 @@ void main() { }); test('TOTP Item equality', () { - var item1 = TotpItem("A"); - var item2 = TotpItem("A"); - var item3 = TotpItem("B"); + var item1 = OtpItem("A"); + var item2 = OtpItem("A"); + var item3 = OtpItem("B"); // Same object expect(item1 == item1, true); // Same secret (and other fields apart from id), so still equal From 23dd49e52a0e00e6d1e2f1017ffffa5f4758d7c4 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 20:57:58 +1000 Subject: [PATCH 02/10] ref: totp to otp [no ci] --- {totp => otp}/analysis_options.yaml | 0 {totp => otp}/example/totp_example.dart | 2 +- {totp => otp}/lib/models/otp_algorithm.dart | 0 {totp => otp}/lib/models/otp_item.dart | 11 +- {totp => otp}/lib/models/otp_type.dart | 0 otp/lib/otp.dart | 4 + {totp => otp}/lib/otp/base32.dart | 0 .../lib/otp/code_generator_base.dart | 0 {totp => otp}/lib/otp/totp_algorithm.dart | 2 +- .../lib/otp/totp_code_generator.dart | 6 +- {totp => otp}/lib/parser/otp_uri_parser.dart | 0 {totp => otp}/lib/parser/uri_parser_base.dart | 2 +- {totp => otp}/pubspec.lock | 0 {totp => otp}/pubspec.yaml | 4 +- otp/test/otp_test.dart | 122 ++++++++++++++++++ pubspec.yaml | 4 +- .../lib/legacy/legacy_authenticator_item.dart | 2 +- state/lib/legacy/legacy_repository.dart | 2 +- state/pubspec.yaml | 4 +- totp/lib/totp.dart | 4 - totp/test/totp_test.dart | 114 ---------------- 21 files changed, 145 insertions(+), 138 deletions(-) rename {totp => otp}/analysis_options.yaml (100%) rename {totp => otp}/example/totp_example.dart (53%) rename {totp => otp}/lib/models/otp_algorithm.dart (100%) rename {totp => otp}/lib/models/otp_item.dart (92%) rename {totp => otp}/lib/models/otp_type.dart (100%) create mode 100644 otp/lib/otp.dart rename {totp => otp}/lib/otp/base32.dart (100%) rename {totp => otp}/lib/otp/code_generator_base.dart (100%) rename {totp => otp}/lib/otp/totp_algorithm.dart (100%) rename {totp => otp}/lib/otp/totp_code_generator.dart (65%) rename {totp => otp}/lib/parser/otp_uri_parser.dart (100%) rename {totp => otp}/lib/parser/uri_parser_base.dart (78%) rename {totp => otp}/pubspec.lock (100%) rename {totp => otp}/pubspec.yaml (62%) create mode 100644 otp/test/otp_test.dart delete mode 100644 totp/lib/totp.dart delete mode 100644 totp/test/totp_test.dart diff --git a/totp/analysis_options.yaml b/otp/analysis_options.yaml similarity index 100% rename from totp/analysis_options.yaml rename to otp/analysis_options.yaml diff --git a/totp/example/totp_example.dart b/otp/example/totp_example.dart similarity index 53% rename from totp/example/totp_example.dart rename to otp/example/totp_example.dart index 114450c..fe65d39 100644 --- a/totp/example/totp_example.dart +++ b/otp/example/totp_example.dart @@ -1,4 +1,4 @@ -import 'package:another_authenticator_totp/totp_algorithm.dart'; +import 'package:another_authenticator_otp/otp/totp_algorithm.dart'; void main() { print(Totp.generateCode(1542791843, "JBSWY3DPEHPK3PXP")); diff --git a/totp/lib/models/otp_algorithm.dart b/otp/lib/models/otp_algorithm.dart similarity index 100% rename from totp/lib/models/otp_algorithm.dart rename to otp/lib/models/otp_algorithm.dart diff --git a/totp/lib/models/otp_item.dart b/otp/lib/models/otp_item.dart similarity index 92% rename from totp/lib/models/otp_item.dart rename to otp/lib/models/otp_item.dart index c04cbb9..5b5c5ec 100644 --- a/totp/lib/models/otp_item.dart +++ b/otp/lib/models/otp_item.dart @@ -1,9 +1,8 @@ -import 'package:another_authenticator_totp/models/otp_type.dart'; -import 'package:another_authenticator_totp/otp/code_generator_base.dart'; -import 'package:another_authenticator_totp/otp/totp_code_generator.dart'; -import 'package:another_authenticator_totp/parser/uri_parser_base.dart'; - -import './otp_algorithm.dart'; +import 'otp_type.dart'; +import 'otp_algorithm.dart'; +import '../otp/code_generator_base.dart'; +import '../otp/totp_code_generator.dart'; +import '../parser/uri_parser_base.dart'; import '../parser/otp_uri_parser.dart' show OtpAuthUriParser; /// Represents a TOTP item and associated information. diff --git a/totp/lib/models/otp_type.dart b/otp/lib/models/otp_type.dart similarity index 100% rename from totp/lib/models/otp_type.dart rename to otp/lib/models/otp_type.dart diff --git a/otp/lib/otp.dart b/otp/lib/otp.dart new file mode 100644 index 0000000..b553749 --- /dev/null +++ b/otp/lib/otp.dart @@ -0,0 +1,4 @@ +library another_authenticator_otp; + +export 'models/otp_item.dart'; +export 'otp/base32.dart'; diff --git a/totp/lib/otp/base32.dart b/otp/lib/otp/base32.dart similarity index 100% rename from totp/lib/otp/base32.dart rename to otp/lib/otp/base32.dart diff --git a/totp/lib/otp/code_generator_base.dart b/otp/lib/otp/code_generator_base.dart similarity index 100% rename from totp/lib/otp/code_generator_base.dart rename to otp/lib/otp/code_generator_base.dart diff --git a/totp/lib/otp/totp_algorithm.dart b/otp/lib/otp/totp_algorithm.dart similarity index 100% rename from totp/lib/otp/totp_algorithm.dart rename to otp/lib/otp/totp_algorithm.dart index 6310a83..b6249bb 100644 --- a/totp/lib/otp/totp_algorithm.dart +++ b/otp/lib/otp/totp_algorithm.dart @@ -2,8 +2,8 @@ import 'dart:math' show pow; import 'dart:typed_data' show Uint8List, Endian; import 'package:crypto/crypto.dart'; -import '../models/otp_algorithm.dart'; import 'base32.dart' show Base32; +import '../models/otp_algorithm.dart'; /// Static class for generating TOTP codes. /// diff --git a/totp/lib/otp/totp_code_generator.dart b/otp/lib/otp/totp_code_generator.dart similarity index 65% rename from totp/lib/otp/totp_code_generator.dart rename to otp/lib/otp/totp_code_generator.dart index 30ae09a..444517f 100644 --- a/totp/lib/otp/totp_code_generator.dart +++ b/otp/lib/otp/totp_code_generator.dart @@ -1,6 +1,6 @@ -import 'package:another_authenticator_totp/models/otp_algorithm.dart'; -import 'package:another_authenticator_totp/otp/code_generator_base.dart'; -import 'package:another_authenticator_totp/otp/totp_algorithm.dart'; +import '../models/otp_algorithm.dart'; +import '../otp/code_generator_base.dart'; +import '../otp/totp_algorithm.dart'; class TotpCodeGenerator implements OtpCodeGeneratorBase { final String secret; diff --git a/totp/lib/parser/otp_uri_parser.dart b/otp/lib/parser/otp_uri_parser.dart similarity index 100% rename from totp/lib/parser/otp_uri_parser.dart rename to otp/lib/parser/otp_uri_parser.dart diff --git a/totp/lib/parser/uri_parser_base.dart b/otp/lib/parser/uri_parser_base.dart similarity index 78% rename from totp/lib/parser/uri_parser_base.dart rename to otp/lib/parser/uri_parser_base.dart index 7a31a5a..896718c 100644 --- a/totp/lib/parser/uri_parser_base.dart +++ b/otp/lib/parser/uri_parser_base.dart @@ -1,4 +1,4 @@ -import 'package:another_authenticator_totp/totp.dart'; +import '../otp.dart'; abstract interface class UriParserBase { bool canParse(String uri); diff --git a/totp/pubspec.lock b/otp/pubspec.lock similarity index 100% rename from totp/pubspec.lock rename to otp/pubspec.lock diff --git a/totp/pubspec.yaml b/otp/pubspec.yaml similarity index 62% rename from totp/pubspec.yaml rename to otp/pubspec.yaml index 4bdd8fb..02da6af 100644 --- a/totp/pubspec.yaml +++ b/otp/pubspec.yaml @@ -1,5 +1,5 @@ -name: another_authenticator_totp -description: TOTP library used by another_authenticator. +name: another_authenticator_otp +description: OTP library used by another_authenticator. version: 1.0.0 publish_to: none diff --git a/otp/test/otp_test.dart b/otp/test/otp_test.dart new file mode 100644 index 0000000..72b6f96 --- /dev/null +++ b/otp/test/otp_test.dart @@ -0,0 +1,122 @@ +import 'package:another_authenticator_otp/parser/uri_parser_base.dart'; +import 'package:test/test.dart'; +import 'package:another_authenticator_otp/models/otp_algorithm.dart'; +import 'package:another_authenticator_otp/models/otp_type.dart'; +import 'package:another_authenticator_otp/otp.dart'; +import 'package:another_authenticator_otp/otp/totp_algorithm.dart'; +import 'package:another_authenticator_otp/parser/otp_uri_parser.dart'; + +void main() { + // https://datatracker.ietf.org/doc/html/rfc4648#section-10 + // https://stackoverflow.com/a/54263961 + test('Base32', () { + expect(Base32.decode("MY======"), "f".codeUnits); + expect(Base32.decode("MZXQ===="), "fo".codeUnits); + expect(Base32.decode("MZXW6==="), "foo".codeUnits); + expect(Base32.decode("MZXW6YQ="), "foob".codeUnits); + expect(Base32.decode("MZXW6YTB"), "fooba".codeUnits); + expect(Base32.decode("MZXW6YTBOI======"), "foobar".codeUnits); + expect(Base32.isBase32("MZXW6YTBOI======"), true); + expect(Base32.decode("AE======"), "\x01".codeUnits); + expect(Base32.decode("AA======"), "\x00".codeUnits); + }); + + group('TOTP', () { + var otpUriParser = OtpAuthUriParser(); + + test('Key URI 1', () { + var parsed = otpUriParser.parse( + "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"); + expect(parsed.type, OtpType.totp); + expect(parsed.label, "Example:alice@example.com"); + expect(parsed.algorithm, null); + expect(parsed.digits, null); + expect(parsed.issuer, "Example"); + expect(parsed.period, null); + expect(parsed.secret, "JBSWY3DPEHPK3PXP"); + }); + + test('Key URI Complete', () { + var parsed = otpUriParser.parse( + "otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"); + expect(parsed.type, OtpType.totp); + expect(parsed.label, "ACME Co:john.doe@email.com"); + expect(parsed.algorithm, OtpHashAlgorithm.sha1); + expect(parsed.digits, 6); + expect(parsed.issuer, "ACME Co"); + expect(parsed.period, 30); + expect(parsed.secret, "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"); + }); + + test('Bad Number of Digits', () { + expect( + () => otpUriParser.parse( + "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=12"), + throwsA(predicate((e) => + e is UriParseException && + e.cause == "Unsupported number of digits"))); + }); + + test('Bad Algorithm', () { + expect( + () => otpUriParser.parse( + "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=12&algorithm=sha??"), + throwsA(predicate((e) => + e is UriParseException && e.cause == "Invalid algorithm"))); + }); + + test('1542791843', () { + expect(Totp.generateCode(1542791843, "JBSWY3DPEHPK3PXP"), "092264"); + }); + + test('59', () { + expect(Totp.generateCode(59, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8), + "94287082"); + }); + + test('20000000000', () { + expect( + Totp.generateCode(20000000000, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8), + "65353130"); + }); + + test('Multiple', () { + expect( + Totp.generateCodes( + [59, 20000000000], "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8), + ["94287082", "65353130"]); + }); + + test('Item attributes', () { + var secret = "A"; + var digits = 8; + var period = 60; + var algorithm = OtpHashAlgorithm.sha256; + var issuer = "bar"; + var label = "Example:alice@gmail.com"; + + var item = OtpItem( + OtpType.totp, secret, label, digits, period, algorithm, issuer); + + expect(item.type, OtpType.totp); + expect(item.secret, secret); + expect(item.label, label); + expect(item.digits, digits); + expect(item.period, period); + expect(item.algorithm, algorithm); + expect(item.issuer, issuer); + }); + + test('TOTP Item equality', () { + var item1 = OtpItem(OtpType.totp, "A", "Example:alice@gmail.com"); + var item2 = OtpItem(OtpType.totp, "A", "Example:alice@gmail.com"); + var item3 = OtpItem(OtpType.totp, "B", "Example:alice@gmail.com"); + // Same object + expect(item1 == item1, true); + // Same secret (and other fields apart from id), so still equal + expect(item1 == item2, true); + // Secret changed + expect(item1 == item3, false); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 5c62f39..865d3bd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,8 +22,8 @@ dependencies: package_info_plus: ^8.0.2 # Internal - another_authenticator_totp: - path: ./totp + another_authenticator_otp: + path: ./otp another_authenticator_state: path: ./state diff --git a/state/lib/legacy/legacy_authenticator_item.dart b/state/lib/legacy/legacy_authenticator_item.dart index 1567732..97396a0 100644 --- a/state/lib/legacy/legacy_authenticator_item.dart +++ b/state/lib/legacy/legacy_authenticator_item.dart @@ -1,4 +1,4 @@ -import 'package:another_authenticator_totp/totp.dart'; +import 'package:another_authenticator_otp/otp.dart'; import 'package:uuid/uuid.dart'; class LegacyAuthenticatorItem { diff --git a/state/lib/legacy/legacy_repository.dart b/state/lib/legacy/legacy_repository.dart index 5376afc..4635fa4 100644 --- a/state/lib/legacy/legacy_repository.dart +++ b/state/lib/legacy/legacy_repository.dart @@ -1,7 +1,7 @@ import 'dart:async' show Future; import 'dart:convert' show json; import 'package:another_authenticator_state/file_storage_base.dart'; -import 'package:another_authenticator_totp/totp.dart'; +import 'package:another_authenticator_otp/otp.dart'; import './legacy_authenticator_item.dart'; import '../repository/repository_base.dart' show RepositoryBase; diff --git a/state/pubspec.yaml b/state/pubspec.yaml index 21008fd..10009ad 100644 --- a/state/pubspec.yaml +++ b/state/pubspec.yaml @@ -10,8 +10,8 @@ dependencies: uuid: 4.4.0 # Internal - another_authenticator_totp: - path: ../totp + another_authenticator_otp: + path: ../otp sqflite: ^2.3.3 diff --git a/totp/lib/totp.dart b/totp/lib/totp.dart deleted file mode 100644 index de89bc7..0000000 --- a/totp/lib/totp.dart +++ /dev/null @@ -1,4 +0,0 @@ -library another_authenticator_totp; - -export 'models/otp_item.dart'; -export 'base32.dart'; diff --git a/totp/test/totp_test.dart b/totp/test/totp_test.dart deleted file mode 100644 index 2aada25..0000000 --- a/totp/test/totp_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'package:another_authenticator_totp/models/otp_algorithm.dart'; -import 'package:test/test.dart'; -import 'package:another_authenticator_totp/totp.dart'; -import 'package:another_authenticator_totp/totp_algorithm.dart'; -import 'package:another_authenticator_totp/parser/otp_uri_parser.dart'; - -void main() { - // https://datatracker.ietf.org/doc/html/rfc4648#section-10 - // https://stackoverflow.com/a/54263961 - test('Base32', () { - expect(Base32.decode("MY======"), "f".codeUnits); - expect(Base32.decode("MZXQ===="), "fo".codeUnits); - expect(Base32.decode("MZXW6==="), "foo".codeUnits); - expect(Base32.decode("MZXW6YQ="), "foob".codeUnits); - expect(Base32.decode("MZXW6YTB"), "fooba".codeUnits); - expect(Base32.decode("MZXW6YTBOI======"), "foobar".codeUnits); - expect(Base32.isBase32("MZXW6YTBOI======"), true); - expect(Base32.decode("AE======"), "\x01".codeUnits); - expect(Base32.decode("AA======"), "\x00".codeUnits); - }); - - test('TOTP - Key URI 1', () { - var parsed = OtpAuthUriParser.parse( - "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example"); - expect(parsed.accountName, "alice@example.com"); - expect(parsed.algorithm, OtpHashAlgorithm.sha1); - expect(parsed.digits, 6); - expect(parsed.issuer, "Example"); - expect(parsed.period, 30); - expect(parsed.secret, "JBSWY3DPEHPK3PXP"); - }); - - test('TOTP - Key URI Complete', () { - var parsed = OtpAuthUriParser.parse( - "otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"); - expect(parsed.accountName, "john.doe@email.com"); - expect(parsed.algorithm, OtpHashAlgorithm.sha1); - expect(parsed.digits, 6); - expect(parsed.issuer, "ACME Co"); - expect(parsed.period, 30); - expect(parsed.secret, "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"); - }); - - test('TOTP - Bad Key URI', () { - expect( - () => OtpAuthUriParser.parse( - "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=12"), - throwsA(allOf( - isFormatException, - predicate((e) => - e is FormatException && e.message == "Incorrect parameters")))); - }); - - test('TOTP - Bad Algorithm', () { - expect( - () => OtpAuthUriParser.parse( - "otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&digits=12&algorithm=sha??"), - throwsA(allOf( - isFormatException, - predicate((e) => - e is FormatException && - e.message == "Unrecognised algorithm")))); - }); - - test('TOTP - 1542791843', () { - expect(Totp.generateCode(1542791843, "JBSWY3DPEHPK3PXP"), "092264"); - }); - - test('TOTP - 59', () { - expect(Totp.generateCode(59, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8), - "94287082"); - }); - - test('TOTP - 20000000000', () { - expect( - Totp.generateCode(20000000000, "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8), - "65353130"); - }); - - test('TOTP - Multiple', () { - expect( - Totp.generateCodes( - [59, 20000000000], "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", 8), - ["94287082", "65353130"]); - }); - - test('TOTP Item attributes', () { - var secret = "A"; - var digits = 8; - var period = 60; - var algorithm = OtpHashAlgorithm.sha256; - var issuer = "bar"; - var accountName = "foo@bar"; - var item = OtpItem(secret, digits, period, algorithm, issuer, accountName); - expect(item.secret, secret); - expect(item.digits, digits); - expect(item.period, period); - expect(item.algorithm, algorithm); - expect(item.issuer, issuer); - expect(item.accountName, accountName); - }); - - test('TOTP Item equality', () { - var item1 = OtpItem("A"); - var item2 = OtpItem("A"); - var item3 = OtpItem("B"); - // Same object - expect(item1 == item1, true); - // Same secret (and other fields apart from id), so still equal - expect(item1 == item2, true); - // Secret changed - expect(item1 == item3, false); - }); -} From a6ef73dd48c870fbb09639fd944787436567635c Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 21:14:25 +1000 Subject: [PATCH 03/10] fix: more items --- lib/pages/add.dart | 4 ++-- lib/pages/android/list_item.dart | 2 +- lib/state/app_state.dart | 2 +- otp/lib/models/otp_item.dart | 18 +++++++++++------- otp/lib/otp/code_generator_base.dart | 1 + otp/lib/otp/totp_code_generator.dart | 5 +++++ pubspec.lock | 8 ++++---- state/lib/repository/repository.dart | 2 +- state/lib/repository/repository_base.dart | 2 +- state/pubspec.lock | 4 ++-- state/test/legacy_repository_test.dart | 7 ++++--- 11 files changed, 33 insertions(+), 22 deletions(-) diff --git a/lib/pages/add.dart b/lib/pages/add.dart index d6bd0c8..9bab6ba 100644 --- a/lib/pages/add.dart +++ b/lib/pages/add.dart @@ -1,8 +1,8 @@ import 'package:another_authenticator/state/app_state.dart'; import 'package:another_authenticator/ui/adaptive.dart' show AdaptiveDialogAction, AppScaffold, isPlatformAndroid; -import 'package:another_authenticator_totp/models/otp_algorithm.dart'; -import 'package:another_authenticator_totp/totp.dart' show Base32, OtpItem; +import 'package:another_authenticator_otp/models/otp_algorithm.dart'; +import 'package:another_authenticator_otp/otp.dart' show Base32, OtpItem; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/lib/pages/android/list_item.dart b/lib/pages/android/list_item.dart index aa54eb2..3176f11 100644 --- a/lib/pages/android/list_item.dart +++ b/lib/pages/android/list_item.dart @@ -42,7 +42,7 @@ class _TOTPListItemState extends State // Define animation // Adapted from progress_indicator_demo.dart from flutter examples _controller = AnimationController( - duration: Duration(seconds: widget.item.totp.period), + duration: Duration(seconds: widget.item.totp.getPeriod()), lowerBound: 0.0, upperBound: 1.0, vsync: this, diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index f0aaed8..50ecc5e 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -1,5 +1,5 @@ import 'package:another_authenticator_state/state.dart'; -import 'package:another_authenticator_totp/totp.dart'; +import 'package:another_authenticator_otp/otp.dart'; import 'package:flutter/widgets.dart'; import 'package:collection/collection.dart' show ListEquality; diff --git a/otp/lib/models/otp_item.dart b/otp/lib/models/otp_item.dart index 5b5c5ec..a64bf08 100644 --- a/otp/lib/models/otp_item.dart +++ b/otp/lib/models/otp_item.dart @@ -5,14 +5,10 @@ import '../otp/totp_code_generator.dart'; import '../parser/uri_parser_base.dart'; import '../parser/otp_uri_parser.dart' show OtpAuthUriParser; -/// Represents a TOTP item and associated information. -/// -/// Has properties of accountName, issuer, secret, digits, period and algorithm, -/// as well as an id which is randomly assigned on generation. +/// Represents an OTP item and associated information. class OtpItem { OtpItem(this.type, this.secret, this.label, [this.digits, this.period, this.algorithm, this.issuer, this.counter]); - // TODO: Checks on construction? /// Type of item (Currently only supports TOTP) final OtpType type; @@ -35,7 +31,7 @@ class OtpItem { /// Time period final int? period; - /// Counter + /// Counter (HOTP only) final int? counter; // /// Creates a new TOTP item. @@ -71,6 +67,12 @@ class OtpItem { return gen.generateCode(time); } + int getPeriod() { + OtpCodeGeneratorBase gen = + TotpCodeGenerator(secret, digits, period, algorithm); + return gen.getPeriod(); + } + /// Returns a placeholder representation of the generated code. String get placeholder { if (digits == 8) { @@ -96,7 +98,9 @@ class OtpItem { label = json.containsKey('label') ? json['label'] : null, digits = json['digits'], period = json['period'], - algorithm = OtpHashAlgorithm.fromString(json['algorithm']), + algorithm = json['algorithm'] == null + ? null + : OtpHashAlgorithm.fromString(json['algorithm']), issuer = json['issuer'], counter = json.containsKey('counter') ? json['counter'] : null; diff --git a/otp/lib/otp/code_generator_base.dart b/otp/lib/otp/code_generator_base.dart index a533ad5..7d2fc78 100644 --- a/otp/lib/otp/code_generator_base.dart +++ b/otp/lib/otp/code_generator_base.dart @@ -1,3 +1,4 @@ abstract interface class OtpCodeGeneratorBase { String generateCode(int time); + int getPeriod(); } diff --git a/otp/lib/otp/totp_code_generator.dart b/otp/lib/otp/totp_code_generator.dart index 444517f..844ebf4 100644 --- a/otp/lib/otp/totp_code_generator.dart +++ b/otp/lib/otp/totp_code_generator.dart @@ -15,4 +15,9 @@ class TotpCodeGenerator implements OtpCodeGeneratorBase { return Totp.generateCode(time, secret, digits ?? 6, period ?? 30, algorithm ?? OtpHashAlgorithm.sha1); } + + @override + int getPeriod() { + return period ?? 30; + } } diff --git a/pubspec.lock b/pubspec.lock index bb9d0b3..1902a32 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,17 +1,17 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - another_authenticator_state: + another_authenticator_otp: dependency: "direct main" description: - path: state + path: otp relative: true source: path version: "1.0.0" - another_authenticator_totp: + another_authenticator_state: dependency: "direct main" description: - path: totp + path: state relative: true source: path version: "1.0.0" diff --git a/state/lib/repository/repository.dart b/state/lib/repository/repository.dart index 312ddff..79573f2 100644 --- a/state/lib/repository/repository.dart +++ b/state/lib/repository/repository.dart @@ -1,6 +1,6 @@ import 'dart:async' show Future; -import 'package:another_authenticator_totp/totp.dart'; +import 'package:another_authenticator_otp/otp.dart'; import '../file_storage_base.dart'; import '../legacy/legacy_authenticator_item.dart'; diff --git a/state/lib/repository/repository_base.dart b/state/lib/repository/repository_base.dart index 5e6d6ec..26390f9 100644 --- a/state/lib/repository/repository_base.dart +++ b/state/lib/repository/repository_base.dart @@ -1,6 +1,6 @@ import 'dart:async' show Future; -import 'package:another_authenticator_totp/totp.dart'; +import 'package:another_authenticator_otp/otp.dart'; /// Abstract class for loading/saving state from storage. abstract class RepositoryBase { diff --git a/state/pubspec.lock b/state/pubspec.lock index 4770a27..bf7da5c 100644 --- a/state/pubspec.lock +++ b/state/pubspec.lock @@ -22,10 +22,10 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.0" - another_authenticator_totp: + another_authenticator_otp: dependency: "direct main" description: - path: "../totp" + path: "../otp" relative: true source: path version: "1.0.0" diff --git a/state/test/legacy_repository_test.dart b/state/test/legacy_repository_test.dart index 7142ca2..4c952b6 100644 --- a/state/test/legacy_repository_test.dart +++ b/state/test/legacy_repository_test.dart @@ -1,8 +1,9 @@ +import 'package:test/test.dart'; + +import 'package:another_authenticator_otp/models/otp_type.dart'; +import 'package:another_authenticator_otp/otp.dart'; import 'package:another_authenticator_state/legacy/legacy_repository.dart'; import 'package:another_authenticator_state/state.dart'; -import 'package:another_authenticator_totp/models/otp_type.dart'; -import 'package:another_authenticator_totp/totp.dart'; -import 'package:test/test.dart'; import 'test_file_storage.dart'; From c4a60d8494e853757fe1aae8203ce1a982f7c85f Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 21:15:07 +1000 Subject: [PATCH 04/10] fix: period --- lib/pages/cupertino/list_item.dart | 2 +- lib/pages/shared/list_item_base.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/cupertino/list_item.dart b/lib/pages/cupertino/list_item.dart index 49dab9e..8e1fe26 100644 --- a/lib/pages/cupertino/list_item.dart +++ b/lib/pages/cupertino/list_item.dart @@ -46,7 +46,7 @@ class _TOTPListItemState extends State // Define animation // Adapted from progress_indicator_demo.dart from flutter examples _controller = AnimationController( - duration: Duration(seconds: widget.item.totp.period), + duration: Duration(seconds: widget.item.totp.getPeriod()), lowerBound: 0.0, upperBound: 1.0, vsync: this, diff --git a/lib/pages/shared/list_item_base.dart b/lib/pages/shared/list_item_base.dart index db6edc0..0f69361 100644 --- a/lib/pages/shared/list_item_base.dart +++ b/lib/pages/shared/list_item_base.dart @@ -17,7 +17,7 @@ abstract class TotpListItemBase extends StatefulWidget { /// Progress indicator value. double get indicatorValue { - return (_secondsSinceEpoch % item.totp.period) / item.totp.period; + return (_secondsSinceEpoch % item.totp.getPeriod()) / item.totp.getPeriod(); } /// Returns the code of the item for the current time. From 56d0bead037c7d5557dcb3ae7105d9741cc9c781 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 21:33:21 +1000 Subject: [PATCH 05/10] ref: rename JSON to Json --- otp/lib/models/otp_item.dart | 6 ++++-- state/lib/legacy/legacy_authenticator_item.dart | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/otp/lib/models/otp_item.dart b/otp/lib/models/otp_item.dart index a64bf08..50cad40 100644 --- a/otp/lib/models/otp_item.dart +++ b/otp/lib/models/otp_item.dart @@ -67,6 +67,8 @@ class OtpItem { return gen.generateCode(time); } + /// Get period that code is valid for. + /// Currently, HOTP is not supported, so type is yet optional. int getPeriod() { OtpCodeGeneratorBase gen = TotpCodeGenerator(secret, digits, period, algorithm); @@ -90,7 +92,7 @@ class OtpItem { } /// Decode item from JSON. - OtpItem.fromJSON(Map json) + OtpItem.fromJson(Map json) : type = json.containsKey('type') ? OtpType.fromString(json['type']) : OtpType.totp, @@ -114,7 +116,7 @@ class OtpItem { // secret = json['secret']; /// Encode item to JSON. - Map toJSON() => { + Map toJson() => { 'type': type.name, 'secret': secret, 'label': label, diff --git a/state/lib/legacy/legacy_authenticator_item.dart b/state/lib/legacy/legacy_authenticator_item.dart index 97396a0..0584b86 100644 --- a/state/lib/legacy/legacy_authenticator_item.dart +++ b/state/lib/legacy/legacy_authenticator_item.dart @@ -33,11 +33,11 @@ class LegacyAuthenticatorItem { /// Legacy decode. LegacyAuthenticatorItem.fromMap(Map json) : id = json['id'], - totp = OtpItem.fromJSON(json); + totp = OtpItem.fromJson(json); /// Legacy encode. Map toMap() { - return {'id': id, ...totp.toJSON()}; + return {'id': id, ...totp.toJson()}; } @override From 467accaee564c4149f3d77d52812becff30f56fe Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 22:47:28 +1000 Subject: [PATCH 06/10] fix: rest of errors --- lib/pages/add.dart | 6 ++- lib/pages/android/edit_list_item.dart | 4 +- lib/pages/android/list_item.dart | 4 +- lib/pages/cupertino/edit_list_item.dart | 4 +- lib/pages/cupertino/list_item.dart | 4 +- otp/lib/models/otp_item.dart | 52 ++++++++++++++++++++++--- otp/lib/parser/otp_uri_parser.dart | 2 +- 7 files changed, 59 insertions(+), 17 deletions(-) diff --git a/lib/pages/add.dart b/lib/pages/add.dart index 9bab6ba..213ef82 100644 --- a/lib/pages/add.dart +++ b/lib/pages/add.dart @@ -2,6 +2,7 @@ import 'package:another_authenticator/state/app_state.dart'; import 'package:another_authenticator/ui/adaptive.dart' show AdaptiveDialogAction, AppScaffold, isPlatformAndroid; import 'package:another_authenticator_otp/models/otp_algorithm.dart'; +import 'package:another_authenticator_otp/models/otp_type.dart'; import 'package:another_authenticator_otp/otp.dart' show Base32, OtpItem; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; @@ -119,12 +120,13 @@ class _AddPageState extends State { // Initialise and add TOTP item var item = OtpItem( + OtpType.totp, _secretController.text, + "${_issuerController.text}:${_accountNameController.text}", _digits, _period, OtpHashAlgorithm.sha1, - _issuerController.text, - _accountNameController.text); + _issuerController.text); addItem(item); } diff --git a/lib/pages/android/edit_list_item.dart b/lib/pages/android/edit_list_item.dart index 3eb9ae5..9350fad 100644 --- a/lib/pages/android/edit_list_item.dart +++ b/lib/pages/android/edit_list_item.dart @@ -53,7 +53,7 @@ class _EditListItem extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Issuer - Text(widget.item.totp.issuer, + Text(widget.item.totp.getIssuer() ?? "", style: Theme.of(context).textTheme.titleMedium), // Generated code Padding( @@ -61,7 +61,7 @@ class _EditListItem extends State { child: Text(widget.item.totp.placeholder, style: Theme.of(context).textTheme.displaySmall)), // Account name - Text(widget.item.totp.accountName, + Text(widget.item.totp.getAccountName(), style: Theme.of(context).textTheme.bodyMedium) ], ), diff --git a/lib/pages/android/list_item.dart b/lib/pages/android/list_item.dart index 3176f11..7410a2c 100644 --- a/lib/pages/android/list_item.dart +++ b/lib/pages/android/list_item.dart @@ -88,7 +88,7 @@ class _TOTPListItemState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ // Issuer - Text(widget.item.totp.issuer, + Text(widget.item.totp.getIssuer() ?? "", style: Theme.of(context).textTheme.titleMedium), // Generated code Padding( @@ -96,7 +96,7 @@ class _TOTPListItemState extends State child: Text(_code, style: Theme.of(context).textTheme.displaySmall)), // Account name - Text(widget.item.totp.accountName, + Text(widget.item.totp.getAccountName(), style: Theme.of(context).textTheme.bodyMedium) ], ), diff --git a/lib/pages/cupertino/edit_list_item.dart b/lib/pages/cupertino/edit_list_item.dart index db84365..103e2a0 100644 --- a/lib/pages/cupertino/edit_list_item.dart +++ b/lib/pages/cupertino/edit_list_item.dart @@ -58,7 +58,7 @@ class _EditListItem extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Issuer - Text(widget.item.totp.issuer), + Text(widget.item.totp.getIssuer() ?? ""), // Generated code Padding( padding: const EdgeInsets.symmetric(vertical: 10), @@ -67,7 +67,7 @@ class _EditListItem extends State { fontSize: 40, color: Color.fromARGB(255, 125, 125, 125)))), // Account name - Text(widget.item.totp.accountName, + Text(widget.item.totp.getAccountName(), style: const TextStyle(fontSize: 13)) ], ), diff --git a/lib/pages/cupertino/list_item.dart b/lib/pages/cupertino/list_item.dart index 8e1fe26..5a82d5a 100644 --- a/lib/pages/cupertino/list_item.dart +++ b/lib/pages/cupertino/list_item.dart @@ -87,7 +87,7 @@ class _TOTPListItemState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ // Issuer - Text(widget.item.totp.issuer), + Text(widget.item.totp.getIssuer() ?? ""), // Generated code Padding( padding: const EdgeInsets.symmetric(vertical: 10), @@ -96,7 +96,7 @@ class _TOTPListItemState extends State fontSize: 40, color: Color.fromARGB(255, 125, 125, 125)))), // Account name - Text(widget.item.totp.accountName, + Text(widget.item.totp.getAccountName(), style: const TextStyle(fontSize: 13)) ], ), diff --git a/otp/lib/models/otp_item.dart b/otp/lib/models/otp_item.dart index 50cad40..f6fe0cb 100644 --- a/otp/lib/models/otp_item.dart +++ b/otp/lib/models/otp_item.dart @@ -8,13 +8,18 @@ import '../parser/otp_uri_parser.dart' show OtpAuthUriParser; /// Represents an OTP item and associated information. class OtpItem { OtpItem(this.type, this.secret, this.label, - [this.digits, this.period, this.algorithm, this.issuer, this.counter]); + [this.digits, + this.period, + this.algorithm, + this.issuer, + this.counter, + this.originalUri]); /// Type of item (Currently only supports TOTP) final OtpType type; /// Label - final String? label; + final String label; /// Secret key (in base32) final String secret; @@ -34,6 +39,9 @@ class OtpItem { /// Counter (HOTP only) final int? counter; + /// URI in original form (if scanned/imported) + final String? originalUri; + // /// Creates a new TOTP item. // static OtpItem newTotpItem(String secret, // [int digits = 6, @@ -75,6 +83,30 @@ class OtpItem { return gen.getPeriod(); } + /// Get Issuer. + String? getIssuer() { + // Use the issuer parameter + if (issuer != null) { + return issuer!; + } + + // Extract it out of the label + // label = accountname / issuer (“:” / “%3A”) *”%20” accountname + if (label.contains(':')) { + return label.split(':')[0]; + } + + return null; + } + + /// Get account name + String getAccountName() { + if (!label.contains(':')) { + return label; + } + return label.split(':')[1]; + } + /// Returns a placeholder representation of the generated code. String get placeholder { if (digits == 8) { @@ -97,16 +129,23 @@ class OtpItem { ? OtpType.fromString(json['type']) : OtpType.totp, secret = json['secret'], - label = json.containsKey('label') ? json['label'] : null, + label = json.containsKey('label') + ? json['label'] + : json.containsKey('accountName') && json['accountName'] != '' + ? json.containsKey('issuer') && json['issuer'] != '' + ? "${json['issuer']}:${json['accountName']}" + : json['accountName'] + : "", digits = json['digits'], period = json['period'], algorithm = json['algorithm'] == null ? null : OtpHashAlgorithm.fromString(json['algorithm']), issuer = json['issuer'], - counter = json.containsKey('counter') ? json['counter'] : null; + counter = json.containsKey('counter') ? json['counter'] : null, + originalUri = json.containsKey('uri') ? json['uri'] : null; - // TODO: Handle accountName + // The original version // OtpItem.fromJSON(Map json) // : accountName = json['accountName'], // issuer = json['issuer'], @@ -124,7 +163,8 @@ class OtpItem { 'period': period, 'algorithm': algorithm?.name, 'issuer': issuer, - 'counter': counter + 'counter': counter, + 'uri': originalUri }; @override diff --git a/otp/lib/parser/otp_uri_parser.dart b/otp/lib/parser/otp_uri_parser.dart index f520f17..0bd3d8a 100644 --- a/otp/lib/parser/otp_uri_parser.dart +++ b/otp/lib/parser/otp_uri_parser.dart @@ -89,6 +89,6 @@ class OtpAuthUriParser implements UriParserBase { } return OtpItem( - type, secret, label, digits, period, algorithm, issuer, counter); + type, secret, label, digits, period, algorithm, issuer, counter, uri); } } From 82c18ce2b1f49d6f0a419db828fbaed82e3804fa Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 23:08:02 +1000 Subject: [PATCH 07/10] ref: remove commented code --- otp/lib/models/otp_item.dart | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/otp/lib/models/otp_item.dart b/otp/lib/models/otp_item.dart index f6fe0cb..45d22e7 100644 --- a/otp/lib/models/otp_item.dart +++ b/otp/lib/models/otp_item.dart @@ -42,16 +42,6 @@ class OtpItem { /// URI in original form (if scanned/imported) final String? originalUri; - // /// Creates a new TOTP item. - // static OtpItem newTotpItem(String secret, - // [int digits = 6, - // int period = 60, - // OtpHashAlgorithm algorithm = OtpHashAlgorithm.sha1, - // String issuer = "", - // String accountName = ""]) { - // return OtpItem(secret, digits, period, algorithm, issuer, accountName); - // } - /// Parses a TOTP key URI and returns a TOTPItem. static final List _parsers = [OtpAuthUriParser()]; static OtpItem fromUri(String uri) { @@ -145,15 +135,6 @@ class OtpItem { counter = json.containsKey('counter') ? json['counter'] : null, originalUri = json.containsKey('uri') ? json['uri'] : null; - // The original version - // OtpItem.fromJSON(Map json) - // : accountName = json['accountName'], - // issuer = json['issuer'], - // period = json['period'], - // digits = json['digits'], - // algorithm = OtpHashAlgorithm.values.byName(json['algorithm']), - // secret = json['secret']; - /// Encode item to JSON. Map toJson() => { 'type': type.name, From 636cf56a6ac2400eed3d63efb18b558410c75a2f Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sat, 28 Sep 2024 23:19:19 +1000 Subject: [PATCH 08/10] fix: import scope --- lib/pages/add.dart | 5 ++--- otp/lib/otp.dart | 2 ++ state/test/legacy_repository_test.dart | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/add.dart b/lib/pages/add.dart index 213ef82..79a119b 100644 --- a/lib/pages/add.dart +++ b/lib/pages/add.dart @@ -1,9 +1,8 @@ import 'package:another_authenticator/state/app_state.dart'; import 'package:another_authenticator/ui/adaptive.dart' show AdaptiveDialogAction, AppScaffold, isPlatformAndroid; -import 'package:another_authenticator_otp/models/otp_algorithm.dart'; -import 'package:another_authenticator_otp/models/otp_type.dart'; -import 'package:another_authenticator_otp/otp.dart' show Base32, OtpItem; +import 'package:another_authenticator_otp/otp.dart' + show Base32, OtpHashAlgorithm, OtpItem, OtpType; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; diff --git a/otp/lib/otp.dart b/otp/lib/otp.dart index b553749..29829ab 100644 --- a/otp/lib/otp.dart +++ b/otp/lib/otp.dart @@ -1,4 +1,6 @@ library another_authenticator_otp; export 'models/otp_item.dart'; +export 'models/otp_type.dart'; +export 'models/otp_algorithm.dart'; export 'otp/base32.dart'; diff --git a/state/test/legacy_repository_test.dart b/state/test/legacy_repository_test.dart index 4c952b6..5a88968 100644 --- a/state/test/legacy_repository_test.dart +++ b/state/test/legacy_repository_test.dart @@ -1,6 +1,5 @@ import 'package:test/test.dart'; -import 'package:another_authenticator_otp/models/otp_type.dart'; import 'package:another_authenticator_otp/otp.dart'; import 'package:another_authenticator_state/legacy/legacy_repository.dart'; import 'package:another_authenticator_state/state.dart'; From 58cb570a37e0b65425994b1ac17b82b91a7bba39 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sun, 29 Sep 2024 14:22:06 +1000 Subject: [PATCH 09/10] ref: separate generator code from otp --- .github/workflows/build.yml | 2 +- lib/pages/add.dart | 1 - lib/state/app_state.dart | 8 ++++---- otp/lib/{otp => generator}/code_generator_base.dart | 0 otp/lib/{otp => generator}/totp_code_generator.dart | 12 ++++++++---- otp/lib/models/otp_item.dart | 6 +++--- otp/lib/parser/otp_uri_parser.dart | 4 ++-- otp/lib/parser/uri_parser_base.dart | 2 +- 8 files changed, 19 insertions(+), 16 deletions(-) rename otp/lib/{otp => generator}/code_generator_base.dart (100%) rename otp/lib/{otp => generator}/totp_code_generator.dart (54%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f5a814..57875f2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: channel: 'stable' - run: dart test - working-directory: ./totp + working-directory: ./otp - run: dart test working-directory: ./state diff --git a/lib/pages/add.dart b/lib/pages/add.dart index 79a119b..1fe581c 100644 --- a/lib/pages/add.dart +++ b/lib/pages/add.dart @@ -117,7 +117,6 @@ class _AddPageState extends State { return; } - // Initialise and add TOTP item var item = OtpItem( OtpType.totp, _secretController.text, diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index 50ecc5e..366e4ea 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -12,10 +12,10 @@ class AppState extends ChangeNotifier { AppState(this._repository); - /// List of TOTP items (internal implementation). + /// List of items (internal implementation). List? _items; - /// TOTP items as list. + /// Items as list. List? get items { if (_items == null) { loadItems(); @@ -28,13 +28,13 @@ class AppState extends ChangeNotifier { notifyListeners(); } - /// Adds a TOTP item to the list. + /// Adds an item to the list. Future addItem(OtpItem item) async { await _repository.addItem(item); await loadItems(); } - /// Replace list of TOTP items. + /// Replace list of items. Future replaceItems(List items) async { await _repository.replaceItems(items); await loadItems(); diff --git a/otp/lib/otp/code_generator_base.dart b/otp/lib/generator/code_generator_base.dart similarity index 100% rename from otp/lib/otp/code_generator_base.dart rename to otp/lib/generator/code_generator_base.dart diff --git a/otp/lib/otp/totp_code_generator.dart b/otp/lib/generator/totp_code_generator.dart similarity index 54% rename from otp/lib/otp/totp_code_generator.dart rename to otp/lib/generator/totp_code_generator.dart index 844ebf4..b2e9f3d 100644 --- a/otp/lib/otp/totp_code_generator.dart +++ b/otp/lib/generator/totp_code_generator.dart @@ -1,5 +1,5 @@ +import 'code_generator_base.dart'; import '../models/otp_algorithm.dart'; -import '../otp/code_generator_base.dart'; import '../otp/totp_algorithm.dart'; class TotpCodeGenerator implements OtpCodeGeneratorBase { @@ -8,16 +8,20 @@ class TotpCodeGenerator implements OtpCodeGeneratorBase { final int? period; final OtpHashAlgorithm? algorithm; + static const _defaultDigits = 6; + static const _defaultAlgorithm = OtpHashAlgorithm.sha1; + static const _defaultPeriod = 30; + TotpCodeGenerator(this.secret, [this.digits, this.period, this.algorithm]); @override String generateCode(int time) { - return Totp.generateCode(time, secret, digits ?? 6, period ?? 30, - algorithm ?? OtpHashAlgorithm.sha1); + return Totp.generateCode(time, secret, digits ?? _defaultDigits, + period ?? _defaultPeriod, algorithm ?? _defaultAlgorithm); } @override int getPeriod() { - return period ?? 30; + return period ?? _defaultPeriod; } } diff --git a/otp/lib/models/otp_item.dart b/otp/lib/models/otp_item.dart index 45d22e7..cdc7abc 100644 --- a/otp/lib/models/otp_item.dart +++ b/otp/lib/models/otp_item.dart @@ -1,9 +1,9 @@ import 'otp_type.dart'; import 'otp_algorithm.dart'; -import '../otp/code_generator_base.dart'; -import '../otp/totp_code_generator.dart'; +import '../generator/code_generator_base.dart'; +import '../generator/totp_code_generator.dart'; import '../parser/uri_parser_base.dart'; -import '../parser/otp_uri_parser.dart' show OtpAuthUriParser; +import '../parser/otp_uri_parser.dart'; /// Represents an OTP item and associated information. class OtpItem { diff --git a/otp/lib/parser/otp_uri_parser.dart b/otp/lib/parser/otp_uri_parser.dart index 0bd3d8a..245a2a8 100644 --- a/otp/lib/parser/otp_uri_parser.dart +++ b/otp/lib/parser/otp_uri_parser.dart @@ -1,9 +1,9 @@ -import "./uri_parser_base.dart"; +import "uri_parser_base.dart"; import '../models/otp_type.dart'; import '../models/otp_algorithm.dart'; import '../models/otp_item.dart'; -/// Parses TOTP key URI into TOTPItems. +/// Parses OTP URI into OtpItem. /// /// Reference: /// * https://github.com/google/google-authenticator/wiki/Key-Uri-Format diff --git a/otp/lib/parser/uri_parser_base.dart b/otp/lib/parser/uri_parser_base.dart index 896718c..7f5a06b 100644 --- a/otp/lib/parser/uri_parser_base.dart +++ b/otp/lib/parser/uri_parser_base.dart @@ -1,4 +1,4 @@ -import '../otp.dart'; +import '../models/otp_item.dart'; abstract interface class UriParserBase { bool canParse(String uri); From d399befa9b2fb5c48bb17abf0d5f26957d435713 Mon Sep 17 00:00:00 2001 From: Steven Tang Date: Sun, 29 Sep 2024 14:25:44 +1000 Subject: [PATCH 10/10] reject hotp --- lib/pages/qr.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pages/qr.dart b/lib/pages/qr.dart index 5a67dee..f05b9f5 100644 --- a/lib/pages/qr.dart +++ b/lib/pages/qr.dart @@ -1,5 +1,6 @@ import 'dart:async' show Future; import 'package:another_authenticator/state/app_state.dart'; +import 'package:another_authenticator_otp/otp.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show PlatformException; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -24,8 +25,11 @@ class _ScanQRPageState extends State { (_) async { try { final value = await _scan(); - // Parse scanned value into item and pop + // Parse scanned value into item var item = BaseItemType.newAuthenticatorItemFromUri(value); + if (item.totp.type == OtpType.hotp) { + throw Exception('HOTP not supported by UI'); + } // Pop until scan page Navigator.of(context).popUntil(ModalRoute.withName('/add/scan')); // Pop with scanned item