Make build deprecation more resilient to clock skew.

This commit is contained in:
Greyson Parrelli 2024-06-06 10:14:09 -04:00 committed by Alex Hart
parent f572eb5322
commit 3ff218f9c6
11 changed files with 122 additions and 13 deletions

View file

@ -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());
}
}

View file

@ -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()));
}
}

View file

@ -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<RemoteConfigResult> = 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<BuildExpirationConfirmationJob> {
override fun create(params: Parameters, bytes: ByteArray?): BuildExpirationConfirmationJob {
return BuildExpirationConfirmationJob(params)
}
}
}

View file

@ -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());

View file

@ -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]}.
*/

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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());
}
/**

View file

@ -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);

View file

@ -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()

View file

@ -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);