Add support for endpoint checking prekey consistency.
This commit is contained in:
parent
09b0f15294
commit
e1067e30de
8 changed files with 191 additions and 3 deletions
|
@ -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<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
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -30,6 +30,7 @@ import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
|||
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 class SignalServiceAccountManager {
|
|||
return ArchiveApi.create(pushServiceSocket, configuration.getBackupServerPublicParams(), credentials.getAci());
|
||||
}
|
||||
|
||||
public KeysApi getKeysApi() {
|
||||
return KeysApi.create(pushServiceSocket);
|
||||
}
|
||||
|
||||
public AuthCredentials getPaymentsAuthorization() throws IOException {
|
||||
return pushServiceSocket.getPaymentsAuthorization();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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 @@ public class PushServiceSocket {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue