mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-21 02:08:03 +01:00
Require PQ keys when changing numbers or distributing key material
This commit is contained in:
committed by
Jon Chambers
parent
e43487155f
commit
b400d49e77
@@ -17,6 +17,7 @@ import javax.annotation.Nullable;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
|
||||
@@ -51,34 +52,27 @@ public record ChangeNumberRequest(
|
||||
arraySchema=@Schema(description="""
|
||||
A list of synchronization messages to send to companion devices to supply the private keysManager
|
||||
associated with the new identity key and their new prekeys.
|
||||
Exactly one message must be supplied for each enabled device other than the sending (primary) device."""))
|
||||
Exactly one message must be supplied for each device other than the sending (primary) device."""))
|
||||
@NotNull @Valid List<@NotNull @Valid IncomingMessage> deviceMessages,
|
||||
|
||||
@Schema(description="""
|
||||
A new signed elliptic-curve prekey for each enabled device on the account, including this one.
|
||||
A new signed elliptic-curve prekey for each 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<Byte, @NotNull @Valid ECSignedPreKey> devicePniSignedPrekeys,
|
||||
@NotNull @NotEmpty @Valid Map<Byte, @NotNull @Valid ECSignedPreKey> devicePniSignedPrekeys,
|
||||
|
||||
@Schema(description="""
|
||||
A new signed post-quantum last-resort prekey for each enabled device on the account, including this one.
|
||||
May be absent, in which case the last resort PQ prekeys for each device will be deleted if any had been stored.
|
||||
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.
|
||||
A new signed post-quantum last-resort prekey for each device on the account, including this one.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@Valid Map<Byte, @NotNull @Valid KEMSignedPreKey> devicePniPqLastResortPrekeys,
|
||||
@NotNull @NotEmpty @Valid Map<Byte, @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<Byte, Integer> pniRegistrationIds) implements PhoneVerificationRequest {
|
||||
@Schema(description="the new phone-number-identity registration ID for each device on the account, including this one")
|
||||
@NotNull @NotEmpty Map<Byte, Integer> pniRegistrationIds) implements PhoneVerificationRequest {
|
||||
|
||||
public boolean isSignatureValidOnEachSignedPreKey(@Nullable final String userAgent) {
|
||||
List<SignedPreKey<?>> spks = new ArrayList<>();
|
||||
if (devicePniSignedPrekeys != null) {
|
||||
spks.addAll(devicePniSignedPrekeys.values());
|
||||
}
|
||||
if (devicePniPqLastResortPrekeys != null) {
|
||||
spks.addAll(devicePniPqLastResortPrekeys.values());
|
||||
}
|
||||
return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks, userAgent, "change-number");
|
||||
final List<SignedPreKey<?>> spks = new ArrayList<>(devicePniSignedPrekeys.values());
|
||||
spks.addAll(devicePniPqLastResortPrekeys.values());
|
||||
|
||||
return PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks, userAgent, "change-number");
|
||||
}
|
||||
|
||||
@AssertTrue
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -29,36 +30,36 @@ public record PhoneNumberIdentityKeyDistributionRequest(
|
||||
arraySchema=@Schema(description="""
|
||||
A list of synchronization messages to send to companion devices to supply the private keys
|
||||
associated with the new identity key and their new prekeys.
|
||||
Exactly one message must be supplied for each enabled device other than the sending (primary) device.
|
||||
Exactly one message must be supplied for each device other than the sending (primary) device.
|
||||
"""))
|
||||
List<@NotNull @Valid IncomingMessage> deviceMessages,
|
||||
|
||||
@NotNull
|
||||
@NotEmpty
|
||||
@Valid
|
||||
@Schema(description="""
|
||||
A new signed elliptic-curve prekey for each enabled device on the account, including this one.
|
||||
A new signed elliptic-curve prekey for each device on the account, including this one.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
Map<Byte, @NotNull @Valid ECSignedPreKey> devicePniSignedPrekeys,
|
||||
|
||||
@NotNull
|
||||
@NotEmpty
|
||||
@Valid
|
||||
@Schema(description="""
|
||||
A new signed post-quantum last-resort prekey for each enabled device on the account, including this one.
|
||||
May be absent, in which case the last resort PQ prekeys for each device will be deleted if any had been stored.
|
||||
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.
|
||||
A new signed post-quantum last-resort prekey for each device on the account, including this one.
|
||||
Each must be accompanied by a valid signature from the new identity key in this request.""")
|
||||
@Valid Map<Byte, @NotNull @Valid KEMSignedPreKey> devicePniPqLastResortPrekeys,
|
||||
Map<Byte, @NotNull @Valid KEMSignedPreKey> devicePniPqLastResortPrekeys,
|
||||
|
||||
@NotNull
|
||||
@NotEmpty
|
||||
@Valid
|
||||
@Schema(description="The new registration ID to use for the phone-number identity of each device, including this one.")
|
||||
Map<Byte, Integer> pniRegistrationIds) {
|
||||
|
||||
public boolean isSignatureValidOnEachSignedPreKey(@Nullable final String userAgent) {
|
||||
List<SignedPreKey<?>> spks = new ArrayList<>(devicePniSignedPrekeys.values());
|
||||
if (devicePniPqLastResortPrekeys != null) {
|
||||
spks.addAll(devicePniPqLastResortPrekeys.values());
|
||||
}
|
||||
return spks.isEmpty() || PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, spks, userAgent, "distribute-pni-keys");
|
||||
}
|
||||
final List<SignedPreKey<?>> signedPreKeys = new ArrayList<>(devicePniSignedPrekeys.values());
|
||||
signedPreKeys.addAll(devicePniPqLastResortPrekeys.values());
|
||||
|
||||
return PreKeySignatureValidator.validatePreKeySignatures(pniIdentityKey, signedPreKeys, userAgent, "distribute-pni-keys");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ import java.util.stream.Stream;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.signal.libsignal.protocol.IdentityKey;
|
||||
import org.slf4j.Logger;
|
||||
@@ -641,19 +640,16 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
}
|
||||
|
||||
public Account changeNumber(final Account account,
|
||||
final String targetNumber,
|
||||
@Nullable final IdentityKey pniIdentityKey,
|
||||
@Nullable final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
@Nullable final Map<Byte, Integer> pniRegistrationIds) throws InterruptedException, MismatchedDevicesException {
|
||||
final String targetNumber,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
final Map<Byte, Integer> pniRegistrationIds) throws InterruptedException, MismatchedDevicesException {
|
||||
|
||||
final UUID originalPhoneNumberIdentifier = account.getPhoneNumberIdentifier();
|
||||
final UUID targetPhoneNumberIdentifier = phoneNumberIdentifiers.getPhoneNumberIdentifier(targetNumber).join();
|
||||
|
||||
if (originalPhoneNumberIdentifier.equals(targetPhoneNumberIdentifier)) {
|
||||
if (pniIdentityKey != null) {
|
||||
throw new IllegalArgumentException("change number must supply a changed phone number; otherwise use updatePniKeys");
|
||||
}
|
||||
return account;
|
||||
}
|
||||
|
||||
@@ -694,7 +690,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
.join();
|
||||
|
||||
final Collection<TransactWriteItem> keyWriteItems =
|
||||
buildPniKeyWriteItems(uuid, targetPhoneNumberIdentifier, pniSignedPreKeys, pniPqLastResortPreKeys);
|
||||
buildPniKeyWriteItems(targetPhoneNumberIdentifier, pniSignedPreKeys, pniPqLastResortPreKeys);
|
||||
|
||||
final Account numberChangedAccount = updateWithRetries(
|
||||
account,
|
||||
@@ -715,7 +711,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
public Account updatePniKeys(final Account account,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
final Map<Byte, Integer> pniRegistrationIds) throws MismatchedDevicesException {
|
||||
|
||||
validateDevices(account, pniSignedPreKeys, pniPqLastResortPreKeys, pniRegistrationIds);
|
||||
@@ -724,7 +720,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
final UUID pni = account.getIdentifier(IdentityType.PNI);
|
||||
|
||||
final Collection<TransactWriteItem> keyWriteItems =
|
||||
buildPniKeyWriteItems(pni, pni, pniSignedPreKeys, pniPqLastResortPreKeys);
|
||||
buildPniKeyWriteItems(pni, pniSignedPreKeys, pniPqLastResortPreKeys);
|
||||
|
||||
return redisDeleteAsync(account)
|
||||
.thenCompose(ignored -> keysManager.deleteSingleUsePreKeys(pni))
|
||||
@@ -739,41 +735,24 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
}
|
||||
|
||||
private Collection<TransactWriteItem> buildPniKeyWriteItems(
|
||||
final UUID enabledDevicesIdentifier,
|
||||
final UUID phoneNumberIdentifier,
|
||||
@Nullable final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys) {
|
||||
final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys) {
|
||||
|
||||
final List<TransactWriteItem> keyWriteItems = new ArrayList<>();
|
||||
|
||||
if (pniSignedPreKeys != null) {
|
||||
pniSignedPreKeys.forEach((deviceId, signedPreKey) ->
|
||||
keyWriteItems.add(keysManager.buildWriteItemForEcSignedPreKey(phoneNumberIdentifier, deviceId, signedPreKey)));
|
||||
}
|
||||
pniSignedPreKeys.forEach((deviceId, signedPreKey) ->
|
||||
keyWriteItems.add(keysManager.buildWriteItemForEcSignedPreKey(phoneNumberIdentifier, deviceId, signedPreKey)));
|
||||
|
||||
if (pniPqLastResortPreKeys != null) {
|
||||
keysManager.getPqEnabledDevices(enabledDevicesIdentifier)
|
||||
.thenAccept(deviceIds -> deviceIds.stream()
|
||||
.filter(pniPqLastResortPreKeys::containsKey)
|
||||
.map(deviceId -> keysManager.buildWriteItemForLastResortKey(phoneNumberIdentifier,
|
||||
deviceId,
|
||||
pniPqLastResortPreKeys.get(deviceId)))
|
||||
.forEach(keyWriteItems::add))
|
||||
.join();
|
||||
}
|
||||
pniPqLastResortPreKeys.forEach((deviceId, lastResortKey) ->
|
||||
keyWriteItems.add(keysManager.buildWriteItemForLastResortKey(phoneNumberIdentifier, deviceId, lastResortKey)));
|
||||
|
||||
return keyWriteItems;
|
||||
}
|
||||
|
||||
private void setPniKeys(final Account account,
|
||||
@Nullable final IdentityKey pniIdentityKey,
|
||||
@Nullable final Map<Byte, Integer> pniRegistrationIds) {
|
||||
|
||||
if (ObjectUtils.allNull(pniIdentityKey, pniRegistrationIds)) {
|
||||
return;
|
||||
} else if (!ObjectUtils.allNotNull(pniIdentityKey, pniRegistrationIds)) {
|
||||
throw new IllegalArgumentException("PNI identity key and registration IDs must be all null or all non-null");
|
||||
}
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Map<Byte, Integer> pniRegistrationIds) {
|
||||
|
||||
account.getDevices()
|
||||
.forEach(device -> device.setPhoneNumberIdentityRegistrationId(pniRegistrationIds.get(device.getId())));
|
||||
@@ -782,22 +761,15 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
}
|
||||
|
||||
private void validateDevices(final Account account,
|
||||
@Nullable final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
@Nullable final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
@Nullable final Map<Byte, Integer> pniRegistrationIds) throws MismatchedDevicesException {
|
||||
if (pniSignedPreKeys == null && pniRegistrationIds == null) {
|
||||
return;
|
||||
} else if (pniSignedPreKeys == null || pniRegistrationIds == null) {
|
||||
throw new IllegalArgumentException("Signed pre-keys and registration IDs must both be null or both be non-null");
|
||||
}
|
||||
final Map<Byte, ECSignedPreKey> pniSignedPreKeys,
|
||||
final Map<Byte, KEMSignedPreKey> pniPqLastResortPreKeys,
|
||||
final Map<Byte, Integer> pniRegistrationIds) throws MismatchedDevicesException {
|
||||
|
||||
// Check that all including primary ID are in signed pre-keys
|
||||
validateCompleteDeviceList(account, pniSignedPreKeys.keySet());
|
||||
|
||||
// Check that all including primary ID are in Pq pre-keys
|
||||
if (pniPqLastResortPreKeys != null) {
|
||||
validateCompleteDeviceList(account, pniPqLastResortPreKeys.keySet());
|
||||
}
|
||||
validateCompleteDeviceList(account, pniPqLastResortPreKeys.keySet());
|
||||
|
||||
// Check that all devices are accounted for in the map of new PNI registration IDs
|
||||
validateCompleteDeviceList(account, pniRegistrationIds.keySet());
|
||||
@@ -816,8 +788,7 @@ public class AccountsManager extends RedisPubSubAdapter<String, String> implemen
|
||||
extraDeviceIds.removeAll(accountDeviceIds);
|
||||
|
||||
if (!missingDeviceIds.isEmpty() || !extraDeviceIds.isEmpty()) {
|
||||
throw new MismatchedDevicesException(
|
||||
new MismatchedDevices(missingDeviceIds, extraDeviceIds, Collections.emptySet()));
|
||||
throw new MismatchedDevicesException(new MismatchedDevices(missingDeviceIds, extraDeviceIds, Set.of()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,13 +42,14 @@ public class ChangeNumberManager {
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
public Account changeNumber(final Account account, final String number,
|
||||
@Nullable final IdentityKey pniIdentityKey,
|
||||
@Nullable final Map<Byte, ECSignedPreKey> deviceSignedPreKeys,
|
||||
@Nullable final Map<Byte, KEMSignedPreKey> devicePqLastResortPreKeys,
|
||||
@Nullable final List<IncomingMessage> deviceMessages,
|
||||
@Nullable final Map<Byte, Integer> pniRegistrationIds,
|
||||
@Nullable final String senderUserAgent)
|
||||
public Account changeNumber(final Account account,
|
||||
final String number,
|
||||
final IdentityKey pniIdentityKey,
|
||||
final Map<Byte, ECSignedPreKey> deviceSignedPreKeys,
|
||||
final Map<Byte, KEMSignedPreKey> devicePqLastResortPreKeys,
|
||||
final List<IncomingMessage> deviceMessages,
|
||||
final Map<Byte, Integer> pniRegistrationIds,
|
||||
final String senderUserAgent)
|
||||
throws InterruptedException, MismatchedDevicesException, MessageTooLargeException {
|
||||
|
||||
if (!(ObjectUtils.allNotNull(pniIdentityKey, deviceSignedPreKeys, deviceMessages, pniRegistrationIds) ||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -114,10 +113,6 @@ public class KeysManager {
|
||||
return ecSignedPreKeys.find(identifier, deviceId);
|
||||
}
|
||||
|
||||
public CompletableFuture<List<Byte>> getPqEnabledDevices(final UUID identifier) {
|
||||
return pqLastResortKeys.getDeviceIdsWithKeys(identifier).collectList().toFuture();
|
||||
}
|
||||
|
||||
public CompletableFuture<Integer> getEcCount(final UUID identifier, final byte deviceId) {
|
||||
return ecPreKeys.getCount(identifier, deviceId);
|
||||
}
|
||||
|
||||
@@ -14,14 +14,12 @@ import java.util.concurrent.CompletableFuture;
|
||||
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
|
||||
import org.whispersystems.textsecuregcm.metrics.MetricsUtil;
|
||||
import org.whispersystems.textsecuregcm.util.AttributeValues;
|
||||
import reactor.core.publisher.Flux;
|
||||
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Delete;
|
||||
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.Put;
|
||||
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
|
||||
import software.amazon.awssdk.services.dynamodb.model.TransactWriteItem;
|
||||
|
||||
/**
|
||||
@@ -116,20 +114,6 @@ public abstract class RepeatedUseSignedPreKeyStore<K extends SignedPreKey<?>> {
|
||||
return findFuture;
|
||||
}
|
||||
|
||||
public Flux<Byte> getDeviceIdsWithKeys(final UUID identifier) {
|
||||
return Flux.from(dynamoDbAsyncClient.queryPaginator(QueryRequest.builder()
|
||||
.tableName(tableName)
|
||||
.keyConditionExpression("#uuid = :uuid")
|
||||
.expressionAttributeNames(Map.of("#uuid", KEY_ACCOUNT_UUID))
|
||||
.expressionAttributeValues(Map.of(
|
||||
":uuid", getPartitionKey(identifier)))
|
||||
.projectionExpression(KEY_DEVICE_ID)
|
||||
.consistentRead(true)
|
||||
.build())
|
||||
.items())
|
||||
.map(item -> Byte.parseByte(item.get(KEY_DEVICE_ID).n()));
|
||||
}
|
||||
|
||||
protected static Map<String, AttributeValue> getPrimaryKey(final UUID identifier, final byte deviceId) {
|
||||
return Map.of(
|
||||
KEY_ACCOUNT_UUID, getPartitionKey(identifier),
|
||||
|
||||
Reference in New Issue
Block a user