Use strongly-typed pre-keys

This commit is contained in:
Jon Chambers
2023-06-09 10:08:49 -04:00
committed by GitHub
parent b27334b0ff
commit 17aa5d8e74
47 changed files with 805 additions and 719 deletions

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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());
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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)))

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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 { };
}

View File

@@ -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;
}
}