From 3ff218f9c6234c35be3b03b5f924715a12a85d26 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 6 Jun 2024 10:14:09 -0400 Subject: [PATCH] Make build deprecation more resilient to clock skew. --- .../securesms/ApplicationContext.java | 9 +- .../reminder/OutdatedBuildReminder.java | 3 +- .../jobs/BuildExpirationConfirmationJob.kt | 91 +++++++++++++++++++ .../securesms/jobs/JobManagerFactories.java | 1 + .../securesms/keyvalue/MiscellaneousValues.kt | 6 ++ .../migrations/ApplicationMigrations.java | 2 +- .../RemoteDeprecationDetectorInterceptor.java | 2 +- .../securesms/util/RemoteDeprecation.java | 10 +- .../org/thoughtcrime/securesms/util/Util.java | 6 +- .../securesms/util/VersionTracker.kt | 2 +- .../api/services/CdsiSocket.java | 3 +- 11 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 90465469ef..0c8c3c31b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -53,6 +53,7 @@ import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.JumboEmoji; import org.thoughtcrime.securesms.gcm.FcmFetchManager; import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob; +import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob; import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob; import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; @@ -255,7 +256,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr long timeDiff = currentTime - lastForegroundTime; if (timeDiff < 0) { - Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)"); + Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)", true); } SignalStore.misc().setLastForegroundTime(currentTime); @@ -277,9 +278,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr } public void checkBuildExpiration() { - if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) { - Log.w(TAG, "Build expired!"); - SignalStore.misc().setClientDeprecated(true); + if (Util.getTimeUntilBuildExpiry(SignalStore.misc().getEstimatedServerTime()) <= 0 && !SignalStore.misc().isClientDeprecated()) { + Log.w(TAG, "Build potentially expired! Enqueing job to check.", true); + AppDependencies.getJobManager().add(new BuildExpirationConfirmationJob()); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java index eef2b70eb2..d21b9f255a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/reminder/OutdatedBuildReminder.java @@ -5,6 +5,7 @@ import android.content.Context; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.Util; @@ -42,6 +43,6 @@ public class OutdatedBuildReminder extends Reminder { } private static int getDaysUntilExpiry() { - return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry()); + return (int) TimeUnit.MILLISECONDS.toDays(Util.getTimeUntilBuildExpiry(SignalStore.misc().getEstimatedServerTime())); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt new file mode 100644 index 0000000000..2a73724b58 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BuildExpirationConfirmationJob.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.BuildConfig +import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.Util +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.RemoteConfigResult +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.seconds + +/** + * If we have reason to believe a build is expired, we run this job to double-check by fetching the server time. This prevents false positives from people + * moving their clock forward in time. + */ +class BuildExpirationConfirmationJob private constructor(params: Parameters) : Job(params) { + companion object { + const val KEY = "BuildExpirationConfirmationJob" + private val TAG = Log.tag(BuildExpirationConfirmationJob::class.java) + } + + constructor() : this( + Parameters.Builder() + .addConstraint(NetworkConstraint.KEY) + .setMaxInstancesForFactory(2) + .setMaxAttempts(Parameters.UNLIMITED) + .setLifespan(1.days.inWholeMilliseconds) + .build() + ) + + override fun serialize(): ByteArray? = null + + override fun getFactoryKey(): String = KEY + + override fun run(): Result { + if (Util.getTimeUntilBuildExpiry(SignalStore.misc().estimatedServerTime) > 0) { + Log.i(TAG, "Build not expired.", true) + return Result.success() + } + + if (SignalStore.misc().isClientDeprecated) { + Log.i(TAG, "Build already marked expired. Nothing to do.", true) + return Result.success() + } + + if (!SignalStore.account().isRegistered) { + Log.w(TAG, "Not registered. Can't check the server time, so assuming deprecated.", true) + SignalStore.misc().isClientDeprecated = true + return Result.success() + } + + val result: NetworkResult = NetworkResult.fromFetch { + AppDependencies.signalServiceAccountManager.remoteConfig + } + + return when (result) { + is NetworkResult.Success -> { + val serverTimeMs = result.result.serverEpochTimeSeconds.seconds.inWholeMilliseconds + SignalStore.misc().setLastKnownServerTime(serverTimeMs, System.currentTimeMillis()) + + if (Util.getTimeUntilBuildExpiry(serverTimeMs) <= 0) { + Log.w(TAG, "Build confirmed expired! Server time: $serverTimeMs, Local time: ${System.currentTimeMillis()}, Build time: ${BuildConfig.BUILD_TIMESTAMP}, Time since expiry: ${serverTimeMs - BuildConfig.BUILD_TIMESTAMP}", true) + SignalStore.misc().isClientDeprecated = true + } else { + Log.w(TAG, "Build not actually expired! Likely bad local clock. Server time: $serverTimeMs, Local time: ${System.currentTimeMillis()}, Build time: ${BuildConfig.BUILD_TIMESTAMP}") + } + Result.success() + } + is NetworkResult.ApplicationError -> Result.retry(defaultBackoff()) + is NetworkResult.NetworkError -> Result.retry(defaultBackoff()) + is NetworkResult.StatusCodeError -> if (result.code < 500) Result.retry(defaultBackoff()) else Result.success() + } + } + + override fun onFailure() { + } + + class Factory : Job.Factory { + override fun create(params: Parameters, bytes: ByteArray?): BuildExpirationConfirmationJob { + return BuildExpirationConfirmationJob(params) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 78e60c07c4..7813b7d707 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -119,6 +119,7 @@ public final class JobManagerFactories { put(BackupRestoreJob.KEY, new BackupRestoreJob.Factory()); put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory()); put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory()); + put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory()); put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory()); put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory()); put(CallLogEventSendJob.KEY, new CallLogEventSendJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt index 9a2aaecfaa..033ad8da4d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt @@ -202,6 +202,12 @@ internal class MiscellaneousValues internal constructor(store: KeyValueStore) : */ val lastKnownServerTimeOffset by longValue(SERVER_TIME_OFFSET, 0) + /** + * An estimate of the server time, based on the last-known server time offset. + */ + val estimatedServerTime: Long + get() = System.currentTimeMillis() - lastKnownServerTimeOffset + /** * The last time (using our local clock) we updated the server time offset returned by [.getLastKnownServerTimeOffset]}. */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index a828824c05..d81e9f886d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -168,7 +168,7 @@ public class ApplicationMigrations { VersionTracker.updateLastSeenVersion(context); return; } else { - Log.d(TAG, "About to update. Clearing deprecation flag."); + Log.d(TAG, "About to update. Clearing deprecation flag.", true); SignalStore.misc().setClientDeprecated(false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java index cc4b006efe..f4ed3c9db3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/net/RemoteDeprecationDetectorInterceptor.java @@ -22,7 +22,7 @@ public final class RemoteDeprecationDetectorInterceptor implements Interceptor { Response response = chain.proceed(chain.request()); if (response.code() == 499 && !SignalStore.misc().isClientDeprecated()) { - Log.w(TAG, "Received 499. Client version is deprecated."); + Log.w(TAG, "Received 499. Client version is deprecated.", true); SignalStore.misc().setClientDeprecated(true); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java index d5a416bb2d..306eb33044 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteDeprecation.java @@ -19,12 +19,20 @@ public final class RemoteDeprecation { private RemoteDeprecation() { } + /** + * @return The amount of time (in milliseconds) until this client version expires, or -1 if + * there's no pending expiration. + */ + public static long getTimeUntilDeprecation(long currentTime) { + return getTimeUntilDeprecation(FeatureFlags.clientExpiration(), currentTime, BuildConfig.VERSION_NAME); + } + /** * @return The amount of time (in milliseconds) until this client version expires, or -1 if * there's no pending expiration. */ public static long getTimeUntilDeprecation() { - return getTimeUntilDeprecation(FeatureFlags.clientExpiration(), System.currentTimeMillis(), BuildConfig.VERSION_NAME); + return getTimeUntilDeprecation(System.currentTimeMillis()); } /** diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index ef39a8bd9d..b345b92dcc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -349,14 +349,14 @@ public class Util { * @return The amount of time (in ms) until this build of Signal will be considered 'expired'. * Takes into account both the build age as well as any remote deprecation values. */ - public static long getTimeUntilBuildExpiry() { + public static long getTimeUntilBuildExpiry(long currentTime) { if (SignalStore.misc().isClientDeprecated()) { return 0; } - long buildAge = System.currentTimeMillis() - BuildConfig.BUILD_TIMESTAMP; + long buildAge = currentTime - BuildConfig.BUILD_TIMESTAMP; long timeUntilBuildDeprecation = BUILD_LIFESPAN - buildAge; - long timeUntilRemoteDeprecation = RemoteDeprecation.getTimeUntilDeprecation(); + long timeUntilRemoteDeprecation = RemoteDeprecation.getTimeUntilDeprecation(currentTime); if (timeUntilRemoteDeprecation != -1) { long timeUntilDeprecation = Math.min(timeUntilBuildDeprecation, timeUntilRemoteDeprecation); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.kt b/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.kt index 4e066af437..ff85b8f324 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/VersionTracker.kt @@ -24,7 +24,7 @@ object VersionTracker { val lastVersionCode = TextSecurePreferences.getLastVersionCode(context) if (currentVersionCode != lastVersionCode) { - Log.i(TAG, "Upgraded from $lastVersionCode to $currentVersionCode") + Log.i(TAG, "Upgraded from $lastVersionCode to $currentVersionCode. Clearing client deprecation.", true) SignalStore.misc().isClientDeprecated = false val jobChain = listOf(RemoteConfigRefreshJob(), RefreshAttributesJob()) AppDependencies.jobManager.startChain(jobChain).enqueue() diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java index f6f01aebba..dfa3975a6c 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/CdsiSocket.java @@ -165,7 +165,8 @@ final class CdsiSocket { webSocket.close(1000, "OK"); break; } - } catch (IOException | AttestationDataException | SgxCommunicationFailureException e) { + } catch (IOException | AttestationDataException | SgxCommunicationFailureException | AssertionError e) { + // TODO only catching AssertionError because of libsignal bug. Remove when bug is fixed. Log.w(TAG, e); webSocket.close(1000, "OK"); emitter.tryOnError(e);