Skip to content

Commit

Permalink
Add support for endpoint checking prekey consistency.
Browse files Browse the repository at this point in the history
  • Loading branch information
greyson-signal authored and alex-signal committed Mar 7, 2024
1 parent 09b0f15 commit e1067e3
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 3 deletions.
Expand Up @@ -15,14 +15,17 @@ 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
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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -240,6 +251,29 @@ class PreKeysSyncJob private constructor(
}
}

@Throws(IOException::class)
private fun checkPreKeyConsistency(serviceIdType: ServiceIdType, protocolStore: SignalServiceAccountDataStore, metadataStore: PreKeyMetadataStore): Boolean {
val result: NetworkResult<Unit> = 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
Expand Down
18 changes: 18 additions & 0 deletions 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()
}
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
@@ -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<Unit> {
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())
}
}
}
@@ -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<ByteArray>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): ByteArray {
return Base64.decode(p.valueAsString)
}
}
@@ -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<ByteArray>() {
override fun serialize(value: ByteArray, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(Base64.encodeWithoutPadding(value))
}
}
@@ -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
)
Expand Up @@ -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";
Expand Down Expand Up @@ -874,6 +876,17 @@ private List<PreKeyBundle> 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
{
Expand Down

0 comments on commit e1067e3

Please sign in to comment.