HOTP next counter (#173)

This commit is contained in:
Neeraj Gupta 2023-08-01 17:58:48 +05:30 committed by GitHub
commit 58cf5ab4c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 110 additions and 75 deletions

View file

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

View file

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

View file

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

View file

@ -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],
),

View file

@ -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),

View file

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

View file

@ -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", () {

View file

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