From e6a6f0a76f506be096fed19dfb9689de1756e27b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:50:16 +0530 Subject: [PATCH 1/3] Remove bad tests --- test/app/view/app_test.dart | 13 ------------- test/widget_test.dart | 28 ---------------------------- 2 files changed, 41 deletions(-) delete mode 100644 test/app/view/app_test.dart delete mode 100644 test/widget_test.dart diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart deleted file mode 100644 index b7daa6fd6..000000000 --- a/test/app/view/app_test.dart +++ /dev/null @@ -1,13 +0,0 @@ -import "dart:ui"; - -import "package:ente_auth/app/app.dart"; -import "package:flutter_test/flutter_test.dart"; - -void main() { - group("App", () { - testWidgets("renders CounterPage", (tester) async { - await tester.pumpWidget(const App(locale: Locale("en"))); - // expect(find.byType(CounterPage), findsOneWidget); - }); - }); -} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 837b36fef..000000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,28 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import "package:ente_auth/app/view/app.dart"; -import "package:flutter_test/flutter_test.dart"; - -void main() { - testWidgets("Counter increments smoke test", (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const App()); - - // Verify that our counter starts at 0. - expect(find.text("Get Started"), findsOneWidget); - expect(find.text("1"), findsNothing); - - // // Tap the "+" icon and trigger a frame. - // await tester.tap(find.byIcon(Icons.add)); - // await tester.pump(); - // - // // Verify that our counter has incremented. - // expect(find.text("0"), findsNothing); - // expect(find.text("1"), findsOneWidget); - }); -} From 80d97734fd1069b8baddfcb2012a79a10ec2ff5b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 1 Aug 2023 15:50:28 +0530 Subject: [PATCH 2/3] Add support for parsing counter --- lib/models/code.dart | 19 +++++++++++++++++++ lib/ui/scanner_gauth_page.dart | 2 +- test/models/code_test.dart | 10 ++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/models/code.dart b/lib/models/code.dart index dd47c31be..7f645ae66 100644 --- a/lib/models/code.dart +++ b/lib/models/code.dart @@ -13,6 +13,7 @@ class Code { final Algorithm algorithm; final Type type; final String rawData; + final int counter; bool? hasSynced; Code( @@ -23,6 +24,7 @@ class Code { this.secret, this.algorithm, this.type, + this.counter, this.rawData, { this.generatedID, }); @@ -35,6 +37,7 @@ class Code { String? secret, Algorithm? algorithm, Type? type, + int? counter, }) { final String updateAccount = account ?? this.account; final String updateIssuer = issuer ?? this.issuer; @@ -43,6 +46,7 @@ class Code { final String updatedSecret = secret ?? this.secret; final Algorithm updatedAlgo = algorithm ?? this.algorithm; final Type updatedType = type ?? this.type; + final int updatedCounter = counter ?? this.counter; return Code( updateAccount, @@ -52,6 +56,7 @@ class Code { updatedSecret, updatedAlgo, updatedType, + updatedCounter, "otpauth://${updatedType.name}/" + updateIssuer + ":" + @@ -77,6 +82,7 @@ class Code { secret, Algorithm.sha1, Type.totp, + 0, "otpauth://totp/" + issuer + ":" + @@ -99,6 +105,7 @@ class Code { getSanitizedSecret(uri.queryParameters['secret']!), _getAlgorithm(uri), _getType(uri), + _getCounter(uri), rawData, ); } catch(e) { @@ -163,6 +170,18 @@ class Code { } } + 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 = diff --git a/lib/ui/scanner_gauth_page.dart b/lib/ui/scanner_gauth_page.dart index ee84c89f6..41909ed32 100644 --- a/lib/ui/scanner_gauth_page.dart +++ b/lib/ui/scanner_gauth_page.dart @@ -46,7 +46,7 @@ class ScannerGoogleAuthPageState extends State { child: QRView( key: qrKey, overlay: QrScannerOverlayShape( - borderColor: getEnteColorScheme(context).primary700), + borderColor: getEnteColorScheme(context).primary700,), onQRViewCreated: _onQRViewCreated, formatsAllowed: const [BarcodeFormat.qrcode], ), diff --git a/test/models/code_test.dart b/test/models/code_test.dart index 7eb91e901..9a0d18e03 100644 --- a/test/models/code_test.dart +++ b/test/models/code_test.dart @@ -19,6 +19,16 @@ void main() { expect(code.account, "testdata@ente.io", reason: "accountMismatch"); expect(code.secret, "ASKZNWOU6SVYAMVS"); }); + + test("validateCount", () { + final code = Code.fromRawData( + "otpauth://hotp/testdata@ente.io?secret=ASKZNWOU6SVYAMVS&issuer=GitHub&counter=15", + ); + expect(code.issuer, "GitHub", reason: "issuerMismatch"); + expect(code.account, "testdata@ente.io", reason: "accountMismatch"); + expect(code.secret, "ASKZNWOU6SVYAMVS"); + expect(code.counter, 15); + }); // test("parseWithFunnyAccountName", () { From e401503948423fe3c62a9ffc4aad4c98524b86b3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 1 Aug 2023 17:34:58 +0530 Subject: [PATCH 3/3] Add support for incrementing hotp counter --- lib/models/code.dart | 4 +- .../view/setup_enter_secret_key_page.dart | 2 +- lib/ui/code_widget.dart | 92 +++++++++++++------ lib/utils/totp_util.dart | 15 ++- 4 files changed, 80 insertions(+), 33 deletions(-) diff --git a/lib/models/code.dart b/lib/models/code.dart index 7f645ae66..40c8c6455 100644 --- a/lib/models/code.dart +++ b/lib/models/code.dart @@ -64,7 +64,7 @@ class Code { "?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=" + updateIssuer + "&period=$updatePeriod&secret=" + - updatedSecret, + updatedSecret + (updatedType == Type.hotp ? "&counter=$updatedCounter" : ""), generatedID: generatedID, ); } @@ -216,6 +216,7 @@ class Code { other.digits == digits && other.period == period && other.secret == secret && + other.counter == counter && other.type == type && other.rawData == rawData; } @@ -228,6 +229,7 @@ class Code { period.hashCode ^ secret.hashCode ^ type.hashCode ^ + counter.hashCode ^ rawData.hashCode; } } diff --git a/lib/onboarding/view/setup_enter_secret_key_page.dart b/lib/onboarding/view/setup_enter_secret_key_page.dart index 23ee3a88d..4d0aab9ec 100644 --- a/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -132,7 +132,7 @@ class _SetupEnterSecretKeyPageState extends State { secret: secret, ); // Verify the validity of the code - getTotp(newCode); + getOTP(newCode); Navigator.of(context).pop(newCode); } catch (e) { _showIncorrectDetailsDialog(context); diff --git a/lib/ui/code_widget.dart b/lib/ui/code_widget.dart index daa983236..dea079a87 100644 --- a/lib/ui/code_widget.dart +++ b/lib/ui/code_widget.dart @@ -35,10 +35,12 @@ class _CodeWidgetState extends State { _everySecondTimer = Timer.periodic(const Duration(milliseconds: 500), (Timer t) { - String newCode = _getTotp(); + String newCode = _getCurrentOTP(); if (newCode != _currentCode.value) { _currentCode.value = newCode; - _nextCode.value = _getNextTotp(); + if (widget.code.type == Type.totp) { + _nextCode.value = _getNextTotp(); + } } }); } @@ -54,8 +56,10 @@ class _CodeWidgetState extends State { @override Widget build(BuildContext context) { if (!_isInitialized) { - _currentCode.value = _getTotp(); - _nextCode.value = _getNextTotp(); + _currentCode.value = _getCurrentOTP(); + if (widget.code.type == Type.totp) { + _nextCode.value = _getNextTotp(); + } _isInitialized = true; } final l10n = context.l10n; @@ -113,9 +117,10 @@ class _CodeWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - CodeTimerProgress( - period: widget.code.period, - ), + if (widget.code.type == Type.totp) + CodeTimerProgress( + period: widget.code.period, + ), const SizedBox( height: 16, ), @@ -174,27 +179,47 @@ class _CodeWidgetState extends State { }, ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - l10n.nextTotpTitle, - style: Theme.of(context).textTheme.caption, - ), - ValueListenableBuilder( - valueListenable: _nextCode, - builder: (context, value, child) { - return Text( - value, - style: const TextStyle( - fontSize: 18, - color: Colors.grey, + widget.code.type == Type.totp + ? Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + l10n.nextTotpTitle, + style: + Theme.of(context).textTheme.caption, ), - ); - }, - ), - ], - ), + ValueListenableBuilder( + valueListenable: _nextCode, + builder: (context, value, child) { + return Text( + value, + style: const TextStyle( + fontSize: 18, + color: Colors.grey, + ), + ); + }, + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + l10n.nextTotpTitle, + style: + Theme.of(context).textTheme.caption, + ), + InkWell( + onTap: _onNextHotpTapped, + child: const Icon( + Icons.forward_outlined, + size: 32, + color: Colors.grey, + ), + ), + ], + ), ], ), ), @@ -213,10 +238,16 @@ class _CodeWidgetState extends State { } void _copyToClipboard() { - FlutterClipboard.copy(_getTotp()) + FlutterClipboard.copy(_getCurrentOTP()) .then((value) => showToast(context, context.l10n.copiedToClipboard)); } + void _onNextHotpTapped() { + if(widget.code.type == Type.hotp) { + CodeStore.instance.addCode(widget.code.copyWith(counter: widget.code.counter + 1), shouldSync: true).ignore(); + } + } + Future _onEditPressed(_) async { final Code? code = await Navigator.of(context).push( MaterialPageRoute( @@ -287,9 +318,9 @@ class _CodeWidgetState extends State { } } - String _getTotp() { + String _getCurrentOTP() { try { - return getTotp(widget.code); + return getOTP(widget.code); } catch (e) { return context.l10n.error; } @@ -297,6 +328,7 @@ class _CodeWidgetState extends State { String _getNextTotp() { try { + assert(widget.code.type == Type.totp); return getNextTotp(widget.code); } catch (e) { return context.l10n.error; diff --git a/lib/utils/totp_util.dart b/lib/utils/totp_util.dart index d3371ea02..d76318280 100644 --- a/lib/utils/totp_util.dart +++ b/lib/utils/totp_util.dart @@ -1,7 +1,10 @@ import 'package:ente_auth/models/code.dart'; import 'package:otp/otp.dart' as otp; -String getTotp(Code code) { +String getOTP(Code code) { + if(code.type == Type.hotp) { + return _getHOTPCode(code); + } return otp.OTP.generateTOTPCodeString( getSanitizedSecret(code.secret), DateTime.now().millisecondsSinceEpoch, @@ -12,6 +15,16 @@ String getTotp(Code code) { ); } +String _getHOTPCode(Code code) { + return otp.OTP.generateHOTPCodeString( + getSanitizedSecret(code.secret), + code.counter, + length: code.digits, + algorithm: _getAlgorithm(code), + isGoogle: true, + ); +} + String getNextTotp(Code code) { return otp.OTP.generateTOTPCodeString( getSanitizedSecret(code.secret),