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/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') {