Represent identity keys as IdentityKey instances

This commit is contained in:
Jon Chambers
2023-06-08 11:36:58 -04:00
committed by GitHub
parent 1c8443210a
commit 234707169e
34 changed files with 390 additions and 263 deletions

View File

@@ -34,7 +34,7 @@ public class CertificateGenerator {
SenderCertificate.Certificate.Builder builder = SenderCertificate.Certificate.newBuilder()
.setSenderDevice(Math.toIntExact(device.getId()))
.setExpires(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(expiresDays))
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey()))
.setIdentityKey(ByteString.copyFrom(account.getIdentityKey().serialize()))
.setSigner(serverCertificate)
.setSenderUuid(account.getUuid().toString());

View File

@@ -32,7 +32,6 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.ArrayUtils;
import org.signal.libsignal.zkgroup.auth.ServerZkAuthOperations;
import org.signal.libsignal.zkgroup.calllinks.CallLinkAuthCredentialResponse;
import org.signal.libsignal.zkgroup.GenericServerSecretParams;
@@ -75,7 +74,7 @@ public class CertificateController {
@QueryParam("includeE164") @DefaultValue("true") boolean includeE164)
throws InvalidKeyException {
if (ArrayUtils.isEmpty(auth.getAccount().getIdentityKey())) {
if (auth.getAccount().getIdentityKey() == null) {
throw new WebApplicationException(Response.Status.BAD_REQUEST);
}

View File

@@ -11,14 +11,14 @@ import com.google.common.net.HttpHeaders;
import io.dropwizard.auth.Auth;
import io.micrometer.core.instrument.Metrics;
import io.micrometer.core.instrument.Tags;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import javax.validation.Valid;
@@ -35,8 +35,7 @@ import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.ArrayUtils;
import org.signal.libsignal.protocol.IdentityKey;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.AuthenticatedAccount;
import org.whispersystems.textsecuregcm.auth.ChangesDeviceEnabledState;
@@ -116,11 +115,11 @@ public class KeysController {
updateAccount = true;
}
final byte[] oldIdentityKey = usePhoneNumberIdentity ? account.getPhoneNumberIdentityKey() : account.getIdentityKey();
if (!Arrays.equals(preKeys.getIdentityKey(), oldIdentityKey)) {
final IdentityKey oldIdentityKey = usePhoneNumberIdentity ? account.getPhoneNumberIdentityKey() : account.getIdentityKey();
if (!Objects.equals(preKeys.getIdentityKey(), oldIdentityKey)) {
updateAccount = true;
final boolean hasIdentityKey = ArrayUtils.isNotEmpty(oldIdentityKey);
final boolean hasIdentityKey = oldIdentityKey != null;
final Tags tags = Tags.of(UserAgentTagUtil.getPlatformTag(userAgent))
.and(HAS_IDENTITY_KEY_TAG_NAME, String.valueOf(hasIdentityKey))
.and(IDENTITY_TYPE_TAG_NAME, usePhoneNumberIdentity ? "pni" : "aci");
@@ -221,7 +220,7 @@ public class KeysController {
}
}
final byte[] identityKey = usePhoneNumberIdentity ? target.getPhoneNumberIdentityKey() : target.getIdentityKey();
final IdentityKey identityKey = usePhoneNumberIdentity ? target.getPhoneNumberIdentityKey() : target.getIdentityKey();
if (responseItems.isEmpty()) {
return Response.status(404).build();

View File

@@ -64,6 +64,7 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredentialResponse;
@@ -383,14 +384,14 @@ public class ProfileController {
if (account.getIdentityKey() == null || account.getPhoneNumberIdentityKey() == null) {
return;
}
final byte[] identityKeyBytes =
final IdentityKey identityKey =
usePhoneNumberIdentity ? account.getPhoneNumberIdentityKey() : account.getIdentityKey();
md.reset();
byte[] digest = md.digest(identityKeyBytes);
byte[] digest = md.digest(identityKey.serialize());
byte[] fingerprint = Util.truncate(digest, 4);
if (!Arrays.equals(fingerprint, element.fingerprint())) {
responseElements.add(new BatchIdentityCheckResponse.Element(element.aci(), element.uuid(), identityKeyBytes));
responseElements.add(new BatchIdentityCheckResponse.Element(element.aci(), element.uuid(), identityKey));
}
});
}

View File

@@ -8,7 +8,9 @@ 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 org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
import java.util.List;
import java.util.UUID;
@@ -16,9 +18,9 @@ import java.util.UUID;
public class BaseProfileResponse {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
private byte[] identityKey;
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
private IdentityKey identityKey;
@JsonProperty
private String unidentifiedAccess;
@@ -38,7 +40,7 @@ public class BaseProfileResponse {
public BaseProfileResponse() {
}
public BaseProfileResponse(final byte[] identityKey,
public BaseProfileResponse(final IdentityKey identityKey,
final String unidentifiedAccess,
final boolean unrestrictedUnidentifiedAccess,
final UserCapabilities capabilities,
@@ -53,7 +55,7 @@ public class BaseProfileResponse {
this.uuid = uuid;
}
public byte[] getIdentityKey() {
public IdentityKey getIdentityKey() {
return identityKey;
}

View File

@@ -11,13 +11,25 @@ import java.util.UUID;
import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
public record BatchIdentityCheckResponse(@Valid List<Element> elements) {
public record Element(@Deprecated @JsonInclude(JsonInclude.Include.NON_EMPTY) @Nullable UUID aci,
@JsonInclude(JsonInclude.Include.NON_EMPTY) @Nullable UUID uuid,
@NotNull @ExactlySize(33) byte[] identityKey) {
public record Element(@Deprecated
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Nullable UUID aci,
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@Nullable UUID uuid,
@NotNull
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
IdentityKey identityKey) {
public Element {
if (aci == null && uuid == null) {

View File

@@ -7,6 +7,7 @@ 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 io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.List;
@@ -15,9 +16,10 @@ import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
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;
@@ -39,8 +41,9 @@ public record ChangeNumberRequest(
@JsonProperty("reglock") @Nullable String registrationLock,
@Schema(description="the new public identity key to use for the phone-number identity associated with the new phone number")
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotEmpty byte[] pniIdentityKey,
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
@NotNull IdentityKey pniIdentityKey,
@Schema(description="""
A list of synchronization messages to send to companion devices to supply the private keysManager

View File

@@ -7,17 +7,18 @@ 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 io.swagger.v3.oas.annotations.media.Schema;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.AssertTrue;
import javax.annotation.Nullable;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
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;
@@ -32,8 +33,9 @@ public record ChangePhoneNumberRequest(
@JsonProperty("reglock") @Nullable String registrationLock,
@Schema(description="the new public identity key to use for the phone-number identity associated with the new phone number")
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@Nullable byte[] pniIdentityKey,
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
@Nullable IdentityKey pniIdentityKey,
@Schema(description="""
A list of synchronization messages to send to companion devices to supply the private keysManager

View File

@@ -5,27 +5,24 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
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(
@NotEmpty
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
@Schema(description="the new identity key for this account's phone-number identity")
byte[] pniIdentityKey,
IdentityKey pniIdentityKey,
@NotNull
@Valid

View File

@@ -6,19 +6,21 @@ package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import io.swagger.v3.oas.annotations.media.Schema;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import java.util.List;
import org.signal.libsignal.protocol.IdentityKey;
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
public class PreKeyResponse {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
@Schema(description="the public identity key for the requested identity")
private byte[] identityKey;
private IdentityKey identityKey;
@JsonProperty
@Schema(description="information about each requested device")
@@ -26,13 +28,13 @@ public class PreKeyResponse {
public PreKeyResponse() {}
public PreKeyResponse(byte[] identityKey, List<PreKeyResponseItem> devices) {
public PreKeyResponse(IdentityKey identityKey, List<PreKeyResponseItem> devices) {
this.identityKey = identityKey;
this.devices = devices;
this.devices = devices;
}
@VisibleForTesting
public byte[] getIdentityKey() {
public IdentityKey getIdentityKey() {
return identityKey;
}

View File

@@ -9,27 +9,23 @@ import static com.codahale.metrics.MetricRegistry.name;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import java.util.Collection;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.protocol.IdentityKey;
public abstract class PreKeySignatureValidator {
public static final Counter INVALID_SIGNATURE_COUNTER =
Metrics.counter(name(PreKeySignatureValidator.class, "invalidPreKeySignature"));
public static boolean validatePreKeySignatures(final byte[] identityKeyBytes, final Collection<SignedPreKey> spks) {
public static boolean validatePreKeySignatures(final IdentityKey identityKey, final Collection<SignedPreKey> spks) {
try {
final ECPublicKey identityKey = Curve.decodePoint(identityKeyBytes, 0);
final boolean success = spks.stream()
.allMatch(spk -> identityKey.verifySignature(spk.getPublicKey(), spk.getSignature()));
.allMatch(spk -> identityKey.getPublicKey().verifySignature(spk.getPublicKey(), spk.getSignature()));
if (!success) {
INVALID_SIGNATURE_COUNTER.increment();
}
return success;
} catch (IllegalArgumentException | InvalidKeyException e) {
} catch (final IllegalArgumentException e) {
INVALID_SIGNATURE_COUNTER.increment();
return false;
}

View File

@@ -6,16 +6,16 @@ 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 com.google.common.annotations.VisibleForTesting;
import io.swagger.v3.oas.annotations.media.Schema;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import java.util.ArrayList;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotEmpty;
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;
@@ -55,24 +55,24 @@ public class PreKeyState {
private SignedPreKey pqLastResortPreKey;
@JsonProperty
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotEmpty
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
@NotNull
@Schema(description="Required. " +
"The public identity key for this identity (account or phone-number identity). " +
"If this device is not the primary device for the account, " +
"must match the existing stored identity key for this identity.")
private byte[] identityKey;
private IdentityKey identityKey;
public PreKeyState() {}
@VisibleForTesting
public PreKeyState(byte[] identityKey, SignedPreKey signedPreKey, List<PreKey> keys) {
public PreKeyState(IdentityKey identityKey, SignedPreKey signedPreKey, List<PreKey> keys) {
this(identityKey, signedPreKey, keys, null, null);
}
@VisibleForTesting
public PreKeyState(byte[] identityKey, SignedPreKey signedPreKey, List<PreKey> keys, List<SignedPreKey> pqKeys, SignedPreKey pqLastResortKey) {
public PreKeyState(IdentityKey identityKey, SignedPreKey signedPreKey, List<PreKey> keys, List<SignedPreKey> pqKeys, SignedPreKey pqLastResortKey) {
this.identityKey = identityKey;
this.signedPreKey = signedPreKey;
this.preKeys = keys;
@@ -96,7 +96,7 @@ public class PreKeyState {
return pqLastResortPreKey;
}
public byte[] getIdentityKey() {
public IdentityKey getIdentityKey() {
return identityKey;
}

View File

@@ -12,16 +12,16 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.google.common.annotations.VisibleForTesting;
import io.swagger.v3.oas.annotations.media.Schema;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import org.whispersystems.textsecuregcm.util.OptionalBase64ByteArrayDeserializer;
import org.whispersystems.textsecuregcm.util.ValidPreKey;
import org.whispersystems.textsecuregcm.util.ValidPreKey.PreKeyType;
import java.util.List;
import java.util.Optional;
import javax.validation.Valid;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
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
@@ -57,16 +57,18 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
provided, an account will be created "atomically," and all other properties needed for
atomic account creation must also be present.
""")
@JsonDeserialize(using = OptionalBase64ByteArrayDeserializer.class)
Optional<byte[]> aciIdentityKey,
@JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class)
Optional<IdentityKey> aciIdentityKey,
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = """
The PNI-associated identity key for the account, encoded as a base64 string. If
provided, an account will be created "atomically," and all other properties needed for
atomic account creation must also be present.
""")
@JsonDeserialize(using = OptionalBase64ByteArrayDeserializer.class)
Optional<byte[]> pniIdentityKey,
@JsonSerialize(using = OptionalIdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = OptionalIdentityKeyAdapter.Deserializer.class)
Optional<IdentityKey> pniIdentityKey,
@JsonUnwrapped
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
@@ -78,8 +80,8 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
@JsonProperty("recoveryPassword") byte[] recoveryPassword,
@JsonProperty("accountAttributes") AccountAttributes accountAttributes,
@JsonProperty("skipDeviceTransfer") boolean skipDeviceTransfer,
@JsonProperty("aciIdentityKey") Optional<byte[]> aciIdentityKey,
@JsonProperty("pniIdentityKey") Optional<byte[]> pniIdentityKey,
@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,
@@ -103,7 +105,7 @@ public record RegistrationRequest(@Schema(requiredMode = Schema.RequiredMode.NOT
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static boolean validatePreKeySignature(final Optional<byte[]> maybeIdentityKey,
private static boolean validatePreKeySignature(final Optional<IdentityKey> maybeIdentityKey,
final Optional<SignedPreKey> maybeSignedPreKey) {
return maybeSignedPreKey.map(signedPreKey -> maybeIdentityKey

View File

@@ -18,14 +18,15 @@ import java.util.Optional;
import java.util.UUID;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import org.signal.libsignal.protocol.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock;
import org.whispersystems.textsecuregcm.entities.AccountAttributes;
import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter;
import org.whispersystems.textsecuregcm.util.IdentityKeyAdapter;
import org.whispersystems.textsecuregcm.util.Util;
public class Account {
@@ -66,14 +67,14 @@ public class Account {
private List<Device> devices = new ArrayList<>();
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
private byte[] identityKey;
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
private IdentityKey identityKey;
@JsonProperty("pniIdentityKey")
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
private byte[] phoneNumberIdentityKey;
@JsonSerialize(using = IdentityKeyAdapter.Serializer.class)
@JsonDeserialize(using = IdentityKeyAdapter.Deserializer.class)
private IdentityKey phoneNumberIdentityKey;
@JsonProperty("cpv")
private String currentProfileVersion;
@@ -327,23 +328,23 @@ public class Account {
this.canonicallyDiscoverable = canonicallyDiscoverable;
}
public void setIdentityKey(byte[] identityKey) {
public void setIdentityKey(final IdentityKey identityKey) {
requireNotStale();
this.identityKey = identityKey;
}
public byte[] getIdentityKey() {
public IdentityKey getIdentityKey() {
requireNotStale();
return identityKey;
}
public byte[] getPhoneNumberIdentityKey() {
public IdentityKey getPhoneNumberIdentityKey() {
return phoneNumberIdentityKey;
}
public void setPhoneNumberIdentityKey(final byte[] phoneNumberIdentityKey) {
public void setPhoneNumberIdentityKey(final IdentityKey phoneNumberIdentityKey) {
this.phoneNumberIdentityKey = phoneNumberIdentityKey;
}

View File

@@ -30,10 +30,6 @@ import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.util.AttributeValues;
@@ -81,8 +77,6 @@ public class Accounts extends AbstractDynamoDbStore {
private static final Timer GET_ALL_FROM_OFFSET_TIMER = Metrics.timer(name(Accounts.class, "getAllFromOffset"));
private static final Timer DELETE_TIMER = Metrics.timer(name(Accounts.class, "delete"));
private static final String INVALID_IDENTITY_KEY_COUNTER_NAME = name(Accounts.class, "invalidIdentityKey");
private static final String CONDITIONAL_CHECK_FAILED = "ConditionalCheckFailed";
private static final String TRANSACTION_CONFLICT = "TransactionConflict";
@@ -915,9 +909,6 @@ public class Accounts extends AbstractDynamoDbStore {
.map(AttributeValue::bool)
.orElse(false));
checkIdentityKey(account.getUuid(), account.getIdentityKey(), "aci");
checkIdentityKey(account.getUuid(), account.getPhoneNumberIdentityKey(), "pni");
return account;
} catch (final IOException e) {
@@ -925,19 +916,6 @@ public class Accounts extends AbstractDynamoDbStore {
}
}
private static void checkIdentityKey(final UUID accountIdentifier, @Nullable final byte[] identityKey, final String keyType) {
if (identityKey != null && identityKey.length > 0) {
try {
new IdentityKey(identityKey);
} catch (final InvalidKeyException e) {
if (identityKey.length != ECPublicKey.KEY_SIZE - 1) {
log.warn("Account {} has an invalid {} identity key; length = {}", accountIdentifier, keyType, identityKey.length);
Metrics.counter(INVALID_IDENTITY_KEY_COUNTER_NAME, "type", keyType).increment();
}
}
}
}
private static boolean conditionalCheckFailed(final CancellationReason reason) {
return CONDITIONAL_CHECK_FAILED.equals(reason.code());
}

View File

@@ -27,6 +27,7 @@ import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.UUID;
@@ -38,6 +39,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.ObjectUtils;
import org.signal.libsignal.protocol.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
@@ -255,7 +257,7 @@ public class AccountsManager {
public Account changeNumber(final Account account,
final String targetNumber,
@Nullable final byte[] pniIdentityKey,
@Nullable final IdentityKey pniIdentityKey,
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
@Nullable final Map<Long, Integer> pniRegistrationIds) throws InterruptedException, MismatchedDevicesException {
@@ -347,7 +349,7 @@ public class AccountsManager {
}
public Account updatePniKeys(final Account account,
final byte[] pniIdentityKey,
final IdentityKey pniIdentityKey,
final Map<Long, SignedPreKey> pniSignedPreKeys,
@Nullable final Map<Long, SignedPreKey> pniPqLastResortPreKeys,
final Map<Long, Integer> pniRegistrationIds) throws MismatchedDevicesException {
@@ -366,7 +368,7 @@ public class AccountsManager {
}
private boolean setPniKeys(final Account account,
@Nullable final byte[] pniIdentityKey,
@Nullable final IdentityKey pniIdentityKey,
@Nullable final Map<Long, SignedPreKey> pniSignedPreKeys,
@Nullable final Map<Long, Integer> pniRegistrationIds) {
if (ObjectUtils.allNull(pniIdentityKey, pniSignedPreKeys, pniRegistrationIds)) {
@@ -375,7 +377,7 @@ public class AccountsManager {
throw new IllegalArgumentException("PNI identity key, signed pre-keys, and registration IDs must be all null or all non-null");
}
boolean changed = !Arrays.equals(pniIdentityKey, account.getPhoneNumberIdentityKey());
boolean changed = !Objects.equals(pniIdentityKey, account.getPhoneNumberIdentityKey());
for (Device device : account.getDevices()) {
if (!device.isEnabled()) {

View File

@@ -6,7 +6,14 @@ package org.whispersystems.textsecuregcm.storage;
import com.google.common.annotations.VisibleForTesting;
import com.google.protobuf.ByteString;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.apache.commons.lang3.ObjectUtils;
import org.signal.libsignal.protocol.IdentityKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.whispersystems.textsecuregcm.controllers.AccountController;
@@ -19,12 +26,6 @@ import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.push.MessageSender;
import org.whispersystems.textsecuregcm.push.NotPushRegisteredException;
import org.whispersystems.textsecuregcm.util.DestinationDeviceValidator;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
public class ChangeNumberManager {
private static final Logger logger = LoggerFactory.getLogger(AccountController.class);
@@ -39,7 +40,7 @@ public class ChangeNumberManager {
}
public Account changeNumber(final Account account, final String number,
@Nullable final byte[] pniIdentityKey,
@Nullable final IdentityKey pniIdentityKey,
@Nullable final Map<Long, SignedPreKey> deviceSignedPreKeys,
@Nullable final Map<Long, SignedPreKey> devicePqLastResortPreKeys,
@Nullable final List<IncomingMessage> deviceMessages,
@@ -79,7 +80,7 @@ public class ChangeNumberManager {
}
public Account updatePniKeys(final Account account,
final byte[] pniIdentityKey,
final IdentityKey pniIdentityKey,
final Map<Long, SignedPreKey> deviceSignedPreKeys,
@Nullable final Map<Long, SignedPreKey> devicePqLastResortPreKeys,
final List<IncomingMessage> deviceMessages,

View File

@@ -0,0 +1,64 @@
/*
* 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 io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.Metrics;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
public class IdentityKeyAdapter {
private static final Counter IDENTITY_KEY_WITHOUT_VERSION_BYTE_COUNTER =
Metrics.counter(MetricsUtil.name(IdentityKeyAdapter.class), "identityKeyWithoutVersionByte");
public static class Serializer extends JsonSerializer<IdentityKey> {
@Override
public void serialize(final IdentityKey identityKey,
final JsonGenerator jsonGenerator,
final SerializerProvider serializers) throws IOException {
jsonGenerator.writeString(Base64.getEncoder().encodeToString(identityKey.serialize()));
}
}
public static class Deserializer extends JsonDeserializer<IdentityKey> {
@Override
public IdentityKey deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
final byte[] identityKeyBytes;
try {
identityKeyBytes = Base64.getDecoder().decode(parser.getValueAsString());
} catch (final IllegalArgumentException e) {
throw new JsonParseException(parser, "Could not parse identity key as a base64-encoded value", e);
}
try {
return new IdentityKey(identityKeyBytes);
} catch (final InvalidKeyException e) {
if (identityKeyBytes.length == ECPublicKey.KEY_SIZE - 1) {
IDENTITY_KEY_WITHOUT_VERSION_BYTE_COUNTER.increment();
return new IdentityKey(ECPublicKey.fromPublicKeyBytes(identityKeyBytes));
}
throw new JsonParseException(parser, "Could not interpret identity key bytes as an EC public key", e);
}
}
}
}

View File

@@ -1,22 +0,0 @@
package org.whispersystems.textsecuregcm.util;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.util.Base64;
import java.util.Optional;
public class OptionalBase64ByteArrayDeserializer extends JsonDeserializer<Optional<byte[]>> {
@Override
public Optional<byte[]> deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
return Optional.of(Base64.getDecoder().decode(jsonParser.getValueAsString()));
}
@Override
public Optional<byte[]> getNullValue(DeserializationContext ctxt) {
return Optional.empty();
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.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 java.util.Optional;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
public class OptionalIdentityKeyAdapter {
public static class Serializer extends JsonSerializer<Optional<IdentityKey>> {
@Override
public void serialize(final Optional<IdentityKey> maybePublicKey,
final JsonGenerator jsonGenerator,
final SerializerProvider serializers) throws IOException {
if (maybePublicKey.isPresent()) {
jsonGenerator.writeString(Base64.getEncoder().encodeToString(maybePublicKey.get().serialize()));
} else {
jsonGenerator.writeNull();
}
}
}
public static class Deserializer extends JsonDeserializer<Optional<IdentityKey>> {
@Override
public Optional<IdentityKey> deserialize(final JsonParser jsonParser, final DeserializationContext deserializationContext) throws IOException {
try {
return Optional.of(new IdentityKey(Base64.getDecoder().decode(jsonParser.getValueAsString())));
} catch (final InvalidKeyException e) {
throw new IOException(e);
}
}
@Override
public Optional<IdentityKey> getNullValue(DeserializationContext ctxt) {
return Optional.empty();
}
}
}