From e1067e30dee4eebc304db67d642f4b2bfc0a95cf Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Mon, 26 Feb 2024 15:59:05 -0500 Subject: [PATCH] Add support for endpoint checking prekey consistency. --- .../securesms/jobs/PreKeysSyncJob.kt | 40 ++++++++++++- .../org/signal/core/util/LongExtensions.kt | 18 ++++++ .../api/SignalServiceAccountManager.java | 5 ++ .../signalservice/api/keys/KeysApi.kt | 57 +++++++++++++++++++ .../push/ByteArrayDeserializerBase64.kt | 20 +++++++ .../ByteArraySerializerBase64NoPadding.kt | 20 +++++++ .../push/CheckRepeatedUsedPreKeysRequest.kt | 21 +++++++ .../internal/push/PushServiceSocket.java | 13 +++++ 8 files changed, 191 insertions(+), 3 deletions(-) create mode 100644 core-util-jvm/src/main/java/org/signal/core/util/LongExtensions.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/keys/KeysApi.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArrayDeserializerBase64.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArraySerializerBase64NoPadding.kt create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/CheckRepeatedUsedPreKeysRequest.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt index f3b51d7a593..2f216142fe1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PreKeysSyncJob.kt @@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint import org.thoughtcrime.securesms.jobs.protos.PreKeysSyncJobData import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.util.FeatureFlags +import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.SignalServiceAccountDataStore import org.whispersystems.signalservice.api.account.PreKeyUpload import org.whispersystems.signalservice.api.push.ServiceId @@ -22,7 +23,9 @@ import org.whispersystems.signalservice.api.push.ServiceIdType import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts +import java.io.IOException import java.util.concurrent.TimeUnit +import kotlin.jvm.Throws import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit @@ -121,9 +124,17 @@ class PreKeysSyncJob private constructor( } val forceRotation = if (forceRotationRequested) { - val timeSinceLastForcedRotation = System.currentTimeMillis() - SignalStore.misc().lastForcedPreKeyRefresh - // We check < 0 in case someone changed their clock and had a bad value set - timeSinceLastForcedRotation > FeatureFlags.preKeyForceRefreshInterval() || timeSinceLastForcedRotation < 0 + if (!checkPreKeyConsistency(ServiceIdType.ACI, ApplicationDependencies.getProtocolStore().aci(), SignalStore.account().aciPreKeys)) { + warn(TAG, ServiceIdType.ACI, "Prekey consistency check failed! Must rotate keys!") + true + } else if (!checkPreKeyConsistency(ServiceIdType.PNI, ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys)) { + warn(TAG, ServiceIdType.PNI, "Prekey consistency check failed! Must rotate keys!") + true + } else { + val timeSinceLastForcedRotation = System.currentTimeMillis() - SignalStore.misc().lastForcedPreKeyRefresh + // We check < 0 in case someone changed their clock and had a bad value set + timeSinceLastForcedRotation > FeatureFlags.preKeyForceRefreshInterval() || timeSinceLastForcedRotation < 0 + } } else { false } @@ -240,6 +251,29 @@ class PreKeysSyncJob private constructor( } } + @Throws(IOException::class) + private fun checkPreKeyConsistency(serviceIdType: ServiceIdType, protocolStore: SignalServiceAccountDataStore, metadataStore: PreKeyMetadataStore): Boolean { + val result: NetworkResult = ApplicationDependencies.getSignalServiceAccountManager().keysApi.checkRepeatedUseKeys( + serviceIdType = serviceIdType, + identityKey = protocolStore.identityKeyPair.publicKey, + signedPreKeyId = metadataStore.activeSignedPreKeyId, + signedPreKey = protocolStore.loadSignedPreKey(metadataStore.activeSignedPreKeyId).keyPair.publicKey, + lastResortKyberKeyId = metadataStore.lastResortKyberPreKeyId, + lastResortKyberKey = protocolStore.loadKyberPreKey(metadataStore.lastResortKyberPreKeyId).keyPair.publicKey + ) + + return when (result) { + is NetworkResult.Success -> true + is NetworkResult.NetworkError -> throw result.throwable ?: PushNetworkException("Network error") + is NetworkResult.ApplicationError -> throw result.throwable + is NetworkResult.StatusCodeError -> if (result.code == 409) { + false + } else { + throw NonSuccessfulResponseCodeException(result.code) + } + } + } + override fun onShouldRetry(e: Exception): Boolean { return when (e) { is NonSuccessfulResponseCodeException -> false diff --git a/core-util-jvm/src/main/java/org/signal/core/util/LongExtensions.kt b/core-util-jvm/src/main/java/org/signal/core/util/LongExtensions.kt new file mode 100644 index 00000000000..8bd8b2c4147 --- /dev/null +++ b/core-util-jvm/src/main/java/org/signal/core/util/LongExtensions.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import java.nio.ByteBuffer + +/** + * Converts the long into [ByteArray]. + */ +fun Long.toByteArray(): ByteArray { + return ByteBuffer + .allocate(Long.SIZE_BYTES) + .putLong(this) + .array() +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 3d66981af23..8343fd234fd 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -30,6 +30,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.kbs.MasterKey; +import org.whispersystems.signalservice.api.keys.KeysApi; import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.payments.CurrencyConversions; @@ -864,6 +865,10 @@ public ArchiveApi getArchiveApi() { return ArchiveApi.create(pushServiceSocket, configuration.getBackupServerPublicParams(), credentials.getAci()); } + public KeysApi getKeysApi() { + return KeysApi.create(pushServiceSocket); + } + public AuthCredentials getPaymentsAuthorization() throws IOException { return pushServiceSocket.getPaymentsAuthorization(); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/keys/KeysApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/keys/KeysApi.kt new file mode 100644 index 00000000000..9c7ba8d1009 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/keys/KeysApi.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.api.keys + +import org.signal.core.util.toByteArray +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.ecc.ECPublicKey +import org.signal.libsignal.protocol.kem.KEMPublicKey +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.push.ServiceIdType +import org.whispersystems.signalservice.internal.push.PushServiceSocket +import java.security.MessageDigest + +/** + * Contains APIs for interacting with /keys endpoints on the service. + */ +class KeysApi(private val pushServiceSocket: PushServiceSocket) { + + companion object { + @JvmStatic + fun create(pushServiceSocket: PushServiceSocket): KeysApi { + return KeysApi(pushServiceSocket) + } + } + + /** + * Checks to see if our local view of our repeated-use prekeys matches the server's view. It's an all-or-nothing match, and no details can be given beyond + * whether or not everything perfectly matches or not. + * + * Status codes: + * - 200: Everything matches + * - 409: Something doesn't match + */ + fun checkRepeatedUseKeys( + serviceIdType: ServiceIdType, + identityKey: IdentityKey, + signedPreKeyId: Int, + signedPreKey: ECPublicKey, + lastResortKyberKeyId: Int, + lastResortKyberKey: KEMPublicKey + ): NetworkResult { + val digest: MessageDigest = MessageDigest.getInstance("SHA-256").apply { + update(identityKey.serialize()) + update(signedPreKeyId.toLong().toByteArray()) + update(signedPreKey.serialize()) + update(lastResortKyberKeyId.toLong().toByteArray()) + update(lastResortKyberKey.serialize()) + } + + return NetworkResult.fromFetch { + pushServiceSocket.checkRepeatedUsePreKeys(serviceIdType, digest.digest()) + } + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArrayDeserializerBase64.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArrayDeserializerBase64.kt new file mode 100644 index 00000000000..74d0a0978e7 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArrayDeserializerBase64.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import org.signal.core.util.Base64 + +/** + * Deserializes any valid base64 (regardless of padding or url-safety) into a ByteArray. + */ +class ByteArrayDeserializerBase64 : JsonDeserializer() { + override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ByteArray { + return Base64.decode(p.valueAsString) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArraySerializerBase64NoPadding.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArraySerializerBase64NoPadding.kt new file mode 100644 index 00000000000..e0552ca25ae --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ByteArraySerializerBase64NoPadding.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import org.signal.core.util.Base64 + +/** + * JSON serializer to encode a ByteArray as a base64 string without padding. + */ +class ByteArraySerializerBase64NoPadding : JsonSerializer() { + override fun serialize(value: ByteArray, gen: JsonGenerator, serializers: SerializerProvider) { + gen.writeString(Base64.encodeWithoutPadding(value)) + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/CheckRepeatedUsedPreKeysRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/CheckRepeatedUsedPreKeysRequest.kt new file mode 100644 index 00000000000..a304069ab90 --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/CheckRepeatedUsedPreKeysRequest.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.signalservice.internal.push + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.annotation.JsonSerialize + +/** + * Request body to check if our prekeys match what's on the service. + */ +class CheckRepeatedUsedPreKeysRequest( + @JsonProperty + val identityType: String, + + @JsonProperty + @JsonSerialize(using = ByteArraySerializerBase64NoPadding::class) + val digest: ByteArray +) diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index ea3cd10d806..cce59586c6f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -231,6 +231,8 @@ public class PushServiceSocket { private static final String PREKEY_METADATA_PATH = "/v2/keys?identity=%s"; private static final String PREKEY_PATH = "/v2/keys?identity=%s"; private static final String PREKEY_DEVICE_PATH = "/v2/keys/%s/%s?pq=true"; + private static final String PREKEY_CHECK_PATH = "/v2/keys/check"; + private static final String PROVISIONING_CODE_PATH = "/v1/devices/provisioning/code"; private static final String PROVISIONING_MESSAGE_PATH = "/v1/provisioning/%s"; @@ -874,6 +876,17 @@ private List getPreKeysBySpecifier(SignalServiceAddress destinatio } } + public void checkRepeatedUsePreKeys(ServiceIdType serviceIdType, byte[] digest) throws IOException { + String body = JsonUtil.toJson(new CheckRepeatedUsedPreKeysRequest(serviceIdType.toString(), digest)); + + makeServiceRequest(PREKEY_CHECK_PATH, "POST", body, NO_HEADERS, (responseCode, body1) -> { + // Must override this handling because otherwise code assumes a device mismatch error + if (responseCode == 409) { + throw new NonSuccessfulResponseCodeException(409); + } + }, Optional.empty()); + } + public void retrieveAttachment(int cdnNumber, SignalServiceAttachmentRemoteId cdnPath, File destination, long maxSizeBytes, ProgressListener listener) throws IOException, MissingConfigurationException {