Add an integration test for changing phone numbers
This commit is contained in:
parent
664fb23e97
commit
b7e986f43c
|
@ -72,14 +72,12 @@ public final class Operations {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TestUser newRegisteredUser(final String number) {
|
public static TestUser newRegisteredUser(final String number) {
|
||||||
final byte[] registrationPassword = randomBytes(32);
|
final byte[] registrationPassword = populateRandomRecoveryPassword(number);
|
||||||
final String accountPassword = Base64.getEncoder().encodeToString(randomBytes(32));
|
final String accountPassword = Base64.getEncoder().encodeToString(randomBytes(32));
|
||||||
|
|
||||||
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
final TestUser user = TestUser.create(number, accountPassword, registrationPassword);
|
||||||
final AccountAttributes accountAttributes = user.accountAttributes();
|
final AccountAttributes accountAttributes = user.accountAttributes();
|
||||||
|
|
||||||
INTEGRATION_TOOLS.populateRecoveryPassword(number, registrationPassword).join();
|
|
||||||
|
|
||||||
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
final ECKeyPair aciIdentityKeyPair = Curve.generateKeyPair();
|
||||||
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
|
||||||
|
@ -108,6 +106,7 @@ public final class Operations {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PrescribedVerificationNumber(String number, String verificationCode) {}
|
public record PrescribedVerificationNumber(String number, String verificationCode) {}
|
||||||
|
|
||||||
public static PrescribedVerificationNumber prescribedVerificationNumber() {
|
public static PrescribedVerificationNumber prescribedVerificationNumber() {
|
||||||
return new PrescribedVerificationNumber(
|
return new PrescribedVerificationNumber(
|
||||||
CONFIG.prescribedRegistrationNumber(),
|
CONFIG.prescribedRegistrationNumber(),
|
||||||
|
@ -123,6 +122,13 @@ public final class Operations {
|
||||||
.orElseThrow(() -> new RuntimeException("push challenge not found for the verification session"));
|
.orElseThrow(() -> new RuntimeException("push challenge not found for the verification session"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] populateRandomRecoveryPassword(final String number) {
|
||||||
|
final byte[] recoveryPassword = randomBytes(32);
|
||||||
|
INTEGRATION_TOOLS.populateRecoveryPassword(number, recoveryPassword).join();
|
||||||
|
|
||||||
|
return recoveryPassword;
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> T sendEmptyRequestAuthenticated(
|
public static <T> T sendEmptyRequestAuthenticated(
|
||||||
final String endpoint,
|
final String endpoint,
|
||||||
final String method,
|
final String method,
|
||||||
|
@ -329,15 +335,15 @@ public final class Operations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ECSignedPreKey generateSignedECPreKey(long id, final ECKeyPair identityKeyPair) {
|
public static ECSignedPreKey generateSignedECPreKey(final long id, final ECKeyPair identityKeyPair) {
|
||||||
final ECPublicKey pubKey = Curve.generateKeyPair().getPublicKey();
|
final ECPublicKey pubKey = Curve.generateKeyPair().getPublicKey();
|
||||||
final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
final byte[] signature = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
||||||
return new ECSignedPreKey(id, pubKey, sig);
|
return new ECSignedPreKey(id, pubKey, signature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static KEMSignedPreKey generateSignedKEMPreKey(long id, final ECKeyPair identityKeyPair) {
|
public static KEMSignedPreKey generateSignedKEMPreKey(final long id, final ECKeyPair identityKeyPair) {
|
||||||
final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey();
|
final KEMPublicKey pubKey = KEMKeyPair.generate(KEMKeyType.KYBER_1024).getPublicKey();
|
||||||
final byte[] sig = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
final byte[] signature = identityKeyPair.getPrivateKey().calculateSignature(pubKey.serialize());
|
||||||
return new KEMSignedPreKey(id, pubKey, sig);
|
return new KEMSignedPreKey(id, pubKey, signature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,27 +6,35 @@
|
||||||
package org.signal.integration;
|
package org.signal.integration;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.apache.http.HttpStatus;
|
import org.apache.http.HttpStatus;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.signal.libsignal.protocol.IdentityKey;
|
||||||
|
import org.signal.libsignal.protocol.ecc.Curve;
|
||||||
|
import org.signal.libsignal.protocol.ecc.ECKeyPair;
|
||||||
import org.signal.libsignal.usernames.BaseUsernameException;
|
import org.signal.libsignal.usernames.BaseUsernameException;
|
||||||
import org.signal.libsignal.usernames.Username;
|
import org.signal.libsignal.usernames.Username;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentifierResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
import org.whispersystems.textsecuregcm.entities.AccountIdentityResponse;
|
||||||
|
import org.whispersystems.textsecuregcm.entities.ChangeNumberRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
import org.whispersystems.textsecuregcm.entities.ConfirmUsernameHashRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashRequest;
|
||||||
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
import org.whispersystems.textsecuregcm.entities.ReserveUsernameHashResponse;
|
||||||
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
import org.whispersystems.textsecuregcm.entities.UsernameHashResponse;
|
||||||
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||||
|
import org.whispersystems.textsecuregcm.storage.Device;
|
||||||
|
|
||||||
public class AccountTest {
|
public class AccountTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateAccount() throws Exception {
|
public void testCreateAccount() {
|
||||||
final TestUser user = Operations.newRegisteredUser("+19995550101");
|
final TestUser user = Operations.newRegisteredUser("+19995550101");
|
||||||
try {
|
try {
|
||||||
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
||||||
|
@ -39,7 +47,7 @@ public class AccountTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateAccountAtomic() throws Exception {
|
public void testCreateAccountAtomic() {
|
||||||
final TestUser user = Operations.newRegisteredUser("+19995550201");
|
final TestUser user = Operations.newRegisteredUser("+19995550201");
|
||||||
try {
|
try {
|
||||||
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
final Pair<Integer, AccountIdentityResponse> execute = Operations.apiGet("/v1/accounts/whoami")
|
||||||
|
@ -51,6 +59,33 @@ public class AccountTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void changePhoneNumber() {
|
||||||
|
final TestUser user = Operations.newRegisteredUser("+19995550301");
|
||||||
|
final String targetNumber = "+19995550302";
|
||||||
|
|
||||||
|
final ECKeyPair pniIdentityKeyPair = Curve.generateKeyPair();
|
||||||
|
|
||||||
|
final ChangeNumberRequest changeNumberRequest = new ChangeNumberRequest(null,
|
||||||
|
Operations.populateRandomRecoveryPassword(targetNumber),
|
||||||
|
targetNumber,
|
||||||
|
null,
|
||||||
|
new IdentityKey(pniIdentityKeyPair.getPublicKey()),
|
||||||
|
Collections.emptyList(),
|
||||||
|
Map.of(Device.PRIMARY_ID, Operations.generateSignedECPreKey(1, pniIdentityKeyPair)),
|
||||||
|
Map.of(Device.PRIMARY_ID, Operations.generateSignedKEMPreKey(2, pniIdentityKeyPair)),
|
||||||
|
Map.of(Device.PRIMARY_ID, 17));
|
||||||
|
|
||||||
|
final AccountIdentityResponse accountIdentityResponse =
|
||||||
|
Operations.apiPut("/v2/accounts/number", changeNumberRequest)
|
||||||
|
.authorized(user)
|
||||||
|
.executeExpectSuccess(AccountIdentityResponse.class);
|
||||||
|
|
||||||
|
assertEquals(user.aciUuid(), accountIdentityResponse.uuid());
|
||||||
|
assertNotEquals(user.pniUuid(), accountIdentityResponse.pni());
|
||||||
|
assertEquals(targetNumber, accountIdentityResponse.number());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUsernameOperations() throws Exception {
|
public void testUsernameOperations() throws Exception {
|
||||||
final TestUser user = Operations.newRegisteredUser("+19995550102");
|
final TestUser user = Operations.newRegisteredUser("+19995550102");
|
||||||
|
|
|
@ -102,9 +102,9 @@ public class AccountControllerV2 {
|
||||||
name = "Retry-After",
|
name = "Retry-After",
|
||||||
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
|
description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed"))
|
||||||
public AccountIdentityResponse changeNumber(@Mutable @Auth final AuthenticatedDevice authenticatedDevice,
|
public AccountIdentityResponse changeNumber(@Mutable @Auth final AuthenticatedDevice authenticatedDevice,
|
||||||
@NotNull @Valid final ChangeNumberRequest request, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString,
|
@NotNull @Valid final ChangeNumberRequest request,
|
||||||
@Context final ContainerRequestContext requestContext)
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgentString,
|
||||||
throws RateLimitExceededException, InterruptedException {
|
@Context final ContainerRequestContext requestContext) throws RateLimitExceededException, InterruptedException {
|
||||||
|
|
||||||
if (!authenticatedDevice.getAuthenticatedDevice().isPrimary()) {
|
if (!authenticatedDevice.getAuthenticatedDevice().isPrimary()) {
|
||||||
throw new ForbiddenException();
|
throw new ForbiddenException();
|
||||||
|
|
|
@ -7,30 +7,30 @@ package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter;
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter;
|
||||||
|
|
||||||
public record AccountIdentityResponse(
|
public record AccountIdentityResponse(
|
||||||
@Schema(description="the account identifier for this account")
|
@Schema(description = "the account identifier for this account")
|
||||||
UUID uuid,
|
UUID uuid,
|
||||||
|
|
||||||
@Schema(description="the phone number associated with this account")
|
@Schema(description = "the phone number associated with this account")
|
||||||
String number,
|
String number,
|
||||||
|
|
||||||
@Schema(description="the account identifier for this account's phone-number identity")
|
@Schema(description = "the account identifier for this account's phone-number identity")
|
||||||
UUID pni,
|
UUID pni,
|
||||||
|
|
||||||
@Schema(description="a hash of this account's username, if set")
|
@Schema(description = "a hash of this account's username, if set")
|
||||||
@JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class)
|
@JsonSerialize(using = ByteArrayBase64UrlAdapter.Serializing.class)
|
||||||
@JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class)
|
@JsonDeserialize(using = ByteArrayBase64UrlAdapter.Deserializing.class)
|
||||||
@Nullable byte[] usernameHash,
|
@Nullable byte[] usernameHash,
|
||||||
|
|
||||||
@Schema(description="this account's username link handle, if set")
|
@Schema(description = "this account's username link handle, if set")
|
||||||
@Nullable UUID usernameLinkHandle,
|
@Nullable UUID usernameLinkHandle,
|
||||||
|
|
||||||
@Schema(description="whether any of this account's devices support storage")
|
@Schema(description = "whether any of this account's devices support storage")
|
||||||
boolean storageCapable,
|
boolean storageCapable,
|
||||||
|
|
||||||
@Schema(description = "entitlements for this account and their current expirations")
|
@Schema(description = "entitlements for this account and their current expirations")
|
||||||
|
|
Loading…
Reference in New Issue