new /v2/accounts endpoint to distribute PNI key material without changing phone number

This commit is contained in:
Jonathan Klabunde Tomer
2023-04-21 12:20:57 -07:00
committed by GitHub
parent 4fb89360ce
commit 47ad5779ad
10 changed files with 658 additions and 68 deletions

View File

@@ -323,13 +323,38 @@ class AccountControllerTest {
final String pniIdentityKey = invocation.getArgument(2, String.class);
final UUID uuid = account.getUuid();
final UUID pni = number.equals(account.getNumber()) ? account.getPhoneNumberIdentifier() : UUID.randomUUID();
final List<Device> devices = account.getDevices();
final Account updatedAccount = mock(Account.class);
when(updatedAccount.getUuid()).thenReturn(uuid);
when(updatedAccount.getNumber()).thenReturn(number);
when(updatedAccount.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey);
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(UUID.randomUUID());
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni);
when(updatedAccount.getDevices()).thenReturn(devices);
for (long i = 1; i <= 3; i++) {
final Optional<Device> d = account.getDevice(i);
when(updatedAccount.getDevice(i)).thenReturn(d);
}
return updatedAccount;
});
when(changeNumberManager.updatePNIKeys(any(), any(), any(), any(), any())).thenAnswer((Answer<Account>) invocation -> {
final Account account = invocation.getArgument(0, Account.class);
final String pniIdentityKey = invocation.getArgument(1, String.class);
final String number = account.getNumber();
final UUID uuid = account.getUuid();
final UUID pni = account.getPhoneNumberIdentifier();
final List<Device> devices = account.getDevices();
final Account updatedAccount = mock(Account.class);
when(updatedAccount.getNumber()).thenReturn(number);
when(updatedAccount.getUuid()).thenReturn(uuid);
when(updatedAccount.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey);
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni);
when(updatedAccount.getDevices()).thenReturn(devices);
for (long i = 1; i <= 3; i++) {
@@ -1646,6 +1671,61 @@ class AccountControllerTest {
assertThat(accountIdentityResponse.pni()).isNotEqualTo(AuthHelper.VALID_PNI);
}
@Test
void testChangePhoneNumberSameNumberChangePrekeys() throws Exception {
final String code = "987654";
final String pniIdentityKey = "changed-pni-identity-key";
final byte[] sessionId = "session-id".getBytes(StandardCharsets.UTF_8);
Device device2 = mock(Device.class);
when(device2.getId()).thenReturn(2L);
when(device2.isEnabled()).thenReturn(true);
when(device2.getRegistrationId()).thenReturn(2);
Device device3 = mock(Device.class);
when(device3.getId()).thenReturn(3L);
when(device3.isEnabled()).thenReturn(true);
when(device3.getRegistrationId()).thenReturn(3);
when(AuthHelper.VALID_ACCOUNT.getDevices()).thenReturn(List.of(AuthHelper.VALID_DEVICE, device2, device3));
when(AuthHelper.VALID_ACCOUNT.getDevice(2L)).thenReturn(Optional.of(device2));
when(AuthHelper.VALID_ACCOUNT.getDevice(3L)).thenReturn(Optional.of(device3));
when(pendingAccountsManager.getCodeForNumber(AuthHelper.VALID_NUMBER)).thenReturn(
Optional.of(new StoredVerificationCode(null, System.currentTimeMillis(), "push", sessionId)));
when(registrationServiceClient.checkVerificationCode(any(), any(), any()))
.thenReturn(CompletableFuture.completedFuture(true));
var deviceMessages = List.of(
new IncomingMessage(1, 2, 2, "content2"),
new IncomingMessage(1, 3, 3, "content3"));
var deviceKeys = Map.of(1L, new SignedPreKey(), 2L, new SignedPreKey(), 3L, new SignedPreKey());
final Map<Long, Integer> registrationIds = Map.of(1L, 17, 2L, 47, 3L, 89);
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
.target("/v1/accounts/number")
.request()
.header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(new ChangePhoneNumberRequest(
AuthHelper.VALID_NUMBER, code, null,
pniIdentityKey, deviceMessages,
deviceKeys,
registrationIds),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(
eq(AuthHelper.VALID_ACCOUNT), eq(AuthHelper.VALID_NUMBER), any(), any(), any(), any());
verifyNoInteractions(rateLimiter);
verifyNoInteractions(pendingAccountsManager);
assertThat(accountIdentityResponse.uuid()).isEqualTo(AuthHelper.VALID_UUID);
assertThat(accountIdentityResponse.number()).isEqualTo(AuthHelper.VALID_NUMBER);
assertThat(accountIdentityResponse.pni()).isEqualTo(AuthHelper.VALID_PNI);
}
@Test
void testSetRegistrationLock() {
Response response =

View File

@@ -71,6 +71,7 @@ import org.whispersystems.textsecuregcm.entities.AccountDataReportResponse;
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
import org.whispersystems.textsecuregcm.entities.PhoneNumberDiscoverabilityRequest;
import org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest;
import org.whispersystems.textsecuregcm.entities.RegistrationServiceSession;
import org.whispersystems.textsecuregcm.entities.SignedPreKey;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
@@ -147,7 +148,11 @@ class AccountControllerV2Test {
when(updatedAccount.getUuid()).thenReturn(uuid);
when(updatedAccount.getNumber()).thenReturn(number);
when(updatedAccount.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey);
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(UUID.randomUUID());
if (number.equals(account.getNumber())) {
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(AuthHelper.VALID_PNI);
} else {
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(UUID.randomUUID());
}
when(updatedAccount.getDevices()).thenReturn(devices);
for (long i = 1; i <= 3; i++) {
@@ -187,6 +192,29 @@ class AccountControllerV2Test {
assertNotEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni());
}
@Test
void changeNumberSameNumber() throws Exception {
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
.target("/v2/accounts/number")
.request()
.header(HttpHeaders.AUTHORIZATION,
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.entity(
new ChangeNumberRequest(encodeSessionId("session"), null, AuthHelper.VALID_NUMBER, null,
"pni-identity-key",
Collections.emptyList(),
Collections.emptyMap(), Collections.emptyMap()),
MediaType.APPLICATION_JSON_TYPE), AccountIdentityResponse.class);
verify(changeNumberManager).changeNumber(eq(AuthHelper.VALID_ACCOUNT), eq(AuthHelper.VALID_NUMBER), any(), any(), any(),
any());
assertEquals(AuthHelper.VALID_UUID, accountIdentityResponse.uuid());
assertEquals(AuthHelper.VALID_NUMBER, accountIdentityResponse.number());
assertEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni());
}
@Test
void unprocessableRequestJson() {
final Invocation.Builder request = resources.getJerseyTest()
@@ -426,6 +454,144 @@ class AccountControllerV2Test {
}
}
@Nested
class PhoneNumberIdentityKeyDistribution {
@BeforeEach
void setUp() throws Exception {
when(changeNumberManager.updatePNIKeys(any(), any(), any(), any(), any())).thenAnswer(
(Answer<Account>) invocation -> {
final Account account = invocation.getArgument(0, Account.class);
final String pniIdentityKey = invocation.getArgument(1, String.class);
final UUID uuid = account.getUuid();
final UUID pni = account.getPhoneNumberIdentifier();
final String number = account.getNumber();
final List<Device> devices = account.getDevices();
final Account updatedAccount = mock(Account.class);
when(updatedAccount.getUuid()).thenReturn(uuid);
when(updatedAccount.getNumber()).thenReturn(number);
when(updatedAccount.getPhoneNumberIdentityKey()).thenReturn(pniIdentityKey);
when(updatedAccount.getPhoneNumberIdentifier()).thenReturn(pni);
when(updatedAccount.getDevices()).thenReturn(devices);
for (long i = 1; i <= 3; i++) {
final Optional<Device> d = account.getDevice(i);
when(updatedAccount.getDevice(i)).thenReturn(d);
}
return updatedAccount;
});
}
@Test
void pniKeyDistributionSuccess() throws Exception {
when(AuthHelper.VALID_ACCOUNT.isPniSupported()).thenReturn(true);
final AccountIdentityResponse accountIdentityResponse =
resources.getJerseyTest()
.target("/v2/accounts/phone_number_identity_key_distribution")
.request()
.header(HttpHeaders.AUTHORIZATION,
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD))
.put(Entity.json(requestJson()), AccountIdentityResponse.class);
verify(changeNumberManager).updatePNIKeys(eq(AuthHelper.VALID_ACCOUNT), eq("pni-identity-key"), any(), any(), any());
assertEquals(AuthHelper.VALID_UUID, accountIdentityResponse.uuid());
assertEquals(AuthHelper.VALID_NUMBER, accountIdentityResponse.number());
assertEquals(AuthHelper.VALID_PNI, accountIdentityResponse.pni());
}
@Test
void unprocessableRequestJson() {
final Invocation.Builder request = resources.getJerseyTest()
.target("/v2/accounts/phone_number_identity_key_distribution")
.request()
.header(HttpHeaders.AUTHORIZATION,
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD));
try (Response response = request.put(Entity.json(unprocessableJson()))) {
assertEquals(400, response.getStatus());
}
}
@Test
void missingBasicAuthorization() {
final Invocation.Builder request = resources.getJerseyTest()
.target("/v2/accounts/phone_number_identity_key_distribution")
.request();
try (Response response = request.put(Entity.json(requestJson()))) {
assertEquals(401, response.getStatus());
}
}
@Test
void invalidBasicAuthorization() {
final Invocation.Builder request = resources.getJerseyTest()
.target("/v2/accounts/phone_number_identity_key_distribution")
.request()
.header(HttpHeaders.AUTHORIZATION, "Basic but-invalid");
try (Response response = request.put(Entity.json(requestJson()))) {
assertEquals(401, response.getStatus());
}
}
@Test
void invalidRequestBody() {
final Invocation.Builder request = resources.getJerseyTest()
.target("/v2/accounts/phone_number_identity_key_distribution")
.request()
.header(HttpHeaders.AUTHORIZATION,
AuthHelper.getAuthHeader(AuthHelper.VALID_UUID, AuthHelper.VALID_PASSWORD));
try (Response response = request.put(Entity.json(invalidRequestJson()))) {
assertEquals(422, response.getStatus());
}
}
/**
* Valid request JSON for a {@link org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest}
*/
private static String requestJson() {
return """
{
"pniIdentityKey": "pni-identity-key",
"deviceMessages": [],
"devicePniSignedPrekeys": {},
"pniRegistrationIds": {}
}
""";
}
/**
* Request JSON in the shape of {@link org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest}, but that
* fails validation
*/
private static String invalidRequestJson() {
return """
{
"pniIdentityKey": null,
"deviceMessages": [],
"devicePniSignedPrekeys": {},
"pniRegistrationIds": {}
}
""";
}
/**
* Request JSON that cannot be marshalled into
* {@link org.whispersystems.textsecuregcm.entities.PhoneNumberIdentityKeyDistributionRequest}
*/
private static String unprocessableJson() {
return """
{
"pniIdentityKey": []
}
""";
}
}
@Nested
class PhoneNumberDiscoverability {