Forráskód Böngészése

HOTP next counter (#173)

Neeraj Gupta 2 éve
szülő
commit
58cf5ab4c3

+ 22 - 1
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 +
           ":" +
@@ -59,7 +64,7 @@ class Code {
           "?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=" +
           updateIssuer +
           "&period=$updatePeriod&secret=" +
-          updatedSecret,
+          updatedSecret + (updatedType == Type.hotp ? "&counter=$updatedCounter" : ""),
       generatedID: generatedID,
     );
   }
@@ -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 =
@@ -197,6 +216,7 @@ class Code {
         other.digits == digits &&
         other.period == period &&
         other.secret == secret &&
+        other.counter == counter &&
         other.type == type &&
         other.rawData == rawData;
   }
@@ -209,6 +229,7 @@ class Code {
         period.hashCode ^
         secret.hashCode ^
         type.hashCode ^
+        counter.hashCode ^
         rawData.hashCode;
   }
 }

+ 1 - 1
lib/onboarding/view/setup_enter_secret_key_page.dart

@@ -132,7 +132,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
                                 secret: secret,
                               );
                         // Verify the validity of the code
-                        getTotp(newCode);
+                        getOTP(newCode);
                         Navigator.of(context).pop(newCode);
                       } catch (e) {
                         _showIncorrectDetailsDialog(context);

+ 62 - 30
lib/ui/code_widget.dart

@@ -35,10 +35,12 @@ class _CodeWidgetState extends State<CodeWidget> {
 
     _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<CodeWidget> {
   @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<CodeWidget> {
                     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<CodeWidget> {
                                 },
                               ),
                             ),
-                            Column(
-                              crossAxisAlignment: CrossAxisAlignment.end,
-                              children: [
-                                Text(
-                                  l10n.nextTotpTitle,
-                                  style: Theme.of(context).textTheme.caption,
-                                ),
-                                ValueListenableBuilder<String>(
-                                  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<String>(
+                                        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<CodeWidget> {
   }
 
   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<void> _onEditPressed(_) async {
     final Code? code = await Navigator.of(context).push(
       MaterialPageRoute(
@@ -287,9 +318,9 @@ class _CodeWidgetState extends State<CodeWidget> {
     }
   }
 
-  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<CodeWidget> {
 
   String _getNextTotp() {
     try {
+      assert(widget.code.type == Type.totp);
       return getNextTotp(widget.code);
     } catch (e) {
       return context.l10n.error;

+ 1 - 1
lib/ui/scanner_gauth_page.dart

@@ -46,7 +46,7 @@ class ScannerGoogleAuthPageState extends State<ScannerGoogleAuthPage> {
             child: QRView(
               key: qrKey,
               overlay: QrScannerOverlayShape(
-                  borderColor: getEnteColorScheme(context).primary700),
+                  borderColor: getEnteColorScheme(context).primary700,),
               onQRViewCreated: _onQRViewCreated,
               formatsAllowed: const [BarcodeFormat.qrcode],
             ),

+ 14 - 1
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),

+ 0 - 13
test/app/view/app_test.dart

@@ -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);
-    });
-  });
-}

+ 10 - 0
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", () {

+ 0 - 28
test/widget_test.dart

@@ -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);
-  });
-}