Make build deprecation more resilient to clock skew.
This commit is contained in:
parent
f572eb5322
commit
3ff218f9c6
11 changed files with 122 additions and 13 deletions
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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]}.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue