HOTP next counter (#173)
This commit is contained in:
commit
58cf5ab4c3
8 changed files with 110 additions and 75 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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],
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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", () {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Add table
Reference in a new issue