Add an authentication-required gRPC service for working with accounts

This commit is contained in:
Jon Chambers
2023-10-25 14:47:20 -04:00
committed by GitHub
parent 3d92e5b8a9
commit 54bc3bce96
10 changed files with 1358 additions and 2 deletions

View File

@@ -0,0 +1,731 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.grpc;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import java.time.Duration;
import java.util.HexFormat;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.signal.chat.account.AccountsGrpc;
import org.signal.chat.account.ClearRegistrationLockRequest;
import org.signal.chat.account.ClearRegistrationLockResponse;
import org.signal.chat.account.ConfigureUnidentifiedAccessRequest;
import org.signal.chat.account.ConfirmUsernameHashRequest;
import org.signal.chat.account.ConfirmUsernameHashResponse;
import org.signal.chat.account.DeleteAccountRequest;
import org.signal.chat.account.DeleteAccountResponse;
import org.signal.chat.account.DeleteUsernameHashRequest;
import org.signal.chat.account.DeleteUsernameLinkRequest;
import org.signal.chat.account.GetAccountIdentityRequest;
import org.signal.chat.account.GetAccountIdentityResponse;
import org.signal.chat.account.ReserveUsernameHashError;
import org.signal.chat.account.ReserveUsernameHashErrorType;
import org.signal.chat.account.ReserveUsernameHashRequest;
import org.signal.chat.account.ReserveUsernameHashResponse;
import org.signal.chat.account.SetDiscoverableByPhoneNumberRequest;
import org.signal.chat.account.SetRegistrationLockRequest;
import org.signal.chat.account.SetRegistrationLockResponse;
import org.signal.chat.account.SetRegistrationRecoveryPasswordRequest;
import org.signal.chat.account.SetUsernameLinkRequest;
import org.signal.chat.account.SetUsernameLinkResponse;
import org.signal.chat.common.AccountIdentifiers;
import org.signal.libsignal.usernames.BaseUsernameException;
import org.whispersystems.textsecuregcm.auth.SaltedTokenHash;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
import org.whispersystems.textsecuregcm.entities.EncryptedUsername;
import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
import org.whispersystems.textsecuregcm.limits.RateLimiter;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.Device;
import org.whispersystems.textsecuregcm.storage.RegistrationRecoveryPasswordsManager;
import org.whispersystems.textsecuregcm.storage.UsernameHashNotAvailableException;
import org.whispersystems.textsecuregcm.storage.UsernameReservationNotFoundException;
import org.whispersystems.textsecuregcm.util.UUIDUtil;
import org.whispersystems.textsecuregcm.util.UsernameHashZkProofVerifier;
import reactor.core.publisher.Mono;
class AccountsGrpcServiceTest extends SimpleBaseGrpcTest<AccountsGrpcService, AccountsGrpc.AccountsBlockingStub> {
@Mock
private AccountsManager accountsManager;
@Mock
private RateLimiter rateLimiter;
@Mock
private UsernameHashZkProofVerifier usernameHashZkProofVerifier;
@Mock
private RegistrationRecoveryPasswordsManager registrationRecoveryPasswordsManager;
@Override
protected AccountsGrpcService createServiceBeforeEachTest() {
when(accountsManager.updateAsync(any(), any()))
.thenAnswer(invocation -> {
final Account account = invocation.getArgument(0);
final Consumer<Account> updater = invocation.getArgument(1);
updater.accept(account);
return CompletableFuture.completedFuture(account);
});
final RateLimiters rateLimiters = mock(RateLimiters.class);
when(rateLimiters.getUsernameReserveLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getUsernameSetLimiter()).thenReturn(rateLimiter);
when(rateLimiters.getUsernameLinkOperationLimiter()).thenReturn(rateLimiter);
when(rateLimiter.validateReactive(any(UUID.class))).thenReturn(Mono.empty());
when(rateLimiter.validateReactive(anyString())).thenReturn(Mono.empty());
when(registrationRecoveryPasswordsManager.storeForCurrentNumber(anyString(), any()))
.thenReturn(CompletableFuture.completedFuture(null));
return new AccountsGrpcService(accountsManager,
rateLimiters,
usernameHashZkProofVerifier,
registrationRecoveryPasswordsManager);
}
@Test
void getAccountIdentity() {
final UUID phoneNumberIdentifier = UUID.randomUUID();
final String e164 = PhoneNumberUtil.getInstance().format(
PhoneNumberUtil.getInstance().getExampleNumber("US"), PhoneNumberUtil.PhoneNumberFormat.E164);
final byte[] usernameHash = new byte[32];
ThreadLocalRandom.current().nextBytes(usernameHash);
final Account account = mock(Account.class);
when(account.getUuid()).thenReturn(AUTHENTICATED_ACI);
when(account.getPhoneNumberIdentifier()).thenReturn(phoneNumberIdentifier);
when(account.getNumber()).thenReturn(e164);
when(account.getUsernameHash()).thenReturn(Optional.of(usernameHash));
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final GetAccountIdentityResponse expectedResponse = GetAccountIdentityResponse.newBuilder()
.setAccountIdentifiers(AccountIdentifiers.newBuilder()
.addServiceIdentifiers(ServiceIdentifierUtil.toGrpcServiceIdentifier(new AciServiceIdentifier(AUTHENTICATED_ACI)))
.addServiceIdentifiers(ServiceIdentifierUtil.toGrpcServiceIdentifier(new PniServiceIdentifier(phoneNumberIdentifier)))
.setE164(e164)
.setUsernameHash(ByteString.copyFrom(usernameHash))
.build())
.build();
assertEquals(expectedResponse, authenticatedServiceStub().getAccountIdentity(GetAccountIdentityRequest.newBuilder().build()));
}
@Test
void deleteAccount() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accountsManager.delete(any(), any()))
.thenReturn(CompletableFuture.completedFuture(null));
final DeleteAccountResponse ignored =
authenticatedServiceStub().deleteAccount(DeleteAccountRequest.newBuilder().build());
verify(accountsManager).delete(account, AccountsManager.DeletionReason.USER_REQUEST);
}
@Test
void deleteAccountLinkedDevice() {
getMockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, Device.PRIMARY_ID + 1);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.PERMISSION_DENIED,
() -> authenticatedServiceStub().deleteAccount(DeleteAccountRequest.newBuilder().build()));
verify(accountsManager, never()).delete(any(), any());
}
@Test
void setRegistrationLock() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] registrationLockSecret = new byte[32];
ThreadLocalRandom.current().nextBytes(registrationLockSecret);
final SetRegistrationLockResponse ignored =
authenticatedServiceStub().setRegistrationLock(SetRegistrationLockRequest.newBuilder()
.setRegistrationLock(ByteString.copyFrom(registrationLockSecret))
.build());
final ArgumentCaptor<String> hashCaptor = ArgumentCaptor.forClass(String.class);
final ArgumentCaptor<String> saltCaptor = ArgumentCaptor.forClass(String.class);
verify(account).setRegistrationLock(hashCaptor.capture(), saltCaptor.capture());
final SaltedTokenHash registrationLock = new SaltedTokenHash(hashCaptor.getValue(), saltCaptor.getValue());
assertTrue(registrationLock.verify(HexFormat.of().formatHex(registrationLockSecret)));
}
@Test
void setRegistrationLockEmptySecret() {
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().setRegistrationLock(SetRegistrationLockRequest.newBuilder()
.build()));
verify(accountsManager, never()).updateAsync(any(), any());
}
@Test
void setRegistrationLockLinkedDevice() {
getMockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, Device.PRIMARY_ID + 1);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.PERMISSION_DENIED,
() -> authenticatedServiceStub().setRegistrationLock(SetRegistrationLockRequest.newBuilder()
.build()));
verify(accountsManager, never()).updateAsync(any(), any());
}
@Test
void clearRegistrationLock() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final ClearRegistrationLockResponse ignored =
authenticatedServiceStub().clearRegistrationLock(ClearRegistrationLockRequest.newBuilder().build());
verify(account).setRegistrationLock(null, null);
}
@Test
void clearRegistrationLockLinkedDevice() {
getMockAuthenticationInterceptor().setAuthenticatedDevice(AUTHENTICATED_ACI, Device.PRIMARY_ID + 1);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.PERMISSION_DENIED,
() -> authenticatedServiceStub().clearRegistrationLock(ClearRegistrationLockRequest.newBuilder().build()));
verify(accountsManager, never()).updateAsync(any(), any());
}
@Test
void reserveUsernameHash() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
when(accountsManager.reserveUsernameHash(any(), any()))
.thenAnswer(invocation -> {
final List<byte[]> usernameHashes = invocation.getArgument(1);
return CompletableFuture.completedFuture(
new AccountsManager.UsernameReservation(invocation.getArgument(0), usernameHashes.get(0)));
});
final ReserveUsernameHashResponse expectedResponse = ReserveUsernameHashResponse.newBuilder()
.setUsernameHash(ByteString.copyFrom(usernameHash))
.build();
assertEquals(expectedResponse,
authenticatedServiceStub().reserveUsernameHash(ReserveUsernameHashRequest.newBuilder()
.addUsernameHashes(ByteString.copyFrom(usernameHash))
.build()));
}
@Test
void reserveUsernameHashNotAvailable() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
when(accountsManager.reserveUsernameHash(any(), any()))
.thenReturn(CompletableFuture.failedFuture(new UsernameHashNotAvailableException()));
final ReserveUsernameHashResponse expectedResponse = ReserveUsernameHashResponse.newBuilder()
.setError(ReserveUsernameHashError.newBuilder()
.setErrorType(ReserveUsernameHashErrorType.RESERVE_USERNAME_HASH_ERROR_TYPE_NO_HASHES_AVAILABLE)
.build())
.build();
assertEquals(expectedResponse,
authenticatedServiceStub().reserveUsernameHash(ReserveUsernameHashRequest.newBuilder()
.addUsernameHashes(ByteString.copyFrom(usernameHash))
.build()));
}
@Test
void reserveUsernameHashNoHashes() {
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().reserveUsernameHash(ReserveUsernameHashRequest.newBuilder().build()));
}
@Test
void reserveUsernameHashTooManyHashes() {
final ReserveUsernameHashRequest.Builder requestBuilder = ReserveUsernameHashRequest.newBuilder();
for (int i = 0; i < AccountController.MAXIMUM_USERNAME_HASHES_LIST_LENGTH + 1; i++) {
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
requestBuilder.addUsernameHashes(ByteString.copyFrom(usernameHash));
}
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().reserveUsernameHash(requestBuilder.build()));
}
@Test
void reserveUsernameHashBadHashLength() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH + 1];
ThreadLocalRandom.current().nextBytes(usernameHash);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().reserveUsernameHash(ReserveUsernameHashRequest.newBuilder()
.addUsernameHashes(ByteString.copyFrom(usernameHash))
.build()));
}
@Test
void reserveUsernameHashRateLimited() {
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
final Duration retryAfter = Duration.ofMinutes(3);
when(rateLimiter.validateReactive(any(UUID.class)))
.thenReturn(Mono.error(new RateLimitExceededException(retryAfter, false)));
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
() -> authenticatedServiceStub().reserveUsernameHash(ReserveUsernameHashRequest.newBuilder()
.addUsernameHashes(ByteString.copyFrom(usernameHash))
.build()),
accountsManager);
}
@Test
void confirmUsernameHash() {
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
final byte[] usernameCiphertext = new byte[32];
ThreadLocalRandom.current().nextBytes(usernameCiphertext);
final byte[] zkProof = new byte[32];
ThreadLocalRandom.current().nextBytes(zkProof);
final UUID linkHandle = UUID.randomUUID();
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accountsManager.confirmReservedUsernameHash(account, usernameHash, usernameCiphertext))
.thenAnswer(invocation -> {
final Account updatedAccount = mock(Account.class);
when(updatedAccount.getUsernameHash()).thenReturn(Optional.of(usernameHash));
when(updatedAccount.getUsernameLinkHandle()).thenReturn(linkHandle);
return CompletableFuture.completedFuture(updatedAccount);
});
final ConfirmUsernameHashResponse expectedResponse = ConfirmUsernameHashResponse.newBuilder()
.setUsernameHash(ByteString.copyFrom(usernameHash))
.setUsernameLinkHandle(UUIDUtil.toByteString(linkHandle))
.build();
assertEquals(expectedResponse,
authenticatedServiceStub().confirmUsernameHash(ConfirmUsernameHashRequest.newBuilder()
.setUsernameHash(ByteString.copyFrom(usernameHash))
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
.setZkProof(ByteString.copyFrom(zkProof))
.build()));
}
@ParameterizedTest
@MethodSource
void confirmUsernameHashConfirmationException(final Exception confirmationException, final Status expectedStatus) {
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
final byte[] usernameCiphertext = new byte[32];
ThreadLocalRandom.current().nextBytes(usernameCiphertext);
final byte[] zkProof = new byte[32];
ThreadLocalRandom.current().nextBytes(zkProof);
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accountsManager.confirmReservedUsernameHash(any(), any(), any()))
.thenReturn(CompletableFuture.failedFuture(confirmationException));
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(expectedStatus,
() -> authenticatedServiceStub().confirmUsernameHash(ConfirmUsernameHashRequest.newBuilder()
.setUsernameHash(ByteString.copyFrom(usernameHash))
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
.setZkProof(ByteString.copyFrom(zkProof))
.build()));
}
private static Stream<Arguments> confirmUsernameHashConfirmationException() {
return Stream.of(
Arguments.of(new UsernameHashNotAvailableException(), Status.NOT_FOUND),
Arguments.of(new UsernameReservationNotFoundException(), Status.FAILED_PRECONDITION)
);
}
@Test
void confirmUsernameHashInvalidProof() throws BaseUsernameException {
final byte[] usernameHash = new byte[AccountController.USERNAME_HASH_LENGTH];
ThreadLocalRandom.current().nextBytes(usernameHash);
final byte[] usernameCiphertext = new byte[32];
ThreadLocalRandom.current().nextBytes(usernameCiphertext);
final byte[] zkProof = new byte[32];
ThreadLocalRandom.current().nextBytes(zkProof);
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
doThrow(BaseUsernameException.class).when(usernameHashZkProofVerifier).verifyProof(any(), any());
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().confirmUsernameHash(ConfirmUsernameHashRequest.newBuilder()
.setUsernameHash(ByteString.copyFrom(usernameHash))
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
.setZkProof(ByteString.copyFrom(zkProof))
.build()));
}
@ParameterizedTest
@MethodSource
void confirmUsernameHashInvalidArgument(final ConfirmUsernameHashRequest request) {
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().confirmUsernameHash(request));
}
private static List<ConfirmUsernameHashRequest> confirmUsernameHashInvalidArgument() {
final ConfirmUsernameHashRequest prototypeRequest = ConfirmUsernameHashRequest.newBuilder()
.setUsernameHash(ByteString.copyFrom(new byte[AccountController.USERNAME_HASH_LENGTH]))
.setUsernameCiphertext(ByteString.copyFrom(new byte[AccountController.MAXIMUM_USERNAME_CIPHERTEXT_LENGTH]))
.setZkProof(ByteString.copyFrom(new byte[32]))
.build();
return List.of(
// No username hash
ConfirmUsernameHashRequest.newBuilder(prototypeRequest)
.clearUsernameHash()
.build(),
// Incorrect username hash length
ConfirmUsernameHashRequest.newBuilder(prototypeRequest)
.setUsernameHash(ByteString.copyFrom(new byte[AccountController.USERNAME_HASH_LENGTH + 1]))
.build(),
// No username ciphertext
ConfirmUsernameHashRequest.newBuilder(prototypeRequest)
.clearUsernameCiphertext()
.build(),
// Excessive username ciphertext length
ConfirmUsernameHashRequest.newBuilder(prototypeRequest)
.setUsernameCiphertext(ByteString.copyFrom(new byte[AccountController.MAXIMUM_USERNAME_CIPHERTEXT_LENGTH + 1]))
.build(),
// No ZK proof
ConfirmUsernameHashRequest.newBuilder(prototypeRequest)
.clearZkProof()
.build());
}
@Test
void deleteUsernameHash() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
when(accountsManager.clearUsernameHash(account)).thenReturn(CompletableFuture.completedFuture(account));
assertDoesNotThrow(() ->
authenticatedServiceStub().deleteUsernameHash(DeleteUsernameHashRequest.newBuilder().build()));
verify(accountsManager).clearUsernameHash(account);
}
@Test
void setUsernameLink() {
final Account account = mock(Account.class);
when(account.getUsernameHash()).thenReturn(Optional.of(new byte[AccountController.USERNAME_HASH_LENGTH]));
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] usernameCiphertext = new byte[EncryptedUsername.MAX_SIZE];
ThreadLocalRandom.current().nextBytes(usernameCiphertext);
final SetUsernameLinkResponse response =
authenticatedServiceStub().setUsernameLink(SetUsernameLinkRequest.newBuilder()
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
.build());
final ArgumentCaptor<UUID> linkHandleCaptor = ArgumentCaptor.forClass(UUID.class);
verify(account).setUsernameLinkDetails(linkHandleCaptor.capture(), eq(usernameCiphertext));
final SetUsernameLinkResponse expectedResponse = SetUsernameLinkResponse.newBuilder()
.setUsernameLinkHandle(UUIDUtil.toByteString(linkHandleCaptor.getValue()))
.build();
assertEquals(expectedResponse, response);
}
@Test
void setUsernameLinkMissingUsernameHash() {
final Account account = mock(Account.class);
when(account.getUsernameHash()).thenReturn(Optional.empty());
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] usernameCiphertext = new byte[EncryptedUsername.MAX_SIZE];
ThreadLocalRandom.current().nextBytes(usernameCiphertext);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.FAILED_PRECONDITION,
() -> authenticatedServiceStub().setUsernameLink(SetUsernameLinkRequest.newBuilder()
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
.build()));
}
@ParameterizedTest
@MethodSource
void setUsernameLinkIllegalCiphertext(final SetUsernameLinkRequest request) {
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().setUsernameLink(request));
}
private static List<SetUsernameLinkRequest> setUsernameLinkIllegalCiphertext() {
return List.of(
// No username ciphertext
SetUsernameLinkRequest.newBuilder().build(),
// Excessive username ciphertext
SetUsernameLinkRequest.newBuilder()
.setUsernameCiphertext(ByteString.copyFrom(new byte[EncryptedUsername.MAX_SIZE + 1]))
.build()
);
}
@Test
void setUsernameLinkRateLimited() {
final Duration retryAfter = Duration.ofSeconds(97);
when(rateLimiter.validateReactive(any(UUID.class)))
.thenReturn(Mono.error(new RateLimitExceededException(retryAfter, false)));
final byte[] usernameCiphertext = new byte[EncryptedUsername.MAX_SIZE];
ThreadLocalRandom.current().nextBytes(usernameCiphertext);
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
() -> authenticatedServiceStub().setUsernameLink(SetUsernameLinkRequest.newBuilder()
.setUsernameCiphertext(ByteString.copyFrom(usernameCiphertext))
.build()),
accountsManager);
}
@Test
void deleteUsernameLink() {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
assertDoesNotThrow(
() -> authenticatedServiceStub().deleteUsernameLink(DeleteUsernameLinkRequest.newBuilder().build()));
verify(account).setUsernameLinkDetails(null, null);
}
@Test
void deleteUsernameLinkRateLimited() {
final Duration retryAfter = Duration.ofSeconds(11);
when(rateLimiter.validateReactive(any(UUID.class)))
.thenReturn(Mono.error(new RateLimitExceededException(retryAfter, false)));
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
() -> authenticatedServiceStub().deleteUsernameLink(DeleteUsernameLinkRequest.newBuilder().build()),
accountsManager);
}
@ParameterizedTest
@MethodSource
void configureUnidentifiedAccess(final boolean unrestrictedUnidentifiedAccess,
final byte[] unidentifiedAccessKey,
final byte[] expectedUnidentifiedAccessKey) {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
assertDoesNotThrow(() -> authenticatedServiceStub().configureUnidentifiedAccess(ConfigureUnidentifiedAccessRequest.newBuilder()
.setAllowUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess)
.setUnidentifiedAccessKey(ByteString.copyFrom(unidentifiedAccessKey))
.build()));
verify(account).setUnrestrictedUnidentifiedAccess(unrestrictedUnidentifiedAccess);
verify(account).setUnidentifiedAccessKey(expectedUnidentifiedAccessKey);
}
private static Stream<Arguments> configureUnidentifiedAccess() {
final byte[] unidentifiedAccessKey = new byte[UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH];
ThreadLocalRandom.current().nextBytes(unidentifiedAccessKey);
return Stream.of(
Arguments.of(true, new byte[0], null),
Arguments.of(true, unidentifiedAccessKey, null),
Arguments.of(false, unidentifiedAccessKey, unidentifiedAccessKey)
);
}
@ParameterizedTest
@MethodSource
void configureUnidentifiedAccessIllegalArguments(final ConfigureUnidentifiedAccessRequest request) {
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().configureUnidentifiedAccess(request));
}
private static List<ConfigureUnidentifiedAccessRequest> configureUnidentifiedAccessIllegalArguments() {
return List.of(
// No key and no unrestricted unidentified access
ConfigureUnidentifiedAccessRequest.newBuilder().build(),
// Key with incorrect length
ConfigureUnidentifiedAccessRequest.newBuilder()
.setAllowUnrestrictedUnidentifiedAccess(false)
.setUnidentifiedAccessKey(ByteString.copyFrom(new byte[15]))
.build()
);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void setDiscoverableByPhoneNumber(final boolean discoverableByPhoneNumber) {
final Account account = mock(Account.class);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
assertDoesNotThrow(() ->
authenticatedServiceStub().setDiscoverableByPhoneNumber(SetDiscoverableByPhoneNumberRequest.newBuilder()
.setDiscoverableByPhoneNumber(discoverableByPhoneNumber)
.build()));
verify(account).setDiscoverableByPhoneNumber(discoverableByPhoneNumber);
}
@Test
void setRegistrationRecoveryPassword() {
final String phoneNumber =
PhoneNumberUtil.getInstance().format(PhoneNumberUtil.getInstance().getExampleNumber("US"),
PhoneNumberUtil.PhoneNumberFormat.E164);
final Account account = mock(Account.class);
when(account.getNumber()).thenReturn(phoneNumber);
when(accountsManager.getByAccountIdentifierAsync(AUTHENTICATED_ACI))
.thenReturn(CompletableFuture.completedFuture(Optional.of(account)));
final byte[] registrationRecoveryPassword = new byte[32];
ThreadLocalRandom.current().nextBytes(registrationRecoveryPassword);
assertDoesNotThrow(() ->
authenticatedServiceStub().setRegistrationRecoveryPassword(SetRegistrationRecoveryPasswordRequest.newBuilder()
.setRegistrationRecoveryPassword(ByteString.copyFrom(registrationRecoveryPassword))
.build()));
verify(registrationRecoveryPasswordsManager).storeForCurrentNumber(phoneNumber, registrationRecoveryPassword);
}
@Test
void setRegistrationRecoveryPasswordMissingPassword() {
//noinspection ResultOfMethodCallIgnored
GrpcTestUtils.assertStatusException(Status.INVALID_ARGUMENT,
() -> authenticatedServiceStub().setRegistrationRecoveryPassword(
SetRegistrationRecoveryPasswordRequest.newBuilder().build()));
}
}

View File

@@ -148,4 +148,8 @@ public abstract class SimpleBaseGrpcTest<SERVICE extends BindableService, STUB e
protected MockRemoteAddressInterceptor getMockRemoteAddressInterceptor() {
return mockRemoteAddressInterceptor;
}
protected MockAuthenticationInterceptor getMockAuthenticationInterceptor() {
return mockAuthenticationInterceptor;
}
}