mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-09-28 07:51:42 +01:00
Improve protections around stale/invalid sender certificates.
This commit is contained in:
committed by
Michelle Tang
parent
0d8f7d7f99
commit
5be9220226
@@ -226,7 +226,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
.addNonBlocking(this::initializeCircumvention)
|
||||
.addNonBlocking(this::initializeCleanup)
|
||||
.addNonBlocking(this::initializeGlideCodecs)
|
||||
.addNonBlocking(SealedSenderConstraint::checkAndSetValidity)
|
||||
.addNonBlocking(SealedSenderConstraint::refreshAndRotateIfNeeded)
|
||||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(this::beginJobLoop)
|
||||
.addNonBlocking(EmojiSource::refresh)
|
||||
@@ -284,6 +284,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
|
||||
startAnrDetector();
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
SealedSenderConstraint.refreshAndRotateIfNeeded();
|
||||
BackupRefreshJob.enqueueIfNecessary();
|
||||
InAppPaymentAuthCheckJob.enqueueIfNeeded();
|
||||
RemoteConfig.refreshIfNecessary();
|
||||
|
||||
+3
@@ -29,6 +29,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNum
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.BackoffUtil
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.net.SignalNetwork
|
||||
@@ -236,6 +237,8 @@ class ChangeNumberRepository(
|
||||
|
||||
SignalStore.certificate.setUnidentifiedAccessCertificate(certificateType, certificate)
|
||||
}
|
||||
|
||||
RotateCertificateJob.markRotated()
|
||||
}
|
||||
|
||||
private fun <T> retryChangeLocalNumberNetworkOperation(operation: () -> NetworkResult<T>): NetworkResult<T> {
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode;
|
||||
import org.thoughtcrime.securesms.database.model.RecipientRecord;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint;
|
||||
import org.thoughtcrime.securesms.keyvalue.CertificateType;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -51,11 +52,21 @@ public class SealedSenderAccessUtil {
|
||||
|
||||
@WorkerThread
|
||||
public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient, boolean log) {
|
||||
return SealedSenderAccess.forIndividual(getAccessFor(recipient, log));
|
||||
return SealedSenderAccess.forIndividual(getAccessFor(recipient, getSealedSenderCertificate(), log));
|
||||
}
|
||||
|
||||
/**
|
||||
* Profile fetches only send the access key, so this tolerates an expired certificate and never triggers a rotation.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @Nullable SealedSenderAccess getSealedSenderAccessForProfileFetch(@NonNull Recipient recipient, boolean log) {
|
||||
return SealedSenderAccess.forIndividual(getAccessFor(recipient, getStoredSenderCertificate(), log));
|
||||
}
|
||||
|
||||
public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull Recipient recipient, @Nullable SealedSenderAccess.CreateGroupSendToken createGroupSendToken) {
|
||||
return SealedSenderAccess.forIndividualWithGroupFallback(getAccessFor(recipient, true), getSealedSenderCertificate(), createGroupSendToken);
|
||||
SenderCertificate certificate = getSealedSenderCertificate();
|
||||
|
||||
return SealedSenderAccess.forIndividualWithGroupFallback(getAccessFor(recipient, certificate, true), certificate, createGroupSendToken);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -65,33 +76,31 @@ public class SealedSenderAccessUtil {
|
||||
|
||||
@WorkerThread
|
||||
public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull RecipientRecord record, boolean log) {
|
||||
return SealedSenderAccess.forIndividual(getAccessFor(record, log));
|
||||
return SealedSenderAccess.forIndividual(getAccessFor(record, getSealedSenderCertificate(), log));
|
||||
}
|
||||
|
||||
public static @Nullable SealedSenderAccess getSealedSenderAccessFor(@NonNull RecipientRecord record, @Nullable SealedSenderAccess.CreateGroupSendToken createGroupSendToken) {
|
||||
return SealedSenderAccess.forIndividualWithGroupFallback(getAccessFor(record, true), getSealedSenderCertificate(), createGroupSendToken);
|
||||
SenderCertificate certificate = getSealedSenderCertificate();
|
||||
|
||||
return SealedSenderAccess.forIndividualWithGroupFallback(getAccessFor(record, certificate, true), certificate, createGroupSendToken);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static @Nullable UnidentifiedAccess getAccessFor(@NonNull Recipient recipient, boolean log) {
|
||||
return getAccessFor(Collections.singletonList(recipient), false, log)
|
||||
private static @Nullable UnidentifiedAccess getAccessFor(@NonNull Recipient recipient, @Nullable SenderCertificate certificate, boolean log) {
|
||||
return getAccessFor(Collections.singletonList(recipient), certificate, false, log)
|
||||
.get(0)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static @Nullable UnidentifiedAccess getAccessFor(@NonNull RecipientRecord record, boolean log) {
|
||||
byte[] ourUnidentifiedAccessCertificate = SignalStore.certificate().getUnidentifiedAccessCertificate(getUnidentifiedAccessCertificateType());
|
||||
|
||||
private static @Nullable UnidentifiedAccess getAccessFor(@NonNull RecipientRecord record, @Nullable SenderCertificate certificate, boolean log) {
|
||||
UnidentifiedAccess unidentifiedAccess = null;
|
||||
if (ourUnidentifiedAccessCertificate != null) {
|
||||
if (certificate != null) {
|
||||
try {
|
||||
unidentifiedAccess = getTargetUnidentifiedAccess(record.getProfileKey(), getEffectiveSealedSenderAccessMode(record), ourUnidentifiedAccessCertificate, false);
|
||||
unidentifiedAccess = getTargetUnidentifiedAccess(record.getProfileKey(), getEffectiveSealedSenderAccessMode(record), certificate.getSerialized(), false);
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, "Invalid unidentified access certificate!", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Missing our unidentified access certificate!");
|
||||
}
|
||||
|
||||
if (log) {
|
||||
@@ -114,7 +123,7 @@ public class SealedSenderAccessUtil {
|
||||
|
||||
@WorkerThread
|
||||
public static Map<RecipientId, Optional<UnidentifiedAccess>> getAccessMapFor(@NonNull List<Recipient> recipients, boolean isForStory) {
|
||||
List<Optional<UnidentifiedAccess>> accessList = getAccessFor(recipients, isForStory, true);
|
||||
List<Optional<UnidentifiedAccess>> accessList = getAccessFor(recipients, getSealedSenderCertificate(), isForStory, true);
|
||||
|
||||
Iterator<Recipient> recipientIterator = recipients.iterator();
|
||||
Iterator<Optional<UnidentifiedAccess>> accessIterator = accessList.iterator();
|
||||
@@ -129,21 +138,16 @@ public class SealedSenderAccessUtil {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private static List<Optional<UnidentifiedAccess>> getAccessFor(@NonNull List<Recipient> recipients, boolean isForStory, boolean log) {
|
||||
CertificateType certificateType = getUnidentifiedAccessCertificateType();
|
||||
byte[] ourUnidentifiedAccessCertificate = SignalStore.certificate().getUnidentifiedAccessCertificate(certificateType);
|
||||
|
||||
private static List<Optional<UnidentifiedAccess>> getAccessFor(@NonNull List<Recipient> recipients, @Nullable SenderCertificate certificate, boolean isForStory, boolean log) {
|
||||
List<Optional<UnidentifiedAccess>> access = recipients.parallelStream().map(recipient -> {
|
||||
UnidentifiedAccess unidentifiedAccess = null;
|
||||
if (ourUnidentifiedAccessCertificate != null) {
|
||||
if (certificate != null) {
|
||||
try {
|
||||
Recipient resolved = recipient.resolve();
|
||||
unidentifiedAccess = getTargetUnidentifiedAccess(resolved.getProfileKey(), resolved.getSealedSenderAccessMode(), ourUnidentifiedAccessCertificate, isForStory);
|
||||
unidentifiedAccess = getTargetUnidentifiedAccess(resolved.getProfileKey(), resolved.getSealedSenderAccessMode(), certificate.getSerialized(), isForStory);
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, "Invalid unidentified access certificate!", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Missing our unidentified access certificate!");
|
||||
}
|
||||
return Optional.ofNullable(unidentifiedAccess);
|
||||
}).collect(Collectors.toList());
|
||||
@@ -158,20 +162,6 @@ public class SealedSenderAccessUtil {
|
||||
return access;
|
||||
}
|
||||
|
||||
public static @Nullable SenderCertificate getSealedSenderCertificate() {
|
||||
byte[] unidentifiedAccessCertificate = getUnidentifiedAccessCertificate();
|
||||
if (unidentifiedAccessCertificate == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SenderCertificate(unidentifiedAccessCertificate);
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull CertificateType getUnidentifiedAccessCertificateType() {
|
||||
if (SignalStore.account().isPhoneNumberless()) {
|
||||
return CertificateType.ACI_ONLY;
|
||||
@@ -187,6 +177,54 @@ public class SealedSenderAccessUtil {
|
||||
.getUnidentifiedAccessCertificate(getUnidentifiedAccessCertificateType());
|
||||
}
|
||||
|
||||
private static @Nullable SenderCertificate getStoredSenderCertificate() {
|
||||
byte[] certificateBytes = getUnidentifiedAccessCertificate();
|
||||
|
||||
if (certificateBytes == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SenderCertificate(certificateBytes);
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, "Unable to parse our unidentified access certificate!", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve once per access lookup: each null path re-runs {@link SealedSenderConstraint#refreshAndRotateIfNeeded()}, so calling this twice enqueues two
|
||||
* rotations.
|
||||
*/
|
||||
public static @Nullable SenderCertificate getSealedSenderCertificate() {
|
||||
byte[] certificateBytes = getUnidentifiedAccessCertificate();
|
||||
|
||||
if (certificateBytes == null) {
|
||||
Log.w(TAG, "Missing our unidentified access certificate!");
|
||||
SealedSenderConstraint.refreshAndRotateIfNeeded();
|
||||
return null;
|
||||
}
|
||||
|
||||
SenderCertificate certificate;
|
||||
try {
|
||||
certificate = new SenderCertificate(certificateBytes);
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, "Unable to parse our unidentified access certificate!", e);
|
||||
SealedSenderConstraint.refreshAndRotateIfNeeded();
|
||||
return null;
|
||||
}
|
||||
|
||||
long now = SignalStore.misc().getEstimatedServerTime();
|
||||
|
||||
if (now >= certificate.getExpiration()) {
|
||||
Log.w(TAG, "Our unidentified access certificate expired " + (now - certificate.getExpiration()) + " ms ago! Sending without sealed sender until it rotates.");
|
||||
SealedSenderConstraint.refreshAndRotateIfNeeded();
|
||||
return null;
|
||||
}
|
||||
|
||||
return certificate;
|
||||
}
|
||||
|
||||
private static @Nullable UnidentifiedAccess getTargetUnidentifiedAccess(@Nullable byte[] theirProfileKeyBytes, @NonNull SealedSenderAccessMode accessMode, @NonNull byte[] certificate, boolean isForStory) throws InvalidCertificateException {
|
||||
ProfileKey theirProfileKey = ProfileKeyUtil.profileKeyOrNull(theirProfileKeyBytes);
|
||||
|
||||
|
||||
+34
-33
@@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.jobmanager.ConstraintObserver
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Constraint that holds jobs until the sealed sender certificate is confirmed valid.
|
||||
@@ -21,54 +20,56 @@ object SealedSenderConstraint : Constraint {
|
||||
const val KEY = "SealedSenderConstraint"
|
||||
|
||||
private val TAG = Log.tag(SealedSenderConstraint::class.java)
|
||||
private val CERTIFICATE_EXPIRATION_BUFFER = TimeUnit.DAYS.toMillis(1)
|
||||
private val ROTATION_LEAD_TIME = TimeUnit.DAYS.toMillis(1)
|
||||
|
||||
private val valid = AtomicBoolean(false)
|
||||
@Volatile
|
||||
private var expiresAt: Long = 0
|
||||
|
||||
override fun isMet(): Boolean = valid.get()
|
||||
override fun isMet(): Boolean = System.currentTimeMillis() < expiresAt
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun applyToJobInfo(jobInfoBuilder: JobInfo.Builder) = Unit
|
||||
|
||||
@JvmStatic
|
||||
fun markValid() {
|
||||
valid.set(true)
|
||||
Observer.onChange()
|
||||
fun refresh() {
|
||||
expiresAt = computeExpiresAt()
|
||||
|
||||
if (isMet()) {
|
||||
Observer.onChange()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks all required certificate types. If all are present and not near expiry,
|
||||
* marks the constraint as valid. Otherwise enqueues a [RotateCertificateJob] and
|
||||
* leaves the constraint unmet until the rotation completes and calls [markValid].
|
||||
*/
|
||||
@JvmStatic
|
||||
fun checkAndSetValidity() {
|
||||
try {
|
||||
val requiredTypes = SignalStore.phoneNumberPrivacy.getRequiredCertificateTypes()
|
||||
fun refreshAndRotateIfNeeded() {
|
||||
refresh()
|
||||
|
||||
if (System.currentTimeMillis() > expiresAt - ROTATION_LEAD_TIME) {
|
||||
Log.w(TAG, "A sealed sender certificate is missing, expired, or nearly expired. Enqueuing rotation.")
|
||||
AppDependencies.jobManager.add(RotateCertificateJob())
|
||||
} else {
|
||||
Log.i(TAG, "All sealed sender certificates are valid.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeExpiresAt(): Long {
|
||||
return try {
|
||||
val requiredTypes = SignalStore.phoneNumberPrivacy.requiredCertificateTypes
|
||||
var earliest = Long.MAX_VALUE
|
||||
|
||||
for (certificateType in requiredTypes) {
|
||||
val certificateBytes = SignalStore.certificate.getUnidentifiedAccessCertificate(certificateType)
|
||||
|
||||
if (certificateBytes == null) {
|
||||
Log.w(TAG, "Missing certificate $certificateType. Enqueuing rotation.")
|
||||
AppDependencies.jobManager.add(RotateCertificateJob())
|
||||
return
|
||||
}
|
||||
|
||||
val certificate = SenderCertificate(certificateBytes)
|
||||
if (System.currentTimeMillis() > certificate.expiration - CERTIFICATE_EXPIRATION_BUFFER) {
|
||||
Log.w(TAG, "Certificate $certificateType is expired or near expiry. Enqueuing rotation.")
|
||||
AppDependencies.jobManager.add(RotateCertificateJob())
|
||||
return
|
||||
}
|
||||
val certificateBytes = SignalStore.certificate.getUnidentifiedAccessCertificate(certificateType) ?: return 0
|
||||
earliest = minOf(earliest, SenderCertificate(certificateBytes).expiration)
|
||||
}
|
||||
|
||||
Log.i(TAG, "All sealed sender certificates are valid.")
|
||||
markValid()
|
||||
if (requiredTypes.isEmpty()) {
|
||||
Long.MAX_VALUE
|
||||
} else {
|
||||
earliest + SignalStore.misc.lastKnownServerTimeOffset
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error checking certificate validity. Enqueuing rotation.", e)
|
||||
AppDependencies.jobManager.add(RotateCertificateJob())
|
||||
Log.w(TAG, "Error reading certificate validity.", e)
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ class RetrieveProfileJob private constructor(parameters: Parameters, private val
|
||||
id = recipient.id,
|
||||
serviceId = recipient.requireServiceId(),
|
||||
profileKey = recipient.profileKey?.let { ProfileKey(it) },
|
||||
sealedSenderAccess = SealedSenderAccessUtil.getSealedSenderAccessFor(recipient),
|
||||
sealedSenderAccess = SealedSenderAccessUtil.getSealedSenderAccessForProfileFetch(recipient, true),
|
||||
fetchExpiringCredential = !ExpiringProfileCredentialUtil.isValid(recipient.expiringProfileKeyCredential)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.network.exceptions.NonSuccessfulResponseCodeException;
|
||||
@@ -28,6 +29,7 @@ public final class RotateCertificateJob extends BaseJob {
|
||||
public RotateCertificateJob() {
|
||||
this(new Job.Parameters.Builder()
|
||||
.setQueue("__ROTATE_SENDER_CERTIFICATE__")
|
||||
.setMaxInstancesForFactory(1)
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
@@ -94,7 +96,13 @@ public final class RotateCertificateJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
SealedSenderConstraint.markValid();
|
||||
markRotated();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void markRotated() {
|
||||
SignalStore.certificate().setLastRotationTime(System.currentTimeMillis());
|
||||
SealedSenderConstraint.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,19 +1,36 @@
|
||||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class CertificateValues extends SignalStoreValues {
|
||||
|
||||
private static final String TAG = Log.tag(CertificateValues.class);
|
||||
|
||||
private static final String SEALED_SENDER_CERT_ACI_AND_E164 = "certificate.uuidAndE164";
|
||||
private static final String SEALED_SENDER_CERT_ACI_ONLY = "certificate.uuidOnly";
|
||||
private static final String LAST_ROTATION_TIME = "certificate.lastRotationTime";
|
||||
|
||||
CertificateValues(@NonNull KeyValueStore store) {
|
||||
private static final long NEVER_ROTATED = -1;
|
||||
private static final long LEGACY_ROTATION_INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
CertificateValues(@NonNull KeyValueStore store, @NonNull Context context) {
|
||||
super(store);
|
||||
|
||||
if (!store.containsKey(LAST_ROTATION_TIME)) {
|
||||
migrateFromSharedPrefsV1(context);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,4 +65,30 @@ public final class CertificateValues extends SignalStoreValues {
|
||||
}
|
||||
}
|
||||
|
||||
public long getLastRotationTime() {
|
||||
return getLong(LAST_ROTATION_TIME, NEVER_ROTATED);
|
||||
}
|
||||
|
||||
public void setLastRotationTime(long lastRotationTime) {
|
||||
putLong(LAST_ROTATION_TIME, lastRotationTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not alter. If you need to migrate more stuff, create a new method.
|
||||
* <p>
|
||||
* The legacy value held the next rotation time, always written as the clock at the last rotation plus a day.
|
||||
*/
|
||||
private void migrateFromSharedPrefsV1(@NonNull Context context) {
|
||||
Log.i(TAG, "[V1] Migrating certificate values from shared prefs.");
|
||||
|
||||
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
long legacyNextRotationTime = sharedPrefs.getLong("pref_unidentified_access_certificate_rotation_time", 0);
|
||||
|
||||
putLong(LAST_ROTATION_TIME, legacyNextRotationTime > 0 ? legacyNextRotationTime - LEGACY_ROTATION_INTERVAL : NEVER_ROTATED);
|
||||
|
||||
sharedPrefs.edit()
|
||||
.remove("pref_unidentified_access_certificate_rotation_time")
|
||||
.apply();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.keyvalue
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.ChangeNumberConstraintObserver
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint
|
||||
import org.thoughtcrime.securesms.jobs.DeprecatedNotificationJob
|
||||
import org.thoughtcrime.securesms.keyvalue.protos.LeastActiveLinkedDevice
|
||||
|
||||
@@ -261,6 +262,8 @@ class MiscellaneousValues internal constructor(store: KeyValueStore) : SignalSto
|
||||
.putLong(SERVER_TIME_OFFSET, currentTime - serverTime)
|
||||
.putLong(LAST_SERVER_TIME_OFFSET_UPDATE, System.currentTimeMillis())
|
||||
.apply()
|
||||
|
||||
SealedSenderConstraint.refresh()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,7 @@ class SignalStore(context: Application, private val store: KeyValueStore) {
|
||||
val internalValues = InternalValues(store)
|
||||
val emojiValues = EmojiValues(store)
|
||||
val settingsValues = SettingsValues(store, context)
|
||||
val certificateValues = CertificateValues(store)
|
||||
val certificateValues = CertificateValues(store, context)
|
||||
val phoneNumberPrivacyValues = PhoneNumberPrivacyValues(store)
|
||||
val onboardingValues = OnboardingValues(store)
|
||||
val wallpaperValues = WallpaperValues(store)
|
||||
|
||||
@@ -260,17 +260,19 @@ public final class GroupSendUtil {
|
||||
Set<Recipient> unregisteredTargets = allTargets.stream().filter(it -> it.isUnregistered() || it.isUnknown()).collect(Collectors.toSet());
|
||||
List<Recipient> registeredTargets = allTargets.stream().filter(r -> !unregisteredTargets.contains(r)).collect(Collectors.toList());
|
||||
|
||||
SenderCertificate senderCertificate = SealedSenderAccessUtil.getSealedSenderCertificate();
|
||||
|
||||
if (senderCertificate == null) {
|
||||
throw new IOException("No usable sealed sender certificate. Refusing to fall back to an unsealed send.");
|
||||
}
|
||||
|
||||
RecipientData recipients = new RecipientData(context, registeredTargets, isStorySend);
|
||||
Optional<GroupRecord> groupRecord = groupId != null ? SignalDatabase.groups().getGroup(groupId) : Optional.empty();
|
||||
GroupSendEndorsementRecords groupSendEndorsementRecords = groupRecord.filter(GroupRecord::getHasV2GroupProperties).map(g -> SignalDatabase.groups().getGroupSendEndorsements(g.getId())).orElse(null);
|
||||
long groupSendEndorsementExpiration = groupRecord.map(GroupRecord::getGroupSendEndorsementExpiration).orElse(0L);
|
||||
SenderCertificate senderCertificate = SealedSenderAccessUtil.getSealedSenderCertificate();
|
||||
boolean useGroupSendEndorsements = groupSendEndorsementRecords != null;
|
||||
|
||||
if (useGroupSendEndorsements && senderCertificate == null) {
|
||||
Log.w(TAG, "Can't use group send endorsements without a sealed sender certificate, falling back to access key");
|
||||
useGroupSendEndorsements = false;
|
||||
} else if (useGroupSendEndorsements) {
|
||||
if (useGroupSendEndorsements) {
|
||||
boolean refreshGroupSendEndorsements = false;
|
||||
|
||||
if (groupSendEndorsementExpiration == 0) {
|
||||
@@ -502,7 +504,7 @@ public final class GroupSendUtil {
|
||||
final AtomicLong entryId = new AtomicLong(-1);
|
||||
final boolean includeInMessageLog = sendOperation.shouldIncludeInMessageLog();
|
||||
|
||||
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, legacyTargetAddresses, legacyTargets, SealedSenderAccess.forFanOutGroupSend(groupSendTokens, SealedSenderAccessUtil.getSealedSenderCertificate(), legacyTargetAccesses), recipientUpdate, result -> {
|
||||
List<SendMessageResult> results = sendOperation.sendLegacy(messageSender, legacyTargetAddresses, legacyTargets, SealedSenderAccess.forFanOutGroupSend(groupSendTokens, senderCertificate, legacyTargetAccesses), recipientUpdate, result -> {
|
||||
if (!includeInMessageLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
+17
-5
@@ -3,29 +3,41 @@ package org.thoughtcrime.securesms.service;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.RotateCertificateJob;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RotateSenderCertificateListener extends PersistentAlarmManagerListener {
|
||||
|
||||
private static final String TAG = Log.tag(RotateSenderCertificateListener.class);
|
||||
|
||||
private static final long INTERVAL = TimeUnit.DAYS.toMillis(1);
|
||||
|
||||
@Override
|
||||
protected long getNextScheduledExecutionTime(Context context) {
|
||||
return TextSecurePreferences.getUnidentifiedAccessCertificateRotationTime(context);
|
||||
return resolveNextExecutionTime(SignalStore.certificate().getLastRotationTime(), System.currentTimeMillis());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected long onAlarm(Context context, long scheduledTime) {
|
||||
AppDependencies.getJobManager().add(new RotateCertificateJob());
|
||||
|
||||
long nextTime = System.currentTimeMillis() + INTERVAL;
|
||||
TextSecurePreferences.setUnidentifiedAccessCertificateRotationTime(context, nextTime);
|
||||
return System.currentTimeMillis() + INTERVAL;
|
||||
}
|
||||
|
||||
return nextTime;
|
||||
@VisibleForTesting
|
||||
static long resolveNextExecutionTime(long lastRotationTime, long now) {
|
||||
if (lastRotationTime > now) {
|
||||
Log.w(TAG, "Last rotation time is " + (lastRotationTime - now) + " ms in the future. Ignoring it and rotating now.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return lastRotationTime + INTERVAL;
|
||||
}
|
||||
|
||||
public static void schedule(Context context) {
|
||||
|
||||
@@ -147,7 +147,7 @@ public final class ProfileUtil {
|
||||
boolean allowUnidentifiedAccess)
|
||||
{
|
||||
ProfileService profileService = AppDependencies.getProfileService();
|
||||
SealedSenderAccess sealedSenderAccess = allowUnidentifiedAccess ? SealedSenderAccessUtil.getSealedSenderAccessFor(recipient, false) : SealedSenderAccess.NONE;
|
||||
SealedSenderAccess sealedSenderAccess = allowUnidentifiedAccess ? SealedSenderAccessUtil.getSealedSenderAccessForProfileFetch(recipient, false) : SealedSenderAccess.NONE;
|
||||
Optional<ProfileKey> profileKey = ProfileKeyUtil.profileKeyOptional(recipient.getProfileKey());
|
||||
|
||||
return Single.fromCallable(() -> toSignalServiceAddress(context, recipient))
|
||||
|
||||
@@ -124,7 +124,6 @@ public class TextSecurePreferences {
|
||||
|
||||
private static final String NEEDS_MESSAGE_PULL = "pref_needs_message_pull";
|
||||
|
||||
private static final String UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF = "pref_unidentified_access_certificate_rotation_time";
|
||||
public static final String UNIVERSAL_UNIDENTIFIED_ACCESS = "pref_universal_unidentified_access";
|
||||
public static final String SHOW_UNIDENTIFIED_DELIVERY_INDICATORS = "pref_show_unidentifed_delivery_indicators";
|
||||
private static final String UNIDENTIFIED_DELIVERY_ENABLED = "pref_unidentified_delivery_enabled";
|
||||
@@ -439,14 +438,6 @@ public class TextSecurePreferences {
|
||||
return getBooleanPreference(context, IN_THREAD_NOTIFICATION_PREF, true);
|
||||
}
|
||||
|
||||
public static long getUnidentifiedAccessCertificateRotationTime(Context context) {
|
||||
return getLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, 0L);
|
||||
}
|
||||
|
||||
public static void setUnidentifiedAccessCertificateRotationTime(Context context, long value) {
|
||||
setLongPreference(context, UNIDENTIFIED_ACCESS_CERTIFICATE_ROTATION_TIME_PREF, value);
|
||||
}
|
||||
|
||||
public static boolean isUniversalUnidentifiedAccess(Context context) {
|
||||
return getBooleanPreference(context, UNIVERSAL_UNIDENTIFIED_ACCESS, false);
|
||||
}
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.service
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.testutil.EmptyLogger
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RotateSenderCertificateListenerTest {
|
||||
|
||||
companion object {
|
||||
private val INTERVAL = TimeUnit.DAYS.toMillis(1)
|
||||
private const val NOW = 1789734201707L
|
||||
private const val NEVER_ROTATED = -1L
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUpClass() {
|
||||
Log.initialize(EmptyLogger())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a rotation one interval ago, when resolving, then it is due now`() {
|
||||
assertThat(RotateSenderCertificateListener.resolveNextExecutionTime(NOW - INTERVAL, NOW)).isEqualTo(NOW)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a rotation just now, when resolving, then it is due one interval out`() {
|
||||
assertThat(RotateSenderCertificateListener.resolveNextExecutionTime(NOW, NOW)).isEqualTo(NOW + INTERVAL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an overdue rotation, when resolving, then it is due in the past`() {
|
||||
assertThat(RotateSenderCertificateListener.resolveNextExecutionTime(NOW - (INTERVAL * 5), NOW)).isEqualTo(NOW - (INTERVAL * 4))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no recorded rotation, when resolving, then it is due in the past`() {
|
||||
assertThat(RotateSenderCertificateListener.resolveNextExecutionTime(NEVER_ROTATED, NOW)).isEqualTo(NEVER_ROTATED + INTERVAL)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a rotation barely in the future, when resolving, then it rotates now`() {
|
||||
assertThat(RotateSenderCertificateListener.resolveNextExecutionTime(NOW + 1, NOW)).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a rotation written by a bad boot clock, when resolving, then it rotates now`() {
|
||||
assertThat(RotateSenderCertificateListener.resolveNextExecutionTime(2279533251741L, NOW)).isEqualTo(0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user