[auth] Fix: handle incorrect device time during code generation (#6096)
## Description ## Tests
This commit is contained in:
@@ -73,7 +73,10 @@ class AuthenticatorGateway {
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<AuthEntity>> getDiff(int sinceTime, {int limit = 500}) async {
|
||||
Future<(List<AuthEntity>, int?)> getDiff(
|
||||
int sinceTime, {
|
||||
int limit = 500,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _enteDio.get(
|
||||
"/authenticator/entity/diff",
|
||||
@@ -84,11 +87,12 @@ class AuthenticatorGateway {
|
||||
);
|
||||
final List<AuthEntity> authEntities = <AuthEntity>[];
|
||||
final diff = response.data["diff"] as List;
|
||||
final int? unixTimeInMicroSeconds = response.data["timestamp"] as int?;
|
||||
for (var entry in diff) {
|
||||
final AuthEntity entity = AuthEntity.fromMap(entry);
|
||||
authEntities.add(entity);
|
||||
}
|
||||
return authEntities;
|
||||
return (authEntities, unixTimeInMicroSeconds);
|
||||
} catch (e) {
|
||||
if (e is DioException && e.response?.statusCode == 401) {
|
||||
throw UnauthorizedError();
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/store/offline_authenticator_db.dart';
|
||||
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
|
||||
@@ -194,8 +195,13 @@ class AuthenticatorService {
|
||||
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
|
||||
_logger.info("Current sync is $lastSyncTime");
|
||||
const int fetchLimit = 500;
|
||||
final List<AuthEntity> result =
|
||||
late final List<AuthEntity> result;
|
||||
late final int? epochTimeInMicroseconds;
|
||||
(result, epochTimeInMicroseconds) =
|
||||
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
|
||||
PreferenceService.instance
|
||||
.computeAndStoreTimeOffset(epochTimeInMicroseconds);
|
||||
|
||||
_logger.info("${result.length} entries fetched from remote");
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
|
||||
@@ -18,6 +18,7 @@ class PreferenceService {
|
||||
late final SharedPreferences _prefs;
|
||||
|
||||
static const kHasShownCoachMarkKey = "has_shown_coach_mark_v2";
|
||||
static const kLocalTimeOffsetKey = "local_time_offset";
|
||||
static const kShouldShowLargeIconsKey = "should_show_large_icons";
|
||||
static const kShouldHideCodesKey = "should_hide_codes";
|
||||
static const kShouldAutoFocusOnSearchBar = "should_auto_focus_on_search_bar";
|
||||
@@ -114,4 +115,24 @@ class PreferenceService {
|
||||
return installedTimeinMillis;
|
||||
}
|
||||
}
|
||||
|
||||
// localEpochOffsetInMilliSecond returns the local epoch offset in milliseconds.
|
||||
// This is used to adjust the time for TOTP calculations when device local time is not in sync with actual time.
|
||||
int timeOffsetInMilliSeconds() {
|
||||
return _prefs.getInt(kLocalTimeOffsetKey) ?? 0;
|
||||
}
|
||||
|
||||
void computeAndStoreTimeOffset(
|
||||
int? epochTimeInMicroseconds,
|
||||
) {
|
||||
if (epochTimeInMicroseconds == null) {
|
||||
_prefs.remove(kLocalTimeOffsetKey);
|
||||
return;
|
||||
}
|
||||
int serverEpochTimeInMilliSecond = epochTimeInMicroseconds ~/ 1000;
|
||||
int localEpochTimeInMilliSecond = DateTime.now().millisecondsSinceEpoch;
|
||||
int localEpochOffset =
|
||||
serverEpochTimeInMilliSecond - localEpochTimeInMilliSecond;
|
||||
_prefs.setInt(kLocalTimeOffsetKey, localEpochOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@ import 'package:flutter/material.dart';
|
||||
class CodeTimerProgress extends StatefulWidget {
|
||||
final int period;
|
||||
final bool isCompactMode;
|
||||
final int timeOffsetInMilliseconds;
|
||||
const CodeTimerProgress({
|
||||
super.key,
|
||||
required this.period,
|
||||
this.isCompactMode = false,
|
||||
this.timeOffsetInMilliseconds = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -20,7 +22,7 @@ class CodeTimerProgress extends StatefulWidget {
|
||||
class _CodeTimerProgressState extends State<CodeTimerProgress> {
|
||||
late final Timer _timer;
|
||||
late final ValueNotifier<double> _progress;
|
||||
late final int _periodInMicros;
|
||||
late final int _periodInMilii;
|
||||
|
||||
// Reduce update frequency
|
||||
final int _updateIntervalMs =
|
||||
@@ -29,29 +31,30 @@ class _CodeTimerProgressState extends State<CodeTimerProgress> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_periodInMicros = widget.period * 1000000;
|
||||
_periodInMilii = widget.period * 1000;
|
||||
_progress = ValueNotifier<double>(0.0);
|
||||
_updateTimeRemaining(DateTime.now().microsecondsSinceEpoch);
|
||||
_updateTimeRemaining(DateTime.now().millisecondsSinceEpoch);
|
||||
|
||||
_timer = Timer.periodic(Duration(milliseconds: _updateIntervalMs), (timer) {
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
_updateTimeRemaining(now);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateTimeRemaining(int currentMicros) {
|
||||
void _updateTimeRemaining(int currentMilliSeconds) {
|
||||
// More efficient time calculation using modulo
|
||||
final elapsed = (currentMicros) % _periodInMicros;
|
||||
final timeRemaining = _periodInMicros - elapsed;
|
||||
_progress.value = timeRemaining / _periodInMicros;
|
||||
final elapsed = (currentMilliSeconds + widget.timeOffsetInMilliseconds) %
|
||||
_periodInMilii;
|
||||
final timeRemaining = _periodInMilii - elapsed;
|
||||
_progress.value = timeRemaining / _periodInMilii;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant CodeTimerProgress oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.period != widget.period) {
|
||||
_periodInMicros = widget.period * 1000000;
|
||||
_updateTimeRemaining(DateTime.now().microsecondsSinceEpoch);
|
||||
_periodInMilii = widget.period * 1000;
|
||||
_updateTimeRemaining(DateTime.now().millisecondsSinceEpoch);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -152,6 +152,8 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
key: ValueKey('period_${widget.code.period}'),
|
||||
period: widget.code.period,
|
||||
isCompactMode: widget.isCompactMode,
|
||||
timeOffsetInMilliseconds:
|
||||
PreferenceService.instance.timeOffsetInMilliSeconds(),
|
||||
),
|
||||
widget.isCompactMode
|
||||
? const SizedBox(height: 4)
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/preference_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:otp/otp.dart' as otp;
|
||||
import 'package:steam_totp/steam_totp.dart';
|
||||
|
||||
int millisecondsSinceEpoch() {
|
||||
return DateTime.now().millisecondsSinceEpoch +
|
||||
PreferenceService.instance.timeOffsetInMilliSeconds();
|
||||
}
|
||||
|
||||
String getOTP(Code code) {
|
||||
if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') {
|
||||
return _getSteamCode(code);
|
||||
@@ -12,7 +18,7 @@ String getOTP(Code code) {
|
||||
}
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
getSanitizedSecret(code.secret),
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
millisecondsSinceEpoch(),
|
||||
length: code.digits,
|
||||
interval: code.period,
|
||||
algorithm: _getAlgorithm(code),
|
||||
@@ -34,7 +40,7 @@ String _getSteamCode(Code code, [bool isNext = false]) {
|
||||
final SteamTOTP steamtotp = SteamTOTP(secret: code.secret);
|
||||
|
||||
return steamtotp.generate(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000 + (isNext ? code.period : 0),
|
||||
millisecondsSinceEpoch() ~/ 1000 + (isNext ? code.period : 0),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +50,7 @@ String getNextTotp(Code code) {
|
||||
}
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
getSanitizedSecret(code.secret),
|
||||
DateTime.now().millisecondsSinceEpoch + code.period * 1000,
|
||||
millisecondsSinceEpoch() + code.period * 1000,
|
||||
length: code.digits,
|
||||
interval: code.period,
|
||||
algorithm: _getAlgorithm(code),
|
||||
@@ -56,9 +62,7 @@ String getNextTotp(Code code) {
|
||||
// It returns the start time and a list of future codes.
|
||||
(int, List<String>) generateFutureTotpCodes(Code code, int count) {
|
||||
final int startTime =
|
||||
((DateTime.now().millisecondsSinceEpoch ~/ 1000) ~/ code.period) *
|
||||
code.period *
|
||||
1000;
|
||||
((millisecondsSinceEpoch() ~/ 1000) ~/ code.period) * code.period * 1000;
|
||||
final String secret = getSanitizedSecret(code.secret);
|
||||
final List<String> codes = [];
|
||||
if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') {
|
||||
|
||||
Reference in New Issue
Block a user