diff --git a/auth/lib/gateway/authenticator.dart b/auth/lib/gateway/authenticator.dart index 3f3c7c8974..1375146e48 100644 --- a/auth/lib/gateway/authenticator.dart +++ b/auth/lib/gateway/authenticator.dart @@ -73,7 +73,10 @@ class AuthenticatorGateway { ); } - Future> getDiff(int sinceTime, {int limit = 500}) async { + Future<(List, 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 authEntities = []; 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(); diff --git a/auth/lib/services/authenticator_service.dart b/auth/lib/services/authenticator_service.dart index ee0ad32bd4..559e85f81a 100644 --- a/auth/lib/services/authenticator_service.dart +++ b/auth/lib/services/authenticator_service.dart @@ -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 result = + late final List 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; diff --git a/auth/lib/services/preference_service.dart b/auth/lib/services/preference_service.dart index 45a2cf0e66..773ee1b149 100644 --- a/auth/lib/services/preference_service.dart +++ b/auth/lib/services/preference_service.dart @@ -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); + } } diff --git a/auth/lib/ui/code_timer_progress.dart b/auth/lib/ui/code_timer_progress.dart index 520a18d76c..58386fe438 100644 --- a/auth/lib/ui/code_timer_progress.dart +++ b/auth/lib/ui/code_timer_progress.dart @@ -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 { late final Timer _timer; late final ValueNotifier _progress; - late final int _periodInMicros; + late final int _periodInMilii; // Reduce update frequency final int _updateIntervalMs = @@ -29,29 +31,30 @@ class _CodeTimerProgressState extends State { @override void initState() { super.initState(); - _periodInMicros = widget.period * 1000000; + _periodInMilii = widget.period * 1000; _progress = ValueNotifier(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); } } diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index 3ccb7ab67b..be9c2b8a69 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -152,6 +152,8 @@ class _CodeWidgetState extends State { key: ValueKey('period_${widget.code.period}'), period: widget.code.period, isCompactMode: widget.isCompactMode, + timeOffsetInMilliseconds: + PreferenceService.instance.timeOffsetInMilliSeconds(), ), widget.isCompactMode ? const SizedBox(height: 4) diff --git a/auth/lib/utils/totp_util.dart b/auth/lib/utils/totp_util.dart index 6b7f53ae5b..53b68be5ff 100644 --- a/auth/lib/utils/totp_util.dart +++ b/auth/lib/utils/totp_util.dart @@ -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) 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 codes = []; if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') {