mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 17:08:27 +01:00
Use strongly-typed pre-keys
This commit is contained in:
@@ -41,7 +41,9 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
|
||||
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAuthenticatedAccount;
|
||||
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyCount;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponse;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKeyResponseItem;
|
||||
@@ -207,9 +209,9 @@ public class KeysController {
|
||||
|
||||
for (Device device : devices) {
|
||||
UUID identifier = usePhoneNumberIdentity ? target.getPhoneNumberIdentifier() : targetUuid;
|
||||
SignedPreKey signedECPreKey = usePhoneNumberIdentity ? device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
PreKey unsignedECPreKey = keys.takeEC(identifier, device.getId()).orElse(null);
|
||||
SignedPreKey pqPreKey = returnPqKey ? keys.takePQ(identifier, device.getId()).orElse(null) : null;
|
||||
ECSignedPreKey signedECPreKey = usePhoneNumberIdentity ? device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
ECPreKey unsignedECPreKey = keys.takeEC(identifier, device.getId()).orElse(null);
|
||||
KEMSignedPreKey pqPreKey = returnPqKey ? keys.takePQ(identifier, device.getId()).orElse(null) : null;
|
||||
|
||||
if (signedECPreKey != null || unsignedECPreKey != null || pqPreKey != null) {
|
||||
final int registrationId = usePhoneNumberIdentity ?
|
||||
@@ -234,7 +236,7 @@ public class KeysController {
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@ChangesDeviceEnabledState
|
||||
public void setSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
@Valid final SignedPreKey signedPreKey,
|
||||
@Valid final ECSignedPreKey signedPreKey,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
@@ -252,11 +254,11 @@ public class KeysController {
|
||||
@GET
|
||||
@Path("/signed")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Optional<SignedPreKey> getSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
public Optional<ECSignedPreKey> getSignedKey(@Auth final AuthenticatedAccount auth,
|
||||
@QueryParam("identity") final Optional<String> identityType) {
|
||||
|
||||
Device device = auth.getAuthenticatedDevice();
|
||||
SignedPreKey signedPreKey = usePhoneNumberIdentity(identityType) ?
|
||||
ECSignedPreKey signedPreKey = usePhoneNumberIdentity(identityType) ?
|
||||
device.getPhoneNumberIdentitySignedPreKey() : device.getSignedPreKey();
|
||||
|
||||
return Optional.ofNullable(signedPreKey);
|
||||
|
||||
@@ -20,8 +20,6 @@ import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
|
||||
|
||||
public record ChangeNumberRequest(
|
||||
@Schema(description="""
|
||||
@@ -54,7 +52,7 @@ public record ChangeNumberRequest(
|
||||
@Schema(description="""
|
||||
A new signed elliptic-curve prekey for each enabled device on the account, including this one.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@NotNull @Valid Map<Long, @NotNull @Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> devicePniSignedPrekeys,
|
||||
@NotNull @Valid Map<Long, @NotNull @Valid ECSignedPreKey> devicePniSignedPrekeys,
|
||||
|
||||
@Schema(description="""
|
||||
A new signed post-quantum last-resort prekey for each enabled device on the account, including this one.
|
||||
@@ -62,14 +60,14 @@ public record ChangeNumberRequest(
|
||||
If present, must contain one prekey per enabled device including this one.
|
||||
Prekeys for devices that did not previously have any post-quantum prekeys stored will be silently dropped.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@Valid Map<Long, @NotNull @Valid @ValidPreKey(type=PreKeyType.KYBER) SignedPreKey> devicePniPqLastResortPrekeys,
|
||||
@Valid Map<Long, @NotNull @Valid KEMSignedPreKey> devicePniPqLastResortPrekeys,
|
||||
|
||||
@Schema(description="the new phone-number-identity registration ID for each enabled device on the account, including this one")
|
||||
@NotNull Map<Long, Integer> pniRegistrationIds) implements PhoneVerificationRequest {
|
||||
|
||||
@AssertTrue
|
||||
public boolean isSignatureValidOnEachSignedPreKey() {
|
||||
List<SignedPreKey> spks = new ArrayList<>();
|
||||
List<SignedPreKey<?>> spks = new ArrayList<>();
|
||||
if (devicePniSignedPrekeys != null) {
|
||||
spks.addAll(devicePniSignedPrekeys.values());
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
|
||||
|
||||
public record ChangePhoneNumberRequest(
|
||||
@Schema(description="the new phone number for this account")
|
||||
@@ -46,7 +44,7 @@ public record ChangePhoneNumberRequest(
|
||||
@Schema(description="""
|
||||
A new signed elliptic-curve prekey for each enabled device on the account, including this one.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@Nullable Map<Long, @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> devicePniSignedPrekeys,
|
||||
@Nullable Map<Long, ECSignedPreKey> devicePniSignedPrekeys,
|
||||
|
||||
@Schema(description="""
|
||||
A new signed post-quantum last-resort prekey for each enabled device on the account, including this one.
|
||||
@@ -54,14 +52,14 @@ public record ChangePhoneNumberRequest(
|
||||
If present, must contain one prekey per enabled device including this one.
|
||||
Prekeys for devices that did not previously have any post-quantum prekeys stored will be silently dropped.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@Nullable @Valid Map<Long, @NotNull @Valid @ValidPreKey(type=PreKeyType.KYBER) SignedPreKey> devicePniPqLastResortPrekeys,
|
||||
@Nullable @Valid Map<Long, @NotNull @Valid KEMSignedPreKey> devicePniPqLastResortPrekeys,
|
||||
|
||||
@Schema(description="the new phone-number-identity registration ID for each enabled device on the account, including this one")
|
||||
@Nullable Map<Long, Integer> pniRegistrationIds) {
|
||||
|
||||
@AssertTrue
|
||||
public boolean isSignatureValidOnEachSignedPreKey() {
|
||||
List<SignedPreKey> spks = new ArrayList<>();
|
||||
List<SignedPreKey<?>> spks = new ArrayList<>();
|
||||
if (devicePniSignedPrekeys != null) {
|
||||
spks.addAll(devicePniSignedPrekeys.values());
|
||||
}
|
||||
|
||||
@@ -4,9 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record DeviceActivationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
@@ -14,28 +11,28 @@ public record DeviceActivationRequest(@Schema(requiredMode = Schema.RequiredMode
|
||||
will be created "atomically," and all other properties needed for atomic account
|
||||
creation must also be present.
|
||||
""")
|
||||
Optional<@Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> aciSignedPreKey,
|
||||
Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed EC pre-key to be associated with this account's PNI. If provided, an account
|
||||
will be created "atomically," and all other properties needed for atomic account
|
||||
creation must also be present.
|
||||
""")
|
||||
Optional<@Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> pniSignedPreKey,
|
||||
Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's ACI. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
Optional<@Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> aciPqLastResortPreKey,
|
||||
Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
A signed Kyber-1024 "last resort" pre-key to be associated with this account's PNI. If
|
||||
provided, an account will be created "atomically," and all other properties needed for
|
||||
atomic account creation must also be present.
|
||||
""")
|
||||
Optional<@Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> pniPqLastResortPreKey,
|
||||
Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
|
||||
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
An APNs token set for the account's primary device. If provided, the account's primary
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecuregcm.util.ECPublicKeyAdapter;
|
||||
|
||||
public record ECPreKey(long keyId,
|
||||
@JsonSerialize(using = ECPublicKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = ECPublicKeyAdapter.Deserializer.class)
|
||||
ECPublicKey publicKey) implements PreKey<ECPublicKey> {
|
||||
|
||||
@Override
|
||||
public byte[] serializedPublicKey() {
|
||||
return publicKey().serialize();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ECPublicKeyAdapter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public record ECSignedPreKey(long keyId,
|
||||
|
||||
@JsonSerialize(using = ECPublicKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = ECPublicKeyAdapter.Deserializer.class)
|
||||
ECPublicKey publicKey,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
byte[] signature) implements SignedPreKey<ECPublicKey> {
|
||||
|
||||
@Override
|
||||
public byte[] serializedPublicKey() {
|
||||
return publicKey().serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
ECSignedPreKey that = (ECSignedPreKey) o;
|
||||
return keyId == that.keyId && publicKey.equals(that.publicKey) && Arrays.equals(signature, that.signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(keyId, publicKey);
|
||||
result = 31 * result + Arrays.hashCode(signature);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.KEMPublicKeyAdapter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public record KEMSignedPreKey(long keyId,
|
||||
|
||||
@JsonSerialize(using = KEMPublicKeyAdapter.Serializer.class)
|
||||
@JsonDeserialize(using = KEMPublicKeyAdapter.Deserializer.class)
|
||||
KEMPublicKey publicKey,
|
||||
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
byte[] signature) implements SignedPreKey<KEMPublicKey> {
|
||||
|
||||
@Override
|
||||
public byte[] serializedPublicKey() {
|
||||
return publicKey().serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
KEMSignedPreKey that = (KEMSignedPreKey) o;
|
||||
return keyId == that.keyId && publicKey.equals(that.publicKey) && Arrays.equals(signature, that.signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(keyId, publicKey);
|
||||
result = 31 * result + Arrays.hashCode(signature);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,10 @@ public record LinkDeviceRequest(@Schema(requiredMode = Schema.RequiredMode.REQUI
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public LinkDeviceRequest(@JsonProperty("verificationCode") String verificationCode,
|
||||
@JsonProperty("accountAttributes") AccountAttributes accountAttributes,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid SignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid SignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid SignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid SignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
|
||||
|
||||
public record PhoneNumberIdentityKeyDistributionRequest(
|
||||
@NotNull
|
||||
@@ -37,7 +35,7 @@ public record PhoneNumberIdentityKeyDistributionRequest(
|
||||
@Schema(description="""
|
||||
A new signed elliptic-curve prekey for each enabled device on the account, including this one.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
Map<Long, @NotNull @Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> devicePniSignedPrekeys,
|
||||
Map<Long, @NotNull @Valid ECSignedPreKey> devicePniSignedPrekeys,
|
||||
|
||||
@Schema(description="""
|
||||
A new signed post-quantum last-resort prekey for each enabled device on the account, including this one.
|
||||
@@ -45,7 +43,7 @@ public record PhoneNumberIdentityKeyDistributionRequest(
|
||||
If present, must contain one prekey per enabled device including this one.
|
||||
Prekeys for devices that did not previously have any post-quantum prekeys stored will be silently dropped.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@Valid Map<Long, @NotNull @Valid @ValidPreKey(type=PreKeyType.KYBER) SignedPreKey> devicePniPqLastResortPrekeys,
|
||||
@Valid Map<Long, @NotNull @Valid KEMSignedPreKey> devicePniPqLastResortPrekeys,
|
||||
|
||||
@NotNull
|
||||
@Valid
|
||||
@@ -54,7 +52,7 @@ public record PhoneNumberIdentityKeyDistributionRequest(
|
||||
|
||||
@AssertTrue
|
||||
public boolean isSignatureValidOnEachSignedPreKey() {
|
||||
List<SignedPreKey> spks = new ArrayList<>(devicePniSignedPrekeys.values());
|
||||
List<SignedPreKey<?>> spks = new ArrayList<>(devicePniSignedPrekeys.values());
|
||||
if (devicePniPqLastResortPrekeys != null) {
|
||||
spks.addAll(devicePniPqLastResortPrekeys.values());
|
||||
}
|
||||
|
||||
@@ -1,68 +1,15 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
public interface PreKey<K> {
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
long keyId();
|
||||
|
||||
public class PreKey {
|
||||
K publicKey();
|
||||
|
||||
@JsonProperty
|
||||
@NotNull
|
||||
private long keyId;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@NotEmpty
|
||||
private byte[] publicKey;
|
||||
|
||||
public PreKey() {}
|
||||
|
||||
public PreKey(long keyId, byte[] publicKey)
|
||||
{
|
||||
this.keyId = keyId;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public void setPublicKey(byte[] publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public long getKeyId() {
|
||||
return keyId;
|
||||
}
|
||||
|
||||
public void setKeyId(long keyId) {
|
||||
this.keyId = keyId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PreKey preKey = (PreKey) o;
|
||||
return keyId == preKey.keyId && Arrays.equals(publicKey, preKey.publicKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(keyId);
|
||||
result = 31 * result + Arrays.hashCode(publicKey);
|
||||
return result;
|
||||
}
|
||||
byte[] serializedPublicKey();
|
||||
}
|
||||
|
||||
@@ -20,20 +20,20 @@ public class PreKeyResponseItem {
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description="the signed elliptic-curve prekey for the device, if one has been set")
|
||||
private SignedPreKey signedPreKey;
|
||||
private ECSignedPreKey signedPreKey;
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description="an unsigned elliptic-curve prekey for the device, if any remain")
|
||||
private PreKey preKey;
|
||||
private ECPreKey preKey;
|
||||
|
||||
@JsonProperty
|
||||
@Schema(description="a signed post-quantum prekey for the device " +
|
||||
"(a one-time prekey if any remain, otherwise the last-resort prekey if one has been set)")
|
||||
private SignedPreKey pqPreKey;
|
||||
private KEMSignedPreKey pqPreKey;
|
||||
|
||||
public PreKeyResponseItem() {}
|
||||
|
||||
public PreKeyResponseItem(long deviceId, int registrationId, SignedPreKey signedPreKey, PreKey preKey, SignedPreKey pqPreKey) {
|
||||
public PreKeyResponseItem(long deviceId, int registrationId, ECSignedPreKey signedPreKey, ECPreKey preKey, KEMSignedPreKey pqPreKey) {
|
||||
this.deviceId = deviceId;
|
||||
this.registrationId = registrationId;
|
||||
this.signedPreKey = signedPreKey;
|
||||
@@ -42,17 +42,17 @@ public class PreKeyResponseItem {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public SignedPreKey getSignedPreKey() {
|
||||
public ECSignedPreKey getSignedPreKey() {
|
||||
return signedPreKey;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public PreKey getPreKey() {
|
||||
public ECPreKey getPreKey() {
|
||||
return preKey;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public SignedPreKey getPqPreKey() {
|
||||
public KEMSignedPreKey getPqPreKey() {
|
||||
return pqPreKey;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,19 +15,13 @@ public abstract class PreKeySignatureValidator {
|
||||
public static final Counter INVALID_SIGNATURE_COUNTER =
|
||||
Metrics.counter(name(PreKeySignatureValidator.class, "invalidPreKeySignature"));
|
||||
|
||||
public static boolean validatePreKeySignatures(final IdentityKey identityKey, final Collection<SignedPreKey> spks) {
|
||||
try {
|
||||
final boolean success = spks.stream()
|
||||
.allMatch(spk -> identityKey.getPublicKey().verifySignature(spk.getPublicKey(), spk.getSignature()));
|
||||
public static boolean validatePreKeySignatures(final IdentityKey identityKey, final Collection<SignedPreKey<?>> spks) {
|
||||
final boolean success = spks.stream().allMatch(spk -> spk.signatureValid(identityKey));
|
||||
|
||||
if (!success) {
|
||||
INVALID_SIGNATURE_COUNTER.increment();
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (final IllegalArgumentException e) {
|
||||
if (!success) {
|
||||
INVALID_SIGNATURE_COUNTER.increment();
|
||||
return false;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
|
||||
|
||||
public class PreKeyState {
|
||||
|
||||
@@ -26,16 +24,15 @@ public class PreKeyState {
|
||||
@Schema(description="A list of unsigned elliptic-curve prekeys to use for this device. " +
|
||||
"If present and not empty, replaces all stored unsigned EC prekeys for the device; " +
|
||||
"if absent or empty, any stored unsigned EC prekeys for the device are not deleted.")
|
||||
private List<@ValidPreKey(type=PreKeyType.ECC) PreKey> preKeys;
|
||||
private List<@Valid ECPreKey> preKeys;
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@ValidPreKey(type=PreKeyType.ECC)
|
||||
@Schema(description="An optional signed elliptic-curve prekey to use for this device. " +
|
||||
"If present, replaces the stored signed elliptic-curve prekey for the device; " +
|
||||
"if absent, the stored signed prekey is not deleted. " +
|
||||
"If present, must have a valid signature from the identity key in this request.")
|
||||
private SignedPreKey signedPreKey;
|
||||
private ECSignedPreKey signedPreKey;
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@@ -43,16 +40,15 @@ public class PreKeyState {
|
||||
"Each key must have a valid signature from the identity key in this request. " +
|
||||
"If present and not empty, replaces all stored unsigned PQ prekeys for the device; " +
|
||||
"if absent or empty, any stored unsigned PQ prekeys for the device are not deleted.")
|
||||
private List<@ValidPreKey(type=PreKeyType.KYBER) SignedPreKey> pqPreKeys;
|
||||
private List<@Valid KEMSignedPreKey> pqPreKeys;
|
||||
|
||||
@JsonProperty
|
||||
@Valid
|
||||
@ValidPreKey(type=PreKeyType.KYBER)
|
||||
@Schema(description="An optional signed last-resort post-quantum prekey to use for this device. " +
|
||||
"If present, replaces the stored signed post-quantum last-resort prekey for the device; " +
|
||||
"if absent, a stored last-resort prekey will *not* be deleted. " +
|
||||
"If present, must have a valid signature from the identity key in this request.")
|
||||
private SignedPreKey pqLastResortPreKey;
|
||||
private KEMSignedPreKey pqLastResortPreKey;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
|
||||
@@ -67,12 +63,12 @@ public class PreKeyState {
|
||||
public PreKeyState() {}
|
||||
|
||||
@VisibleForTesting
|
||||
public PreKeyState(IdentityKey identityKey, SignedPreKey signedPreKey, List<PreKey> keys) {
|
||||
public PreKeyState(IdentityKey identityKey, ECSignedPreKey signedPreKey, List<ECPreKey> keys) {
|
||||
this(identityKey, signedPreKey, keys, null, null);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public PreKeyState(IdentityKey identityKey, SignedPreKey signedPreKey, List<PreKey> keys, List<SignedPreKey> pqKeys, SignedPreKey pqLastResortKey) {
|
||||
public PreKeyState(IdentityKey identityKey, ECSignedPreKey signedPreKey, List<ECPreKey> keys, List<KEMSignedPreKey> pqKeys, KEMSignedPreKey pqLastResortKey) {
|
||||
this.identityKey = identityKey;
|
||||
this.signedPreKey = signedPreKey;
|
||||
this.preKeys = keys;
|
||||
@@ -80,19 +76,19 @@ public class PreKeyState {
|
||||
this.pqLastResortPreKey = pqLastResortKey;
|
||||
}
|
||||
|
||||
public List<PreKey> getPreKeys() {
|
||||
public List<ECPreKey> getPreKeys() {
|
||||
return preKeys;
|
||||
}
|
||||
|
||||
public SignedPreKey getSignedPreKey() {
|
||||
public ECSignedPreKey getSignedPreKey() {
|
||||
return signedPreKey;
|
||||
}
|
||||
|
||||
public List<SignedPreKey> getPqPreKeys() {
|
||||
public List<KEMSignedPreKey> getPqPreKeys() {
|
||||
return pqPreKeys;
|
||||
}
|
||||
|
||||
public SignedPreKey getPqLastResortPreKey() {
|
||||
public KEMSignedPreKey getPqLastResortPreKey() {
|
||||
return pqLastResortPreKey;
|
||||
}
|
||||
|
||||
@@ -102,7 +98,7 @@ public class PreKeyState {
|
||||
|
||||
@AssertTrue
|
||||
public boolean isSignatureValidOnEachSignedKey() {
|
||||
List<SignedPreKey> spks = new ArrayList<>();
|
||||
List<SignedPreKey<?>> spks = new ArrayList<>();
|
||||
if (pqPreKeys != null) {
|
||||
spks.addAll(pqPreKeys);
|
||||
}
|
||||
|
||||
@@ -20,8 +20,6 @@ import javax.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.OptionalIdentityKeyAdapter;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
|
||||
|
||||
public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
|
||||
The ID of an existing verification session as it appears in a verification session
|
||||
@@ -82,10 +80,10 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
|
||||
@JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer,
|
||||
@JsonProperty("aciIdentityKey") Optional<IdentityKey> aciIdentityKey,
|
||||
@JsonProperty("pniIdentityKey") Optional<IdentityKey> pniIdentityKey,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid @ValidPreKey(type=PreKeyType.ECC) SignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid @ValidPreKey(type=PreKeyType.KYBER) SignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid @ValidPreKey(type=PreKeyType.KYBER) SignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("aciSignedPreKey") Optional<@Valid ECSignedPreKey> aciSignedPreKey,
|
||||
@JsonProperty("pniSignedPreKey") Optional<@Valid ECSignedPreKey> pniSignedPreKey,
|
||||
@JsonProperty("aciPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> aciPqLastResortPreKey,
|
||||
@JsonProperty("pniPqLastResortPreKey") Optional<@Valid KEMSignedPreKey> pniPqLastResortPreKey,
|
||||
@JsonProperty("apnToken") Optional<@Valid ApnRegistrationId> apnToken,
|
||||
@JsonProperty("gcmToken") Optional<@Valid GcmRegistrationId> gcmToken) {
|
||||
|
||||
@@ -106,7 +104,7 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
private static boolean validatePreKeySignature(final Optional<IdentityKey> maybeIdentityKey,
|
||||
final Optional<SignedPreKey> maybeSignedPreKey) {
|
||||
final Optional<? extends SignedPreKey<?>> maybeSignedPreKey) {
|
||||
|
||||
return maybeSignedPreKey.map(signedPreKey -> maybeIdentityKey
|
||||
.map(identityKey -> PreKeySignatureValidator.validatePreKeySignatures(identityKey, List.of(signedPreKey)))
|
||||
|
||||
@@ -1,50 +1,17 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.entities;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.util.Arrays;
|
||||
public interface SignedPreKey<K> extends PreKey<K> {
|
||||
|
||||
public class SignedPreKey extends PreKey {
|
||||
byte[] signature();
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
|
||||
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
|
||||
@NotEmpty
|
||||
private byte[] signature;
|
||||
|
||||
public SignedPreKey() {}
|
||||
|
||||
public SignedPreKey(long keyId, byte[] publicKey, byte[] signature) {
|
||||
super(keyId, publicKey);
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
if (!super.equals(o)) return false;
|
||||
SignedPreKey that = (SignedPreKey) o;
|
||||
return Arrays.equals(signature, that.signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = super.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(signature);
|
||||
return result;
|
||||
default boolean signatureValid(final IdentityKey identityKey) {
|
||||
return identityKey.getPublicKey().verifySignature(serializedPublicKey(), signature());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,8 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.push.ClientPresenceManager;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisCluster;
|
||||
@@ -258,8 +259,8 @@ public class AccountsManager {
|
||||
public Account changeNumber(final Account account,
|
||||
final String targetNumber,
|
||||
@Nullable final IdentityKey pniIdentityKey,
|
||||
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
|
||||
@Nullable final Map<Long, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
@Nullable final Map<Long, Integer> pniRegistrationIds) throws InterruptedException, MismatchedDevicesException {
|
||||
|
||||
final String originalNumber = account.getNumber();
|
||||
@@ -350,8 +351,8 @@ public class AccountsManager {
|
||||
|
||||
public Account updatePniKeys(final Account account,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Map<Long, SignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
|
||||
final Map<Long, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
final Map<Long, Integer> pniRegistrationIds) throws MismatchedDevicesException {
|
||||
validateDevices(account, pniSignedPreKeys, pniPqLastResortPreKeys, pniRegistrationIds);
|
||||
|
||||
@@ -369,7 +370,7 @@ public class AccountsManager {
|
||||
|
||||
private boolean setPniKeys(final Account account,
|
||||
@Nullable final IdentityKey pniIdentityKey,
|
||||
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, Integer> pniRegistrationIds) {
|
||||
if (ObjectUtils.allNull(pniIdentityKey, pniSignedPreKeys, pniRegistrationIds)) {
|
||||
return false;
|
||||
@@ -383,7 +384,7 @@ public class AccountsManager {
|
||||
if (!device.isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
SignedPreKey signedPreKey = pniSignedPreKeys.get(device.getId());
|
||||
ECSignedPreKey signedPreKey = pniSignedPreKeys.get(device.getId());
|
||||
int registrationId = pniRegistrationIds.get(device.getId());
|
||||
changed = changed ||
|
||||
!signedPreKey.equals(device.getPhoneNumberIdentitySignedPreKey()) ||
|
||||
@@ -398,8 +399,8 @@ public class AccountsManager {
|
||||
}
|
||||
|
||||
private void validateDevices(final Account account,
|
||||
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
|
||||
@Nullable final Map<Long, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Long, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
@Nullable final Map<Long, Integer> pniRegistrationIds) throws MismatchedDevicesException {
|
||||
if (pniSignedPreKeys == null && pniRegistrationIds == null) {
|
||||
return;
|
||||
|
||||
@@ -20,9 +20,10 @@ import org.whispersystems.textsecuregcm.controllers.AccountController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MessageController;
|
||||
import org.whispersystems.textsecuregcm.controllers.MismatchedDevicesException;
|
||||
import org.whispersystems.textsecuregcm.controllers.StaleDevicesException;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.IncomingMessage;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.push.MessageSender;
|
||||
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
|
||||
import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator;
|
||||
@@ -41,8 +42,8 @@ public class ChangeNumberManager {
|
||||
|
||||
public Account changeNumber(final Account account, final String number,
|
||||
@Nullable final IdentityKey pniIdentityKey,
|
||||
@Nullable final Map<Long, SignedPreKey> deviceSignedPreKeys,
|
||||
@Nullable final Map<Long, SignedPreKey> devicePqLastResortPreKeys,
|
||||
@Nullable final Map<Long, ECSignedPreKey> deviceSignedPreKeys,
|
||||
@Nullable final Map<Long, KEMSignedPreKey> devicePqLastResortPreKeys,
|
||||
@Nullable final List<IncomingMessage> deviceMessages,
|
||||
@Nullable final Map<Long, Integer> pniRegistrationIds)
|
||||
throws InterruptedException, MismatchedDevicesException, StaleDevicesException {
|
||||
@@ -81,8 +82,8 @@ public class ChangeNumberManager {
|
||||
|
||||
public Account updatePniKeys(final Account account,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Map<Long, SignedPreKey> deviceSignedPreKeys,
|
||||
@Nullable final Map<Long, SignedPreKey> devicePqLastResortPreKeys,
|
||||
final Map<Long, ECSignedPreKey> deviceSignedPreKeys,
|
||||
@Nullable final Map<Long, KEMSignedPreKey> devicePqLastResortPreKeys,
|
||||
final List<IncomingMessage> deviceMessages,
|
||||
final Map<Long, Integer> pniRegistrationIds) throws MismatchedDevicesException, StaleDevicesException {
|
||||
validateDeviceMessages(account, deviceMessages);
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.stream.Collectors;
|
||||
import java.util.stream.LongStream;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.ECSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.Util;
|
||||
|
||||
public class Device {
|
||||
@@ -61,10 +61,10 @@ public class Device {
|
||||
private Integer phoneNumberIdentityRegistrationId;
|
||||
|
||||
@JsonProperty
|
||||
private SignedPreKey signedPreKey;
|
||||
private ECSignedPreKey signedPreKey;
|
||||
|
||||
@JsonProperty("pniSignedPreKey")
|
||||
private SignedPreKey phoneNumberIdentitySignedPreKey;
|
||||
private ECSignedPreKey phoneNumberIdentitySignedPreKey;
|
||||
|
||||
@JsonProperty
|
||||
private long lastSeen;
|
||||
@@ -230,19 +230,19 @@ public class Device {
|
||||
this.phoneNumberIdentityRegistrationId = phoneNumberIdentityRegistrationId;
|
||||
}
|
||||
|
||||
public SignedPreKey getSignedPreKey() {
|
||||
public ECSignedPreKey getSignedPreKey() {
|
||||
return signedPreKey;
|
||||
}
|
||||
|
||||
public void setSignedPreKey(SignedPreKey signedPreKey) {
|
||||
public void setSignedPreKey(ECSignedPreKey signedPreKey) {
|
||||
this.signedPreKey = signedPreKey;
|
||||
}
|
||||
|
||||
public SignedPreKey getPhoneNumberIdentitySignedPreKey() {
|
||||
public ECSignedPreKey getPhoneNumberIdentitySignedPreKey() {
|
||||
return phoneNumberIdentitySignedPreKey;
|
||||
}
|
||||
|
||||
public void setPhoneNumberIdentitySignedPreKey(final SignedPreKey phoneNumberIdentitySignedPreKey) {
|
||||
public void setPhoneNumberIdentitySignedPreKey(final ECSignedPreKey phoneNumberIdentitySignedPreKey) {
|
||||
this.phoneNumberIdentitySignedPreKey = phoneNumberIdentitySignedPreKey;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,15 +13,15 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import javax.annotation.Nullable;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
|
||||
public class KeysManager {
|
||||
|
||||
private final SingleUseECPreKeyStore ecPreKeys;
|
||||
private final SingleUseKEMPreKeyStore pqPreKeys;
|
||||
private final RepeatedUseSignedPreKeyStore pqLastResortKeys;
|
||||
private final RepeatedUseKEMSignedPreKeyStore pqLastResortKeys;
|
||||
|
||||
public KeysManager(
|
||||
final DynamoDbAsyncClient dynamoDbAsyncClient,
|
||||
@@ -30,18 +30,18 @@ public class KeysManager {
|
||||
final String pqLastResortTableName) {
|
||||
this.ecPreKeys = new SingleUseECPreKeyStore(dynamoDbAsyncClient, ecTableName);
|
||||
this.pqPreKeys = new SingleUseKEMPreKeyStore(dynamoDbAsyncClient, pqTableName);
|
||||
this.pqLastResortKeys = new RepeatedUseSignedPreKeyStore(dynamoDbAsyncClient, pqLastResortTableName);
|
||||
this.pqLastResortKeys = new RepeatedUseKEMSignedPreKeyStore(dynamoDbAsyncClient, pqLastResortTableName);
|
||||
}
|
||||
|
||||
public void store(final UUID identifier, final long deviceId, final List<PreKey> keys) {
|
||||
public void store(final UUID identifier, final long deviceId, final List<ECPreKey> keys) {
|
||||
store(identifier, deviceId, keys, null, null);
|
||||
}
|
||||
|
||||
public void store(
|
||||
final UUID identifier, final long deviceId,
|
||||
@Nullable final List<PreKey> ecKeys,
|
||||
@Nullable final List<SignedPreKey> pqKeys,
|
||||
@Nullable final SignedPreKey pqLastResortKey) {
|
||||
@Nullable final List<ECPreKey> ecKeys,
|
||||
@Nullable final List<KEMSignedPreKey> pqKeys,
|
||||
@Nullable final KEMSignedPreKey pqLastResortKey) {
|
||||
|
||||
final List<CompletableFuture<Void>> storeFutures = new ArrayList<>();
|
||||
|
||||
@@ -60,15 +60,15 @@ public class KeysManager {
|
||||
CompletableFuture.allOf(storeFutures.toArray(new CompletableFuture[0])).join();
|
||||
}
|
||||
|
||||
public void storePqLastResort(final UUID identifier, final Map<Long, SignedPreKey> keys) {
|
||||
public void storePqLastResort(final UUID identifier, final Map<Long, KEMSignedPreKey> keys) {
|
||||
pqLastResortKeys.store(identifier, keys).join();
|
||||
}
|
||||
|
||||
public Optional<PreKey> takeEC(final UUID identifier, final long deviceId) {
|
||||
public Optional<ECPreKey> takeEC(final UUID identifier, final long deviceId) {
|
||||
return ecPreKeys.take(identifier, deviceId).join();
|
||||
}
|
||||
|
||||
public Optional<SignedPreKey> takePQ(final UUID identifier, final long deviceId) {
|
||||
public Optional<KEMSignedPreKey> takePQ(final UUID identifier, final long deviceId) {
|
||||
return pqPreKeys.take(identifier, deviceId)
|
||||
.thenCompose(maybeSingleUsePreKey -> maybeSingleUsePreKey
|
||||
.map(singleUsePreKey -> CompletableFuture.completedFuture(maybeSingleUsePreKey))
|
||||
@@ -76,9 +76,8 @@ public class KeysManager {
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Optional<PreKey> getLastResort(final UUID identifier, final long deviceId) {
|
||||
return pqLastResortKeys.find(identifier, deviceId).join()
|
||||
.map(signedPreKey -> signedPreKey);
|
||||
Optional<KEMSignedPreKey> getLastResort(final UUID identifier, final long deviceId) {
|
||||
return pqLastResortKeys.find(identifier, deviceId).join();
|
||||
}
|
||||
|
||||
public List<Long> getPqEnabledDevices(final UUID identifier) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RepeatedUseKEMSignedPreKeyStore extends RepeatedUseSignedPreKeyStore<KEMSignedPreKey> {
|
||||
|
||||
public RepeatedUseKEMSignedPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) {
|
||||
super(dynamoDbAsyncClient, tableName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, AttributeValue> getItemFromPreKey(final UUID accountUuid, final long deviceId, final KEMSignedPreKey signedPreKey) {
|
||||
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, getPartitionKey(accountUuid),
|
||||
KEY_DEVICE_ID, getSortKey(deviceId),
|
||||
ATTR_KEY_ID, AttributeValues.fromLong(signedPreKey.keyId()),
|
||||
ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.serializedPublicKey()),
|
||||
ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.signature()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected KEMSignedPreKey getPreKeyFromItem(final Map<String, AttributeValue> item) {
|
||||
try {
|
||||
return new KEMSignedPreKey(
|
||||
Long.parseLong(item.get(ATTR_KEY_ID).n()),
|
||||
new KEMPublicKey(item.get(ATTR_PUBLIC_KEY).b().asByteArray()),
|
||||
item.get(ATTR_SIGNATURE).b().asByteArray());
|
||||
} catch (final InvalidKeyException e) {
|
||||
// This should never happen since we're serializing keys directly from `KEMPublicKey` instances on the way in
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,12 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import io.micrometer.core.instrument.Timer;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
@@ -37,7 +34,7 @@ import software.amazon.awssdk.services.dynamodb.model.TransactWriteItemsRequest;
|
||||
* Each {@link Account} may have one or more {@link Device devices}. Each "active" (i.e. those that have completed
|
||||
* provisioning and are capable of sending and receiving messages) must have exactly one "last resort" pre-key.
|
||||
*/
|
||||
public class RepeatedUseSignedPreKeyStore {
|
||||
public abstract class RepeatedUseSignedPreKeyStore<K extends SignedPreKey<?>> {
|
||||
|
||||
private final DynamoDbAsyncClient dynamoDbAsyncClient;
|
||||
private final String tableName;
|
||||
@@ -63,9 +60,6 @@ public class RepeatedUseSignedPreKeyStore {
|
||||
private static final String FIND_KEY_TIMER_NAME = MetricsUtil.name(RepeatedUseSignedPreKeyStore.class, "findKey");
|
||||
private static final String KEY_PRESENT_TAG_NAME = "keyPresent";
|
||||
|
||||
private static final Counter INVALID_KEY_COUNTER =
|
||||
Metrics.counter(MetricsUtil.name(RepeatedUseSignedPreKeyStore.class, "invalidKey"));
|
||||
|
||||
public RepeatedUseSignedPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) {
|
||||
this.dynamoDbAsyncClient = dynamoDbAsyncClient;
|
||||
this.tableName = tableName;
|
||||
@@ -81,7 +75,7 @@ public class RepeatedUseSignedPreKeyStore {
|
||||
*
|
||||
* @return a future that completes once the key has been stored
|
||||
*/
|
||||
public CompletableFuture<Void> store(final UUID identifier, final long deviceId, final SignedPreKey signedPreKey) {
|
||||
public CompletableFuture<Void> store(final UUID identifier, final long deviceId, final K signedPreKey) {
|
||||
final Timer.Sample sample = Timer.start();
|
||||
|
||||
return dynamoDbAsyncClient.putItem(PutItemRequest.builder()
|
||||
@@ -101,14 +95,14 @@ public class RepeatedUseSignedPreKeyStore {
|
||||
*
|
||||
* @return a future that completes once all keys have been stored
|
||||
*/
|
||||
public CompletableFuture<Void> store(final UUID identifier, final Map<Long, SignedPreKey> signedPreKeysByDeviceId) {
|
||||
public CompletableFuture<Void> store(final UUID identifier, final Map<Long, K> signedPreKeysByDeviceId) {
|
||||
final Timer.Sample sample = Timer.start();
|
||||
|
||||
return dynamoDbAsyncClient.transactWriteItems(TransactWriteItemsRequest.builder()
|
||||
.transactItems(signedPreKeysByDeviceId.entrySet().stream()
|
||||
.map(entry -> {
|
||||
final long deviceId = entry.getKey();
|
||||
final SignedPreKey signedPreKey = entry.getValue();
|
||||
final K signedPreKey = entry.getValue();
|
||||
|
||||
return TransactWriteItem.builder()
|
||||
.put(Put.builder()
|
||||
@@ -131,10 +125,10 @@ public class RepeatedUseSignedPreKeyStore {
|
||||
* @return a future that yields an optional signed pre-key if one is available for the target device or empty if no
|
||||
* key could be found for the target device
|
||||
*/
|
||||
public CompletableFuture<Optional<SignedPreKey>> find(final UUID identifier, final long deviceId) {
|
||||
public CompletableFuture<Optional<K>> find(final UUID identifier, final long deviceId) {
|
||||
final Timer.Sample sample = Timer.start();
|
||||
|
||||
final CompletableFuture<Optional<SignedPreKey>> findFuture = dynamoDbAsyncClient.getItem(GetItemRequest.builder()
|
||||
final CompletableFuture<Optional<K>> findFuture = dynamoDbAsyncClient.getItem(GetItemRequest.builder()
|
||||
.tableName(tableName)
|
||||
.key(getPrimaryKey(identifier, deviceId))
|
||||
.consistentRead(true)
|
||||
@@ -202,41 +196,21 @@ public class RepeatedUseSignedPreKeyStore {
|
||||
.map(item -> Long.parseLong(item.get(KEY_DEVICE_ID).n()));
|
||||
}
|
||||
|
||||
private static Map<String, AttributeValue> getPrimaryKey(final UUID identifier, final long deviceId) {
|
||||
protected static Map<String, AttributeValue> getPrimaryKey(final UUID identifier, final long deviceId) {
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, getPartitionKey(identifier),
|
||||
KEY_DEVICE_ID, getSortKey(deviceId));
|
||||
}
|
||||
|
||||
private static AttributeValue getPartitionKey(final UUID accountUuid) {
|
||||
protected static AttributeValue getPartitionKey(final UUID accountUuid) {
|
||||
return AttributeValues.fromUUID(accountUuid);
|
||||
}
|
||||
|
||||
private static AttributeValue getSortKey(final long deviceId) {
|
||||
protected static AttributeValue getSortKey(final long deviceId) {
|
||||
return AttributeValues.fromLong(deviceId);
|
||||
}
|
||||
|
||||
private static Map<String, AttributeValue> getItemFromPreKey(final UUID accountUuid, final long deviceId, final SignedPreKey signedPreKey) {
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, getPartitionKey(accountUuid),
|
||||
KEY_DEVICE_ID, getSortKey(deviceId),
|
||||
ATTR_KEY_ID, AttributeValues.fromLong(signedPreKey.getKeyId()),
|
||||
ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.getPublicKey()),
|
||||
ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.getSignature()));
|
||||
}
|
||||
protected abstract Map<String, AttributeValue> getItemFromPreKey(final UUID accountUuid, final long deviceId, final K signedPreKey);
|
||||
|
||||
private static SignedPreKey getPreKeyFromItem(final Map<String, AttributeValue> item) {
|
||||
final byte[] publicKeyBytes = item.get(ATTR_PUBLIC_KEY).b().asByteArray();
|
||||
|
||||
try {
|
||||
new KEMPublicKey(publicKeyBytes);
|
||||
} catch (final InvalidKeyException e) {
|
||||
INVALID_KEY_COUNTER.increment();
|
||||
}
|
||||
|
||||
return new SignedPreKey(
|
||||
Long.parseLong(item.get(ATTR_KEY_ID).n()),
|
||||
publicKeyBytes,
|
||||
item.get(ATTR_SIGNATURE).b().asByteArray());
|
||||
}
|
||||
protected abstract K getPreKeyFromItem(final Map<String, AttributeValue> item);
|
||||
}
|
||||
|
||||
@@ -5,46 +5,39 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.entities.ECPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SingleUseECPreKeyStore extends SingleUsePreKeyStore<PreKey> {
|
||||
|
||||
private static final Counter INVALID_KEY_COUNTER =
|
||||
Metrics.counter(MetricsUtil.name(SingleUseECPreKeyStore.class, "invalidKey"));
|
||||
public class SingleUseECPreKeyStore extends SingleUsePreKeyStore<ECPreKey> {
|
||||
|
||||
protected SingleUseECPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) {
|
||||
super(dynamoDbAsyncClient, tableName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, AttributeValue> getItemFromPreKey(final UUID identifier, final long deviceId, final PreKey preKey) {
|
||||
protected Map<String, AttributeValue> getItemFromPreKey(final UUID identifier, final long deviceId, final ECPreKey preKey) {
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, getPartitionKey(identifier),
|
||||
KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, preKey.getKeyId()),
|
||||
ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(preKey.getPublicKey()));
|
||||
KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, preKey.keyId()),
|
||||
ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(preKey.serializedPublicKey()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PreKey getPreKeyFromItem(final Map<String, AttributeValue> item) {
|
||||
protected ECPreKey getPreKeyFromItem(final Map<String, AttributeValue> item) {
|
||||
final long keyId = item.get(KEY_DEVICE_ID_KEY_ID).b().asByteBuffer().getLong(8);
|
||||
final byte[] publicKey = extractByteArray(item.get(ATTR_PUBLIC_KEY));
|
||||
|
||||
try {
|
||||
new ECPublicKey(publicKey);
|
||||
return new ECPreKey(keyId, new ECPublicKey(publicKey));
|
||||
} catch (final InvalidKeyException e) {
|
||||
INVALID_KEY_COUNTER.increment();
|
||||
// This should never happen since we're serializing keys directly from `ECPublicKey` instances on the way in
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
return new PreKey(keyId, publicKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,48 +5,41 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import io.micrometer.core.instrument.Counter;
|
||||
import io.micrometer.core.instrument.Metrics;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public class SingleUseKEMPreKeyStore extends SingleUsePreKeyStore<SignedPreKey> {
|
||||
|
||||
private static final Counter INVALID_KEY_COUNTER =
|
||||
Metrics.counter(MetricsUtil.name(SingleUseKEMPreKeyStore.class, "invalidKey"));
|
||||
public class SingleUseKEMPreKeyStore extends SingleUsePreKeyStore<KEMSignedPreKey> {
|
||||
|
||||
protected SingleUseKEMPreKeyStore(final DynamoDbAsyncClient dynamoDbAsyncClient, final String tableName) {
|
||||
super(dynamoDbAsyncClient, tableName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, AttributeValue> getItemFromPreKey(final UUID identifier, final long deviceId, final SignedPreKey signedPreKey) {
|
||||
protected Map<String, AttributeValue> getItemFromPreKey(final UUID identifier, final long deviceId, final KEMSignedPreKey signedPreKey) {
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, getPartitionKey(identifier),
|
||||
KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, signedPreKey.getKeyId()),
|
||||
ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.getPublicKey()),
|
||||
ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.getSignature()));
|
||||
KEY_DEVICE_ID_KEY_ID, getSortKey(deviceId, signedPreKey.keyId()),
|
||||
ATTR_PUBLIC_KEY, AttributeValues.fromByteArray(signedPreKey.serializedPublicKey()),
|
||||
ATTR_SIGNATURE, AttributeValues.fromByteArray(signedPreKey.signature()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SignedPreKey getPreKeyFromItem(final Map<String, AttributeValue> item) {
|
||||
protected KEMSignedPreKey getPreKeyFromItem(final Map<String, AttributeValue> item) {
|
||||
final long keyId = item.get(KEY_DEVICE_ID_KEY_ID).b().asByteBuffer().getLong(8);
|
||||
final byte[] publicKey = extractByteArray(item.get(ATTR_PUBLIC_KEY));
|
||||
final byte[] signature = extractByteArray(item.get(ATTR_SIGNATURE));
|
||||
final byte[] publicKey = item.get(ATTR_PUBLIC_KEY).b().asByteArray();
|
||||
final byte[] signature = item.get(ATTR_SIGNATURE).b().asByteArray();
|
||||
|
||||
try {
|
||||
new KEMPublicKey(publicKey);
|
||||
return new KEMSignedPreKey(keyId, new KEMPublicKey(publicKey), signature);
|
||||
} catch (final InvalidKeyException e) {
|
||||
INVALID_KEY_COUNTER.increment();
|
||||
// This should never happen since we're serializing keys directly from `KEMPublicKey` instances on the way in
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
return new SignedPreKey(keyId, publicKey, signature);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ import software.amazon.awssdk.services.dynamodb.model.Select;
|
||||
* the event that a party wants to begin a session with a device that has no single-use pre-keys remaining, that party
|
||||
* may fall back to using the device's repeated-use ("last-resort") signed pre-key instead.
|
||||
*/
|
||||
public abstract class SingleUsePreKeyStore<K extends PreKey> {
|
||||
public abstract class SingleUsePreKeyStore<K extends PreKey<?>> {
|
||||
|
||||
private final DynamoDbAsyncClient dynamoDbAsyncClient;
|
||||
private final String tableName;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.ECPublicKey;
|
||||
|
||||
public class ECPublicKeyAdapter {
|
||||
|
||||
public static class Serializer extends JsonSerializer<ECPublicKey> {
|
||||
|
||||
@Override
|
||||
public void serialize(final ECPublicKey ecPublicKey,
|
||||
final JsonGenerator jsonGenerator,
|
||||
final SerializerProvider serializers) throws IOException {
|
||||
|
||||
jsonGenerator.writeString(Base64.getEncoder().encodeToString(ecPublicKey.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<ECPublicKey> {
|
||||
|
||||
@Override
|
||||
public ECPublicKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
|
||||
final byte[] ecPublicKeyBytes;
|
||||
|
||||
try {
|
||||
ecPublicKeyBytes = Base64.getDecoder().decode(parser.getValueAsString());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new JsonParseException(parser, "Could not parse EC public key as a base64-encoded value", e);
|
||||
}
|
||||
|
||||
try {
|
||||
return new ECPublicKey(ecPublicKeyBytes);
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new JsonParseException(parser, "Could not interpret key bytes as an EC public key", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class KEMPublicKeyAdapter {
|
||||
|
||||
public static class Serializer extends JsonSerializer<KEMPublicKey> {
|
||||
|
||||
@Override
|
||||
public void serialize(final KEMPublicKey kemPublicKey,
|
||||
final JsonGenerator jsonGenerator,
|
||||
final SerializerProvider serializers) throws IOException {
|
||||
|
||||
jsonGenerator.writeString(Base64.getEncoder().encodeToString(kemPublicKey.serialize()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<KEMPublicKey> {
|
||||
|
||||
@Override
|
||||
public KEMPublicKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
|
||||
final byte[] kemPublicKeyBytes;
|
||||
|
||||
try {
|
||||
kemPublicKeyBytes = Base64.getDecoder().decode(parser.getValueAsString());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new JsonParseException(parser, "Could not parse KEM public key as a base64-encoded value", e);
|
||||
}
|
||||
|
||||
try {
|
||||
return new KEMPublicKey(kemPublicKeyBytes);
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new JsonParseException(parser, "Could not interpret key bytes as a KEM public key", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import static java.lang.annotation.ElementType.FIELD;
|
||||
import static java.lang.annotation.ElementType.PARAMETER;
|
||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import javax.validation.Constraint;
|
||||
import javax.validation.Payload;
|
||||
|
||||
@Target({FIELD, PARAMETER, TYPE_USE})
|
||||
@Retention(RUNTIME)
|
||||
@Constraint(validatedBy = {ValidPreKeyValidator.class})
|
||||
@Documented
|
||||
public @interface ValidPreKey {
|
||||
|
||||
public enum PreKeyType {
|
||||
ECC,
|
||||
KYBER
|
||||
}
|
||||
|
||||
PreKeyType type();
|
||||
|
||||
String message() default "{org.whispersystems.textsecuregcm.util.ValidPreKey.message}";
|
||||
|
||||
Class<?>[] groups() default { };
|
||||
|
||||
Class<? extends Payload>[] payload() default { };
|
||||
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import javax.validation.ConstraintValidator;
|
||||
import javax.validation.ConstraintValidatorContext;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.ecc.Curve;
|
||||
import org.signal.libsignal.protocol.kem.KEMPublicKey;
|
||||
import org.whispersystems.textsecuregcm.entities.PreKey;
|
||||
|
||||
public class ValidPreKeyValidator implements ConstraintValidator<ValidPreKey, PreKey> {
|
||||
private ValidPreKey.PreKeyType type;
|
||||
|
||||
@Override
|
||||
public void initialize(ValidPreKey annotation) {
|
||||
type = annotation.type();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(PreKey value, ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
switch (type) {
|
||||
case ECC -> Curve.decodePoint(value.getPublicKey(), 0);
|
||||
case KYBER -> new KEMPublicKey(value.getPublicKey());
|
||||
}
|
||||
} catch (IllegalArgumentException | InvalidKeyException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user