From 48fd25f01ec3473e1d1f4f90360031705e4b321b Mon Sep 17 00:00:00 2001 From: Jonathan Klabunde Tomer Date: Mon, 3 Aug 2026 14:50:33 -0700 Subject: [PATCH] Support linking devices to accounts without phone numbers --- .../controllers/DeviceController.java | 57 ++++--- .../entities/DeviceActivationRequest.java | 2 - .../entities/DeviceAttributes.java | 3 +- .../entities/LinkDeviceResponse.java | 3 +- .../entities/PreKeySignatureValidator.java | 1 - .../grpc/DeviceCapabilityUtil.java | 2 + .../storage/AccountsManager.java | 16 +- .../storage/DeviceCapability.java | 3 +- .../textsecuregcm/storage/DeviceSpec.java | 2 +- .../main/proto/org/signal/chat/common.proto | 1 + .../controllers/DeviceControllerTest.java | 160 +++++++++++++----- .../RegistrationControllerTest.java | 85 +++++----- .../storage/AccountsManagerTest.java | 32 ++-- .../tests/util/AccountsHelper.java | 8 + 14 files changed, 241 insertions(+), 134 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java index bd00e5205..b69c01f88 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/DeviceController.java @@ -48,6 +48,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import javax.annotation.Nullable; +import org.apache.commons.lang3.ObjectUtils; import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice; import org.whispersystems.textsecuregcm.auth.BasicAuthorizationHeader; import org.whispersystems.textsecuregcm.auth.ChangesLinkedDevices; @@ -224,7 +225,7 @@ public class DeviceController { """) @ApiResponse(responseCode = "200", description = "The new device was linked to the calling account", useReturnTypeSchema = true) @ApiResponse(responseCode = "403", description = "The given account was not found or the given verification code was incorrect") - @ApiResponse(responseCode = "409", description = "The new device is missing a capability supported by all other devices on the account") + @ApiResponse(responseCode = "409", description = "The new device is missing a capability required for the account") @ApiResponse(responseCode = "411", description = "The given account already has its maximum number of linked devices") @ApiResponse(responseCode = "422", description = "The request did not pass validation") @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( @@ -234,7 +235,6 @@ public class DeviceController { @HeaderParam(HttpHeaders.USER_AGENT) @Nullable String userAgent, @NotNull @Valid LinkDeviceRequest linkDeviceRequest) throws RateLimitExceededException, DeviceLimitExceededException { - final Account account = accounts.checkDeviceLinkingToken(linkDeviceRequest.verificationCode()) .flatMap(accounts::getByAccountIdentifier) .orElseThrow(ForbiddenException::new); @@ -242,23 +242,33 @@ public class DeviceController { final DeviceActivationRequest deviceActivationRequest = linkDeviceRequest.deviceActivationRequest(); final DeviceAttributes deviceAttributes = linkDeviceRequest.deviceAttributes(); - if (deviceAttributes.phoneNumberIdentityRegistrationId() == null || - deviceActivationRequest.pniSignedPreKey().isEmpty() || - deviceActivationRequest.pniPqLastResortPreKey().isEmpty()) { - throw new WebApplicationException("PNI-associated info must all be provided", 422); - } - rateLimiters.getVerifyDeviceLimiter().validate(account.getAccountIdentifier()); + // Check the optional-phone-number capability before checking PNI keys, so we can give a better error code (since an + // older device will improperly supply PNI keys for a PNI-less account) + if (account.getPhoneNumberIdentifierOptional().isEmpty() && + !linkDeviceRequest.deviceAttributes().capabilities().contains(DeviceCapability.OPTIONAL_PHONE_NUMBER)) { + throw new WebApplicationException("Missing required device capability", 409); + } + final boolean allKeysValid = - PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.ACI), - List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()), - userAgent, - "link-device") - && PreKeySignatureValidator.validatePreKeySignatures(account.getIdentityKey(IdentityType.PNI), - List.of(deviceActivationRequest.pniSignedPreKey().get(), deviceActivationRequest.pniPqLastResortPreKey().get()), - userAgent, - "link-device"); + PreKeySignatureValidator + .validatePreKeySignatures(account.getAccountIdentityKey(), + List.of(deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()), + userAgent, + "link-device") + && account.getPhoneNumberIdentityKey() + .map(pniIdentityKey -> + deviceActivationRequest.pniSignedPreKey().isPresent() + && deviceActivationRequest.pniPqLastResortPreKey().isPresent() + && PreKeySignatureValidator.validatePreKeySignatures( + pniIdentityKey, + List.of(deviceActivationRequest.pniSignedPreKey().get(), deviceActivationRequest.pniPqLastResortPreKey().get()), + userAgent, + "link-device")) + .orElse( + deviceActivationRequest.pniSignedPreKey().isEmpty() + && deviceActivationRequest.pniPqLastResortPreKey().isEmpty()); if (!allKeysValid) { throw new WebApplicationException(Response.status(422).build()); @@ -287,7 +297,7 @@ public class DeviceController { } try { - final Pair accountAndDevice = accounts.addDevice(account.getIdentifier(IdentityType.ACI), + final Pair accountAndDevice = accounts.addDevice(account.getAccountIdentifier(), new DeviceSpec(deviceAttributes.name(), authorizationHeader.getPassword(), signalAgent, @@ -296,18 +306,19 @@ public class DeviceController { deviceAttributes.registrationId(), deviceActivationRequest.aciSignedPreKey(), deviceActivationRequest.aciPqLastResortPreKey()), - Optional.of(new DeviceIdentityInfo( - deviceAttributes.phoneNumberIdentityRegistrationId(), - deviceActivationRequest.pniSignedPreKey().get(), - deviceActivationRequest.pniPqLastResortPreKey().get())), + account.getPhoneNumberIdentityKey().map(_ -> + new DeviceIdentityInfo( + deviceAttributes.phoneNumberIdentityRegistrationId(), + deviceActivationRequest.pniSignedPreKey().get(), + deviceActivationRequest.pniPqLastResortPreKey().get())), deviceAttributes.fetchesMessages(), deviceActivationRequest.apnToken(), deviceActivationRequest.gcmToken()), linkDeviceRequest.verificationCode()); return new LinkDeviceResponse( - accountAndDevice.first().getIdentifier(IdentityType.ACI), - accountAndDevice.first().getIdentifier(IdentityType.PNI), + accountAndDevice.first().getAccountIdentifier(), + accountAndDevice.first().getPhoneNumberIdentifierOptional(), accountAndDevice.second().getId()); } catch (final LinkDeviceTokenAlreadyUsedException e) { throw new ForbiddenException(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java index a16fcee1f..fa51096b8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceActivationRequest.java @@ -1,10 +1,8 @@ package org.whispersystems.textsecuregcm.entities; import io.swagger.v3.oas.annotations.media.Schema; - import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; - import java.util.Optional; public record DeviceActivationRequest( diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceAttributes.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceAttributes.java index 7ecb10031..a0d52cdb8 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceAttributes.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/DeviceAttributes.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.Size; +import java.util.Optional; import java.util.Set; import javax.annotation.Nullable; import org.whispersystems.textsecuregcm.storage.DeviceCapability; @@ -29,13 +30,11 @@ public record DeviceAttributes( Integer phoneNumberIdentityRegistrationId, @JsonSerialize(using = ByteArrayAdapter.Serializing.class) - @JsonDeserialize(using = ByteArrayAdapter.Deserializing.class) @Size(max = 225) byte[] name, @JsonSerialize(using = DeviceCapabilityAdapter.Serializer.class) - @JsonDeserialize(using = DeviceCapabilityAdapter.Deserializer.class) @Nullable Set capabilities) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceResponse.java index 6cc332c11..71e26914b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/LinkDeviceResponse.java @@ -5,7 +5,8 @@ package org.whispersystems.textsecuregcm.entities; +import java.util.Optional; import java.util.UUID; -public record LinkDeviceResponse(UUID uuid, UUID pni, byte deviceId) { +public record LinkDeviceResponse(UUID uuid, Optional pni, byte deviceId) { } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java index e3193d585..732c70f57 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/PreKeySignatureValidator.java @@ -22,7 +22,6 @@ public abstract class PreKeySignatureValidator { final Collection> signedPreKeys, @Nullable final String userAgent, final String context) { - final boolean success = signedPreKeys.stream().allMatch(signedPreKey -> signedPreKey.signatureValid(identityKey)); if (!success) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DeviceCapabilityUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DeviceCapabilityUtil.java index a2b79c13f..1be8fed13 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DeviceCapabilityUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/DeviceCapabilityUtil.java @@ -20,6 +20,7 @@ public class DeviceCapabilityUtil { case DEVICE_CAPABILITY_SPARSE_POST_QUANTUM_RATCHET -> DeviceCapability.SPARSE_POST_QUANTUM_RATCHET; case DEVICE_CAPABILITY_PROFILES_V2 -> DeviceCapability.PROFILES_V2; case DEVICE_CAPABILITY_USERNAME_CHANGE_SYNC_MESSAGE -> DeviceCapability.USERNAME_CHANGE_SYNC_MESSAGE; + case DEVICE_CAPABILITY_OPTIONAL_PHONE_NUMBER -> DeviceCapability.OPTIONAL_PHONE_NUMBER; case DEVICE_CAPABILITY_UNSPECIFIED, UNRECOGNIZED -> throw GrpcExceptions.invalidArguments("unrecognized device capability"); }; @@ -33,6 +34,7 @@ public class DeviceCapabilityUtil { case SPARSE_POST_QUANTUM_RATCHET -> org.signal.chat.common.DeviceCapability.DEVICE_CAPABILITY_SPARSE_POST_QUANTUM_RATCHET; case PROFILES_V2 -> org.signal.chat.common.DeviceCapability.DEVICE_CAPABILITY_PROFILES_V2; case USERNAME_CHANGE_SYNC_MESSAGE -> org.signal.chat.common.DeviceCapability.DEVICE_CAPABILITY_USERNAME_CHANGE_SYNC_MESSAGE; + case OPTIONAL_PHONE_NUMBER -> org.signal.chat.common.DeviceCapability.DEVICE_CAPABILITY_OPTIONAL_PHONE_NUMBER; }; } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java index 85eaba44e..e36fadf1c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -516,11 +516,10 @@ public class AccountsManager extends RedisPubSubAdapter implemen public Pair addDevice(final UUID accountIdentifier, final DeviceSpec deviceSpec, final String linkDeviceToken) throws LinkDeviceTokenAlreadyUsedException { - final UUID phoneNumberIdentifier = accounts.getByAccountIdentifier(accountIdentifier) - .map(account -> account.getIdentifier(IdentityType.PNI)) + final Account account = accounts.getByAccountIdentifier(accountIdentifier) .orElseThrow(() -> new IllegalArgumentException("Account not found: " + accountIdentifier)); - return accountLockManager.withLock(Set.of(phoneNumberIdentifier), + return accountLockManager.withSingleAccountLock(account, () -> addDevice(accountIdentifier, deviceSpec, linkDeviceToken, MAX_UPDATE_ATTEMPTS), accountLockExecutor); } @@ -532,16 +531,19 @@ public class AccountsManager extends RedisPubSubAdapter implemen final byte nextDeviceId = account.getNextDeviceId(); - CompletableFuture.allOf( + CompletableFuture + .allOf( keysManager.deleteSingleUsePreKeys(account.getAccountIdentifier(), nextDeviceId), - keysManager.deleteSingleUsePreKeys(account.getPhoneNumberIdentifier(), nextDeviceId), + account.getPhoneNumberIdentifierOptional() + .map(pni -> keysManager.deleteSingleUsePreKeys(pni, nextDeviceId)) + .orElse(CompletableFuture.completedFuture(null)), messagesManager.clear(account.getAccountIdentifier(), nextDeviceId)) .join(); - account.addDevice(deviceSpec.toDevice(nextDeviceId, clock, account.getIdentityKey(IdentityType.ACI))); + account.addDevice(deviceSpec.toDevice(nextDeviceId, clock, account.getAccountIdentityKey())); final List additionalWriteItems = new ArrayList<>(keysManager.buildWriteItemsForNewDevice( - account.getIdentifier(IdentityType.ACI), + account.getAccountIdentifier(), account.getPhoneNumberIdentifierOptional(), nextDeviceId, deviceSpec.aciInfo().signedPreKey(), diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceCapability.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceCapability.java index 7e8e9c376..05f7ba766 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceCapability.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceCapability.java @@ -18,7 +18,8 @@ public enum DeviceCapability { ATTACHMENT_BACKFILL("attachmentBackfill", AccountCapabilityMode.PRIMARY_DEVICE, AccountCapabilityVisibility.SELF, false, false), SPARSE_POST_QUANTUM_RATCHET("spqr", AccountCapabilityMode.ALL_DEVICES, AccountCapabilityVisibility.PUBLIC, true, true), PROFILES_V2("profiles_v2", AccountCapabilityMode.ALL_DEVICES, AccountCapabilityVisibility.SELF, false, false), - USERNAME_CHANGE_SYNC_MESSAGE("usernameChangeSyncMessage", AccountCapabilityMode.ALL_DEVICES, AccountCapabilityVisibility.SELF, true, false); + USERNAME_CHANGE_SYNC_MESSAGE("usernameChangeSyncMessage", AccountCapabilityMode.ALL_DEVICES, AccountCapabilityVisibility.SELF, true, false), + OPTIONAL_PHONE_NUMBER("optionalPhoneNumber", AccountCapabilityMode.ALL_DEVICES, AccountCapabilityVisibility.SERVER, false, false); public static final List PUBLIC_VISIBLE_CAPABILITIES = Arrays.stream(DeviceCapability.values()) .filter(c -> c.accountCapabilityVisibility == AccountCapabilityVisibility.PUBLIC) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceSpec.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceSpec.java index 97257b868..4ba3c0422 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceSpec.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/DeviceSpec.java @@ -22,7 +22,7 @@ public record DeviceSpec( boolean fetchesMessages, Optional apnRegistrationId, Optional gcmRegistrationId) { - + public Device toDevice(final byte deviceId, final Clock clock, final IdentityKey aciIdentityKey) { final long created = clock.millis(); diff --git a/service/src/main/proto/org/signal/chat/common.proto b/service/src/main/proto/org/signal/chat/common.proto index 896b7b96d..a2cf05767 100644 --- a/service/src/main/proto/org/signal/chat/common.proto +++ b/service/src/main/proto/org/signal/chat/common.proto @@ -92,6 +92,7 @@ enum DeviceCapability { DEVICE_CAPABILITY_SPARSE_POST_QUANTUM_RATCHET = 7; DEVICE_CAPABILITY_PROFILES_V2 = 8; DEVICE_CAPABILITY_USERNAME_CHANGE_SYNC_MESSAGE = 9; + DEVICE_CAPABILITY_OPTIONAL_PHONE_NUMBER = 10; } message ZkCredential { diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java index 7e3ac9922..bf56d1da1 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/DeviceControllerTest.java @@ -138,9 +138,11 @@ class DeviceControllerTest { when(account.getNextDeviceId()).thenReturn(NEXT_DEVICE_ID); when(account.getNumber()).thenReturn(AuthHelper.VALID_NUMBER); + when(account.getNumberOptional()).thenReturn(Optional.of((AuthHelper.VALID_NUMBER))); when(account.getAccountIdentifier()).thenReturn(AuthHelper.VALID_UUID); when(account.getIdentifier(IdentityType.ACI)).thenReturn(AuthHelper.VALID_UUID); when(account.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI); + when(account.getPhoneNumberIdentifierOptional()).thenReturn(Optional.of(AuthHelper.VALID_PNI)); when(account.getIdentifier(IdentityType.PNI)).thenReturn(AuthHelper.VALID_PNI); when(account.getPrimaryDevice()).thenReturn(primaryDevice); when(account.getDevice(anyByte())).thenReturn(Optional.empty()); @@ -214,11 +216,12 @@ class DeviceControllerTest { @ParameterizedTest @MethodSource @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - void linkDeviceAtomic(final boolean fetchesMessages, - final Optional apnRegistrationId, - final Optional gcmRegistrationId, - final Optional expectedApnsToken, - final Optional expectedGcmToken) throws LinkDeviceTokenAlreadyUsedException { + void linkDeviceAtomic(final boolean withPhoneNumber, + final boolean fetchesMessages, + final Optional apnRegistrationId, + final Optional gcmRegistrationId, + final Optional expectedApnsToken, + final Optional expectedGcmToken) throws LinkDeviceTokenAlreadyUsedException { final Device existingDevice = mock(Device.class); when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); @@ -238,8 +241,10 @@ class DeviceControllerTest { pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey()); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(aciIdentityKey); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(aciIdentityKey); + when(account.getPhoneNumberIdentityKey()).thenReturn( + Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey())) + .filter(_ -> withPhoneNumber)); when(accountsManager.checkDeviceLinkingToken(anyString())).thenReturn(Optional.of(AuthHelper.VALID_UUID)); @@ -252,12 +257,15 @@ class DeviceControllerTest { when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); - final DeviceAttributes deviceAttributes = new DeviceAttributes(fetchesMessages, 1234, 5678, null, - DeviceCapability.CAPABILITIES_REQUIRED_FOR_NEW_DEVICES); + final EnumSet capabilities = EnumSet.copyOf(DeviceCapability.CAPABILITIES_REQUIRED_FOR_NEW_DEVICES); + capabilities.add(DeviceCapability.OPTIONAL_PHONE_NUMBER); + + final DeviceAttributes deviceAttributes = new DeviceAttributes(fetchesMessages, 1234, withPhoneNumber ? 5678 : null, null, + capabilities); final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", deviceAttributes, - new DeviceActivationRequest(aciSignedPreKey, Optional.of(pniSignedPreKey), aciPqLastResortPreKey, Optional.of(pniPqLastResortPreKey), apnRegistrationId, gcmRegistrationId)); + new DeviceActivationRequest(aciSignedPreKey, Optional.of(pniSignedPreKey).filter(_ -> withPhoneNumber), aciPqLastResortPreKey, Optional.of(pniPqLastResortPreKey).filter(_ -> withPhoneNumber), apnRegistrationId, gcmRegistrationId)); final LinkDeviceResponse response = resources.getJerseyTest() .target("/v1/devices/link") @@ -286,10 +294,14 @@ class DeviceControllerTest { final String gcmToken = "gcm-token"; return Stream.of( - Arguments.of(true, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()), - Arguments.of(false, Optional.of(new ApnRegistrationId(apnsToken)), Optional.empty(), Optional.of(apnsToken), Optional.empty()), - Arguments.of(false, Optional.of(new ApnRegistrationId(apnsToken)), Optional.empty(), Optional.of(apnsToken), Optional.empty()), - Arguments.of(false, Optional.empty(), Optional.of(new GcmRegistrationId(gcmToken)), Optional.empty(), Optional.of(gcmToken)) + Arguments.of(true, true, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.of(true, false, Optional.of(new ApnRegistrationId(apnsToken)), Optional.empty(), Optional.of(apnsToken), Optional.empty()), + Arguments.of(true, false, Optional.of(new ApnRegistrationId(apnsToken)), Optional.empty(), Optional.of(apnsToken), Optional.empty()), + Arguments.of(true, false, Optional.empty(), Optional.of(new GcmRegistrationId(gcmToken)), Optional.empty(), Optional.of(gcmToken)), + Arguments.of(false, true, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()), + Arguments.of(false, false, Optional.of(new ApnRegistrationId(apnsToken)), Optional.empty(), Optional.of(apnsToken), Optional.empty()), + Arguments.of(false, false, Optional.of(new ApnRegistrationId(apnsToken)), Optional.empty(), Optional.of(apnsToken), Optional.empty()), + Arguments.of(false, false, Optional.empty(), Optional.of(new GcmRegistrationId(gcmToken)), Optional.empty(), Optional.of(gcmToken)) ); } @@ -320,8 +332,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); when(account.hasCapability(capability)).thenReturn(accountHasCapability); when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); @@ -376,8 +388,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); when(asyncCommands.set(any(), any(), any())).thenReturn(MockRedisFuture.completedFuture(null)); @@ -422,8 +434,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", new DeviceAttributes(false, 1234, 5678, null, null), @@ -458,8 +470,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); when(accountsManager.checkDeviceLinkingToken(anyString())).thenReturn(Optional.of(AuthHelper.VALID_UUID)); @@ -507,8 +519,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); when(commands.get(anyString())).thenReturn(""); @@ -558,8 +570,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); final LinkDeviceRequest request = new LinkDeviceRequest(deviceCode.token(), new DeviceAttributes(fetchesMessages, 1234, 5678, null, null), @@ -599,9 +611,8 @@ class DeviceControllerTest { final Device existingDevice = mock(Device.class); when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); when(account.getDevices()).thenReturn(List.of(existingDevice)); - - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(aciIdentityKey); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(pniIdentityKey); + when(account.getAccountIdentityKey()).thenReturn(aciIdentityKey); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(pniIdentityKey)); final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", new DeviceAttributes(true, 1234, 5678, null, null), @@ -637,6 +648,37 @@ class DeviceControllerTest { ); } + @Test + void linkDeviceSpuriousPniMaterial() { + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + when(accountsManager.checkDeviceLinkingToken(any())).thenReturn(Optional.of(AuthHelper.VALID_UUID)); + + final ECKeyPair aciIdentityKeyPair = ECKeyPair.generate(); + final ECKeyPair pniIdentityKeyPair = ECKeyPair.generate(); + final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey()); + final IdentityKey pniIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey()); + final ECSignedPreKey aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair); + final ECSignedPreKey pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair); + final KEMSignedPreKey aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); + final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); + + when(account.getAccountIdentityKey()).thenReturn(aciIdentityKey); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.empty()); + + final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", + new DeviceAttributes(true, 1234, 5678, null, null), + new DeviceActivationRequest(aciSignedPreKey, Optional.of(pniSignedPreKey), aciPqLastResortPreKey, Optional.of(pniPqLastResortPreKey), Optional.empty(), Optional.empty())); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(422, response.getStatus()); + } + } + @Test void linkDeviceAtomicMissingCapabilities() { final ECSignedPreKey aciSignedPreKey; @@ -658,8 +700,8 @@ class DeviceControllerTest { when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); when(account.getDevices()).thenReturn(List.of(existingDevice)); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); when(accountsManager.checkDeviceLinkingToken(anyString())).thenReturn(Optional.of(AuthHelper.VALID_UUID)); @@ -677,6 +719,46 @@ class DeviceControllerTest { } } + @Test + void linkDeviceAtomicMissingOptionalPhoneNumberCapability() { + final ECSignedPreKey aciSignedPreKey; + final ECSignedPreKey pniSignedPreKey; + final KEMSignedPreKey aciPqLastResortPreKey; + final KEMSignedPreKey pniPqLastResortPreKey; + + final ECKeyPair aciIdentityKeyPair = ECKeyPair.generate(); + final ECKeyPair pniIdentityKeyPair = ECKeyPair.generate(); + + aciSignedPreKey = KeysHelper.signedECPreKey(1, aciIdentityKeyPair); + pniSignedPreKey = KeysHelper.signedECPreKey(2, pniIdentityKeyPair); + aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); + pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); + + when(accountsManager.getByAccountIdentifier(AuthHelper.VALID_UUID)).thenReturn(Optional.of(account)); + + final Device existingDevice = mock(Device.class); + when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); + when(account.getDevices()).thenReturn(List.of(existingDevice)); + + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentifierOptional()).thenReturn(Optional.empty()); + + when(accountsManager.checkDeviceLinkingToken(anyString())).thenReturn(Optional.of(AuthHelper.VALID_UUID)); + + final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", + new DeviceAttributes(true, 1234, 5678, null, DeviceCapability.CAPABILITIES_REQUIRED_FOR_NEW_DEVICES), + new DeviceActivationRequest(aciSignedPreKey, Optional.of(pniSignedPreKey), aciPqLastResortPreKey, Optional.of(pniPqLastResortPreKey), Optional.empty(), Optional.empty())); + + try (final Response response = resources.getJerseyTest() + .target("/v1/devices/link") + .request() + .header("Authorization", AuthHelper.getProvisioningAuthHeader(AuthHelper.VALID_NUMBER, "password1")) + .put(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE))) { + + assertEquals(409, response.getStatus()); + } + } + @ParameterizedTest @MethodSource void linkDeviceAtomicInvalidSignature(final IdentityKey aciIdentityKey, @@ -691,8 +773,8 @@ class DeviceControllerTest { final Device existingDevice = mock(Device.class); when(existingDevice.getId()).thenReturn(Device.PRIMARY_ID); when(account.getDevices()).thenReturn(List.of(existingDevice)); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(aciIdentityKey); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(pniIdentityKey); + when(account.getAccountIdentityKey()).thenReturn(aciIdentityKey); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(pniIdentityKey)); when(accountsManager.checkDeviceLinkingToken(anyString())).thenReturn(Optional.of(AuthHelper.VALID_UUID)); @@ -752,8 +834,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", new DeviceAttributes(false, 1234, 5678, TestRandomUtil.nextBytes(512), null), @@ -786,8 +868,8 @@ class DeviceControllerTest { final KEMSignedPreKey pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); final IdentityKey aciIdentityKey = new IdentityKey(aciIdentityKeyPair.getPublicKey()); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(aciIdentityKey); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(aciIdentityKey); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); when(accountsManager.addDevice(any(), any(), any())).thenAnswer(invocation -> { final Account a = accountsManager.getByAccountIdentifier(invocation.getArgument(0)).orElseThrow(); @@ -848,8 +930,8 @@ class DeviceControllerTest { aciPqLastResortPreKey = KeysHelper.signedKEMPreKey(3, aciIdentityKeyPair); pniPqLastResortPreKey = KeysHelper.signedKEMPreKey(4, pniIdentityKeyPair); - when(account.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); - when(account.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(pniIdentityKeyPair.getPublicKey())); + when(account.getAccountIdentityKey()).thenReturn(new IdentityKey(aciIdentityKeyPair.getPublicKey())); + when(account.getPhoneNumberIdentityKey()).thenReturn(Optional.of(new IdentityKey(pniIdentityKeyPair.getPublicKey()))); final LinkDeviceRequest request = new LinkDeviceRequest("link-device-token", null, diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java index 878e95459..ac064bba0 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/controllers/RegistrationControllerTest.java @@ -654,46 +654,6 @@ class RegistrationControllerTest { } - @ParameterizedTest - @MethodSource - void atomicAccountCreationSuccess(final RegistrationRequest registrationRequest, - final IdentityKey expectedAciIdentityKey, - final IdentityKey expectedPniIdentityKey, - final DeviceSpec expectedDeviceSpec) { - - final UUID accountIdentifier = UUID.randomUUID(); - final UUID phoneNumberIdentifier = UUID.randomUUID(); - final Device device = mock(Device.class); - - final Account account = MockUtils.buildMock(Account.class, a -> { - when(a.getAccountIdentifier()).thenReturn(accountIdentifier); - when(a.getPhoneNumberIdentifierOptional()).thenReturn(Optional.of(phoneNumberIdentifier)); - when(a.getPrimaryDevice()).thenReturn(device); - }); - - when(accountsManager.create(any(), any(), any(), any(), any(), any())) - .thenReturn(account); - - final Invocation.Builder request = resources.getJerseyTest() - .target("/v1/registration") - .request() - .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); - - try (Response response = request.post(Entity.json(registrationRequest))) { - assertEquals(200, response.getStatus()); - final AccountIdentityResponse identityResponse = response.readEntity(AccountIdentityResponse.class); - assertEquals(accountIdentifier, identityResponse.uuid()); - } - - verify(accountsManager).create( - eq(NUMBER), - argThat(attributes -> accountAttributesEqual(attributes, registrationRequest.accountAttributes())), - eq(expectedAciIdentityKey), - eq(expectedPniIdentityKey), - eq(expectedDeviceSpec), - any()); - } - @ParameterizedTest @ValueSource(booleans = {true, false}) void reregistrationFlag(final boolean accountExists) { @@ -1177,6 +1137,45 @@ class RegistrationControllerTest { && Arrays.equals(a.recoveryPassword().orElse(null), b.recoveryPassword().orElse(null)); } + @ParameterizedTest + @MethodSource + void atomicAccountCreationSuccess(final RegistrationRequest registrationRequest, + final IdentityKey expectedAciIdentityKey, + final IdentityKey expectedPniIdentityKey, + final DeviceSpec expectedDeviceSpec) throws InterruptedException { + + final UUID accountIdentifier = UUID.randomUUID(); + final UUID phoneNumberIdentifier = UUID.randomUUID(); + final Device device = mock(Device.class); + + final Account account = mock(Account.class); + when(account.getAccountIdentifier()).thenReturn(accountIdentifier); + when(account.getPhoneNumberIdentifierOptional()).thenReturn(Optional.of(phoneNumberIdentifier)); + when(account.getPrimaryDevice()).thenReturn(device); + + when(accountsManager.create(any(), any(), any(), any(), any(), any())) + .thenReturn(account); + + final Invocation.Builder request = resources.getJerseyTest() + .target("/v1/registration") + .request() + .header(HttpHeaders.AUTHORIZATION, AuthHelper.getProvisioningAuthHeader(NUMBER, PASSWORD)); + + try (Response response = request.post(Entity.json(registrationRequest))) { + assertEquals(200, response.getStatus()); + final AccountIdentityResponse identityResponse = response.readEntity(AccountIdentityResponse.class); + assertEquals(accountIdentifier, identityResponse.uuid()); + } + + verify(accountsManager).create( + eq(NUMBER), + argThat(attributes -> accountAttributesEqual(attributes, registrationRequest.accountAttributes())), + eq(expectedAciIdentityKey), + eq(expectedPniIdentityKey), + eq(expectedDeviceSpec), + any()); + } + private static List atomicAccountCreationSuccess() { final IdentityKey aciIdentityKey; final IdentityKey pniIdentityKey; @@ -1203,13 +1202,11 @@ class RegistrationControllerTest { final Set deviceCapabilities = DeviceCapability.CAPABILITIES_REQUIRED_FOR_NEW_DEVICES; final AccountAttributes fetchesMessagesAccountAttributes = - new AccountAttributes(true, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, deviceCapabilities, - null) + new AccountAttributes(true, registrationId, pniRegistrationId, deviceName, null, true, deviceCapabilities, null) .setUnidentifiedAccessKey(TestRandomUtil.nextBytes(16)); final AccountAttributes pushAccountAttributes = - new AccountAttributes(false, registrationId, pniRegistrationId, "test".getBytes(StandardCharsets.UTF_8), null, true, deviceCapabilities, - null) + new AccountAttributes(false, registrationId, pniRegistrationId, deviceName, null, true, deviceCapabilities, null) .setUnidentifiedAccessKey(TestRandomUtil.nextBytes(16)); final String apnsToken = "apns-token"; diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java index ff24aeebf..9b79924b6 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -994,15 +994,18 @@ class AccountsManagerTest { assertEquals(hasStorage, account.hasCapability(DeviceCapability.STORAGE)); } - @Test - void testAddDevice() throws LinkDeviceTokenAlreadyUsedException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testAddDevice(boolean accountHasPhoneNumber) throws LinkDeviceTokenAlreadyUsedException { final String phoneNumber = PhoneNumberUtil.getInstance().format(PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164); - final Account account = AccountsHelper.generateTestAccount(phoneNumber, List.of(generateTestDevice(CLOCK.millis()))); - final UUID aci = account.getIdentifier(IdentityType.ACI); - final UUID pni = account.getIdentifier(IdentityType.PNI); + final Account account = accountHasPhoneNumber + ? AccountsHelper.generateTestAccount(phoneNumber, List.of(generateTestDevice(CLOCK.millis()))) + : AccountsHelper.generateTestAccountNoPhoneNumber(List.of(generateTestDevice(CLOCK.millis()))); + final UUID aci = account.getAccountIdentifier(); + final Optional maybePni = account.getPhoneNumberIdentifierOptional(); account.setIdentityKey(new IdentityKey(ECKeyPair.generate().getPublicKey())); final byte nextDeviceId = account.getNextDeviceId(); @@ -1027,39 +1030,42 @@ class AccountsManagerTest { CLOCK.pin(CLOCK.instant().plusSeconds(60)); - final Pair updatedAccountAndDevice = accountsManager.addDevice(aci, new DeviceSpec( + final Pair updatedAccountAndDevice = accountsManager.addDevice( + aci, + new DeviceSpec( deviceNameCiphertext, password, signalAgent, deviceCapabilities, new DeviceIdentityInfo(aciRegistrationId, aciSignedPreKey, aciPqLastResortPreKey), - Optional.of(new DeviceIdentityInfo(pniRegistrationId, pniSignedPreKey, pniPqLastResortPreKey)), + Optional.of(new DeviceIdentityInfo(pniRegistrationId, pniSignedPreKey, pniPqLastResortPreKey)).filter(_ -> accountHasPhoneNumber), true, Optional.empty(), Optional.empty()), accountsManager.generateLinkDeviceToken(aci)); verify(keysManager).deleteSingleUsePreKeys(aci, nextDeviceId); - verify(keysManager).deleteSingleUsePreKeys(pni, nextDeviceId); + maybePni.ifPresent(pni -> verify(keysManager).deleteSingleUsePreKeys(pni, nextDeviceId)); verify(messagesManager).clear(aci, nextDeviceId); verify(keysManager).buildWriteItemsForNewDevice( aci, - Optional.of(pni), + maybePni, nextDeviceId, aciSignedPreKey, - Optional.of(pniSignedPreKey), + maybePni.map(_ -> pniSignedPreKey), aciPqLastResortPreKey, - Optional.of(pniPqLastResortPreKey)); + maybePni.map(_ ->pniPqLastResortPreKey)); + verifyNoMoreInteractions(keysManager); final Device device = updatedAccountAndDevice.second(); assertEquals(deviceNameCiphertext, device.getName()); assertTrue(device.getAuthTokenHash().verify(password)); assertEquals(signalAgent, device.getUserAgent()); assertEquals(Collections.emptySet(), device.getCapabilities()); - assertEquals(aciRegistrationId, device.getRegistrationId(IdentityType.ACI)); - assertEquals(pniRegistrationId, device.getRegistrationId(IdentityType.PNI)); + assertEquals(aciRegistrationId, device.getAccountRegistrationId()); + assertEquals(accountHasPhoneNumber ? Optional.of(pniRegistrationId) : Optional.empty(), device.getPhoneNumberIdentityRegistrationId()); assertTrue(device.getFetchesMessages()); assertNull(device.getApnId()); assertNull(device.getGcmId()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java index c01a6da75..10377c75e 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/util/AccountsHelper.java @@ -66,6 +66,14 @@ public class AccountsHelper { return account; } + public static Account generateTestAccountNoPhoneNumber(List devices) { + final Account account = new Account(); + account.setAccountIdentifier(UUID.randomUUID()); + devices.forEach(account::addDevice); + + return account; + } + public static void setupMockUpdate(final AccountsManager mockAccountsManager) { setupMockUpdate(mockAccountsManager, true); }