diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityType.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java similarity index 65% rename from service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityType.java rename to service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java index d558348bf..6b500fcf3 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityType.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/IdentityTypeUtil.java @@ -6,15 +6,17 @@ package org.whispersystems.textsecuregcm.grpc; import io.grpc.Status; +import org.whispersystems.textsecuregcm.identity.IdentityType; -public enum IdentityType { - ACI, - PNI; +public class IdentityTypeUtil { + + private IdentityTypeUtil() { + } public static IdentityType fromGrpcIdentityType(final org.signal.chat.common.IdentityType grpcIdentityType) { return switch (grpcIdentityType) { - case IDENTITY_TYPE_ACI -> ACI; - case IDENTITY_TYPE_PNI -> PNI; + case IDENTITY_TYPE_ACI -> IdentityType.ACI; + case IDENTITY_TYPE_PNI -> IdentityType.PNI; case IDENTITY_TYPE_UNSPECIFIED, UNRECOGNIZED -> throw Status.INVALID_ARGUMENT.asRuntimeException(); }; } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java index bc6410ea2..3e8b90414 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java @@ -10,6 +10,7 @@ import org.signal.chat.keys.GetPreKeysAnonymousRequest; import org.signal.chat.keys.GetPreKeysResponse; import org.signal.chat.keys.ReactorKeysAnonymousGrpc; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.KeysManager; import reactor.core.publisher.Mono; @@ -26,15 +27,15 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony @Override public Mono getPreKeys(final GetPreKeysAnonymousRequest request) { - return KeysGrpcHelper.findAccount(request.getTargetIdentifier(), accountsManager) - .switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException())) - .flatMap(targetAccount -> { - final IdentityType identityType = - IdentityType.fromGrpcIdentityType(request.getTargetIdentifier().getIdentityType()); + final ServiceIdentifier serviceIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier()); - return UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray()) - ? KeysGrpcHelper.getPreKeys(targetAccount, identityType, request.getDeviceId(), keysManager) - : Mono.error(Status.UNAUTHENTICATED.asException()); - }); + return Mono.fromFuture(accountsManager.getByServiceIdentifierAsync(serviceIdentifier)) + .flatMap(Mono::justOrEmpty) + .switchIfEmpty(Mono.error(Status.UNAUTHENTICATED.asException())) + .flatMap(targetAccount -> + UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray()) + ? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier.identityType(), request.getDeviceId(), keysManager) + : Mono.error(Status.UNAUTHENTICATED.asException())); } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java index 948e1aa73..aed7a143a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java @@ -8,19 +8,17 @@ package org.whispersystems.textsecuregcm.grpc; import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.ByteString; import io.grpc.Status; -import java.util.UUID; import org.signal.chat.common.EcPreKey; import org.signal.chat.common.EcSignedPreKey; import org.signal.chat.common.KemSignedPreKey; -import org.signal.chat.common.ServiceIdentifier; import org.signal.chat.keys.GetPreKeysResponse; -import org.signal.libsignal.protocol.IdentityKey; +import org.whispersystems.textsecuregcm.entities.ECPreKey; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.storage.Account; -import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.storage.KeysManager; -import org.whispersystems.textsecuregcm.util.UUIDUtil; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -31,37 +29,10 @@ class KeysGrpcHelper { @VisibleForTesting static final long ALL_DEVICES = 0; - static Mono findAccount(final ServiceIdentifier targetIdentifier, final AccountsManager accountsManager) { - - return Mono.just(IdentityType.fromGrpcIdentityType(targetIdentifier.getIdentityType())) - .flatMap(identityType -> { - final UUID uuid = UUIDUtil.fromByteString(targetIdentifier.getUuid()); - - return Mono.fromFuture(switch (identityType) { - case ACI -> accountsManager.getByAccountIdentifierAsync(uuid); - case PNI -> accountsManager.getByPhoneNumberIdentifierAsync(uuid); - }); - }) - .flatMap(Mono::justOrEmpty) - .onErrorMap(IllegalArgumentException.class, throwable -> Status.INVALID_ARGUMENT.asException()); - } - - static Tuple2 getIdentifierAndIdentityKey(final Account account, final IdentityType identityType) { - final UUID identifier = switch (identityType) { - case ACI -> account.getUuid(); - case PNI -> account.getPhoneNumberIdentifier(); - }; - - final IdentityKey identityKey = switch (identityType) { - case ACI -> account.getIdentityKey(); - case PNI -> account.getPhoneNumberIdentityKey(); - }; - - return Tuples.of(identifier, identityKey); - } - - static Mono getPreKeys(final Account targetAccount, final IdentityType identityType, final long targetDeviceId, final KeysManager keysManager) { - final Tuple2 identifierAndIdentityKey = getIdentifierAndIdentityKey(targetAccount, identityType); + static Mono getPreKeys(final Account targetAccount, + final IdentityType identityType, + final long targetDeviceId, + final KeysManager keysManager) { final Flux devices = targetDeviceId == ALL_DEVICES ? Flux.fromIterable(targetAccount.getDevices()) @@ -70,37 +41,43 @@ class KeysGrpcHelper { return devices .filter(Device::isEnabled) .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())) - .flatMap(device -> Mono.zip(Mono.fromFuture(keysManager.takeEC(identifierAndIdentityKey.getT1(), device.getId())), - Mono.fromFuture(keysManager.takePQ(identifierAndIdentityKey.getT1(), device.getId()))) - .map(oneTimePreKeys -> { - final ECSignedPreKey ecSignedPreKey = switch (identityType) { - case ACI -> device.getSignedPreKey(); - case PNI -> device.getPhoneNumberIdentitySignedPreKey(); - }; + .flatMap(device -> { + final ECSignedPreKey ecSignedPreKey = device.getSignedPreKey(identityType); - final GetPreKeysResponse.PreKeyBundle.Builder preKeyBundleBuilder = GetPreKeysResponse.PreKeyBundle.newBuilder() - .setEcSignedPreKey(EcSignedPreKey.newBuilder() - .setKeyId(ecSignedPreKey.keyId()) - .setPublicKey(ByteString.copyFrom(ecSignedPreKey.serializedPublicKey())) - .setSignature(ByteString.copyFrom(ecSignedPreKey.signature())) + final GetPreKeysResponse.PreKeyBundle.Builder preKeyBundleBuilder = GetPreKeysResponse.PreKeyBundle.newBuilder() + .setEcSignedPreKey(EcSignedPreKey.newBuilder() + .setKeyId(ecSignedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecSignedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(ecSignedPreKey.signature())) + .build()); + + return Flux.merge( + Mono.fromFuture(keysManager.takeEC(targetAccount.getIdentifier(identityType), device.getId())), + Mono.fromFuture(keysManager.takePQ(targetAccount.getIdentifier(identityType), device.getId()))) + .flatMap(Mono::justOrEmpty) + .reduce(preKeyBundleBuilder, (builder, preKey) -> { + if (preKey instanceof ECPreKey ecPreKey) { + builder.setEcOneTimePreKey(EcPreKey.newBuilder() + .setKeyId(ecPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey())) .build()); + } else if (preKey instanceof KEMSignedPreKey kemSignedPreKey) { + preKeyBundleBuilder.setKemOneTimePreKey(KemSignedPreKey.newBuilder() + .setKeyId(kemSignedPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(kemSignedPreKey.serializedPublicKey())) + .setSignature(ByteString.copyFrom(kemSignedPreKey.signature())) + .build()); + } else { + throw new AssertionError("Unexpected pre-key type: " + preKey.getClass()); + } - oneTimePreKeys.getT1().ifPresent(ecPreKey -> preKeyBundleBuilder.setEcOneTimePreKey(EcPreKey.newBuilder() - .setKeyId(ecPreKey.keyId()) - .setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey())) - .build())); - - oneTimePreKeys.getT2().ifPresent(kemSignedPreKey -> preKeyBundleBuilder.setKemOneTimePreKey(KemSignedPreKey.newBuilder() - .setKeyId(kemSignedPreKey.keyId()) - .setPublicKey(ByteString.copyFrom(kemSignedPreKey.serializedPublicKey())) - .setSignature(ByteString.copyFrom(kemSignedPreKey.signature())) - .build())); - - return Tuples.of(device.getId(), preKeyBundleBuilder.build()); - })) + return builder; + }) + .map(builder -> Tuples.of(device.getId(), builder.build())); + }) .collectMap(Tuple2::getT1, Tuple2::getT2) .map(preKeyBundles -> GetPreKeysResponse.newBuilder() - .setIdentityKey(ByteString.copyFrom(identifierAndIdentityKey.getT2().serialize())) + .setIdentityKey(ByteString.copyFrom(targetAccount.getIdentityKey(identityType).serialize())) .putAllPreKeys(preKeyBundles) .build()); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java index f09abb2b8..89d885a76 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java @@ -5,8 +5,6 @@ package org.whispersystems.textsecuregcm.grpc; -import static org.whispersystems.textsecuregcm.grpc.IdentityType.ACI; - import io.grpc.Status; import io.grpc.StatusRuntimeException; import java.util.List; @@ -37,15 +35,15 @@ import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil; import org.whispersystems.textsecuregcm.entities.ECPreKey; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; 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.KeysManager; -import org.whispersystems.textsecuregcm.util.UUIDUtil; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; import reactor.util.function.Tuples; public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { @@ -89,7 +87,7 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { .map(account -> Tuples.of(account, authenticatedDevice.deviceId())) .orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))) .flatMapMany(accountAndDeviceId -> Flux.just( - Tuples.of(ACI, accountAndDeviceId.getT1().getUuid(), accountAndDeviceId.getT2()), + Tuples.of(IdentityType.ACI, accountAndDeviceId.getT1().getUuid(), accountAndDeviceId.getT2()), Tuples.of(IdentityType.PNI, accountAndDeviceId.getT1().getPhoneNumberIdentifier(), accountAndDeviceId.getT2()) )) .flatMap(identityTypeUuidAndDeviceId -> Flux.merge( @@ -128,31 +126,20 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { public Mono getPreKeys(final GetPreKeysRequest request) { final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice(); - final String rateLimitKey; - { - final UUID targetUuid; + final ServiceIdentifier targetIdentifier = + ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getTargetIdentifier()); - try { - targetUuid = UUIDUtil.fromByteString(request.getTargetIdentifier().getUuid()); - } catch (final IllegalArgumentException e) { - throw Status.INVALID_ARGUMENT.asRuntimeException(); - } - - rateLimitKey = authenticatedDevice.accountIdentifier() + "." + - authenticatedDevice.deviceId() + "__" + - targetUuid + "." + - request.getDeviceId(); - } + final String rateLimitKey = authenticatedDevice.accountIdentifier() + "." + + authenticatedDevice.deviceId() + "__" + + targetIdentifier.uuid() + "." + + request.getDeviceId(); return rateLimiters.getPreKeysLimiter().validateReactive(rateLimitKey) - .then(KeysGrpcHelper.findAccount(request.getTargetIdentifier(), accountsManager)) + .then(Mono.fromFuture(accountsManager.getByServiceIdentifierAsync(targetIdentifier)) + .flatMap(Mono::justOrEmpty)) .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())) - .flatMap(targetAccount -> { - final IdentityType identityType = - IdentityType.fromGrpcIdentityType(request.getTargetIdentifier().getIdentityType()); - - return KeysGrpcHelper.getPreKeys(targetAccount, identityType, request.getDeviceId(), keysManager); - }); + .flatMap(targetAccount -> + KeysGrpcHelper.getPreKeys(targetAccount, targetIdentifier.identityType(), request.getDeviceId(), keysManager)); } @Override @@ -160,7 +147,7 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) .flatMap(authenticatedDevice -> storeOneTimePreKeys(authenticatedDevice.accountIdentifier(), request.getPreKeysList(), - IdentityType.fromGrpcIdentityType(request.getIdentityType()), + IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType()), (requestPreKey, ignored) -> checkEcPreKey(requestPreKey), (identifier, preKeys) -> keysManager.storeEcOneTimePreKeys(identifier, authenticatedDevice.deviceId(), preKeys))); } @@ -170,7 +157,7 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) .flatMap(authenticatedDevice -> storeOneTimePreKeys(authenticatedDevice.accountIdentifier(), request.getPreKeysList(), - IdentityType.fromGrpcIdentityType(request.getIdentityType()), + IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType()), KeysGrpcService::checkKemSignedPreKey, (identifier, preKeys) -> keysManager.storeKemOneTimePreKeys(identifier, authenticatedDevice.deviceId(), preKeys))); } @@ -184,18 +171,15 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { return Mono.fromFuture(accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid)) .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) .map(account -> { - final Tuple2 identifierAndIdentityKey = - KeysGrpcHelper.getIdentifierAndIdentityKey(account, identityType); - final List preKeys = requestPreKeys.stream() - .map(requestPreKey -> extractPreKeyFunction.apply(requestPreKey, identifierAndIdentityKey.getT2())) + .map(requestPreKey -> extractPreKeyFunction.apply(requestPreKey, account.getIdentityKey(identityType))) .toList(); if (preKeys.isEmpty()) { throw Status.INVALID_ARGUMENT.asRuntimeException(); } - return Tuples.of(identifierAndIdentityKey.getT1(), preKeys); + return Tuples.of(account.getIdentifier(identityType), preKeys); }) .flatMap(identifierAndPreKeys -> Mono.fromFuture(storeKeysFunction.apply(identifierAndPreKeys.getT1(), identifierAndPreKeys.getT2()))) .thenReturn(SetPreKeyResponse.newBuilder().build()); @@ -209,15 +193,14 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { request.getSignedPreKey(), KeysGrpcService::checkEcSignedPreKey, (account, signedPreKey) -> { - final Consumer deviceUpdater = switch (IdentityType.fromGrpcIdentityType(request.getIdentityType())) { + final IdentityType identityType = IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType()); + + final Consumer deviceUpdater = switch (identityType) { case ACI -> device -> device.setSignedPreKey(signedPreKey); case PNI -> device -> device.setPhoneNumberIdentitySignedPreKey(signedPreKey); }; - final UUID identifier = switch (IdentityType.fromGrpcIdentityType(request.getIdentityType())) { - case ACI -> account.getUuid(); - case PNI -> account.getPhoneNumberIdentifier(); - }; + final UUID identifier = account.getIdentifier(identityType); return Flux.merge( Mono.fromFuture(keysManager.storeEcSignedPreKeys(identifier, Map.of(authenticatedDevice.deviceId(), signedPreKey))), @@ -234,10 +217,8 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { request.getSignedPreKey(), KeysGrpcService::checkKemSignedPreKey, (account, lastResortKey) -> { - final UUID identifier = switch (IdentityType.fromGrpcIdentityType(request.getIdentityType())) { - case ACI -> account.getUuid(); - case PNI -> account.getPhoneNumberIdentifier(); - }; + final UUID identifier = + account.getIdentifier(IdentityTypeUtil.fromGrpcIdentityType(request.getIdentityType())); return Mono.fromFuture(keysManager.storePqLastResort(identifier, Map.of(authenticatedDevice.deviceId(), lastResortKey))); })); @@ -252,11 +233,7 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { return Mono.fromFuture(accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid)) .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) .map(account -> { - final IdentityKey identityKey = switch (IdentityType.fromGrpcIdentityType(identityType)) { - case ACI -> account.getIdentityKey(); - case PNI -> account.getPhoneNumberIdentityKey(); - }; - + final IdentityKey identityKey = account.getIdentityKey(IdentityTypeUtil.fromGrpcIdentityType(identityType)); final K key = extractKeyFunction.apply(storeKeyRequest, identityKey); return Tuples.of(account, key); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java new file mode 100644 index 000000000..4b30c6165 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.grpc; + +import io.grpc.Status; +import java.util.UUID; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; +import org.whispersystems.textsecuregcm.util.UUIDUtil; + +public class ServiceIdentifierUtil { + + private ServiceIdentifierUtil() { + } + + public static ServiceIdentifier fromGrpcServiceIdentifier(final org.signal.chat.common.ServiceIdentifier serviceIdentifier) { + final UUID uuid; + + try { + uuid = UUIDUtil.fromByteString(serviceIdentifier.getUuid()); + } catch (final IllegalArgumentException e) { + throw Status.INVALID_ARGUMENT.asRuntimeException(); + } + + return switch (IdentityTypeUtil.fromGrpcIdentityType(serviceIdentifier.getIdentityType())) { + case ACI -> new AciServiceIdentifier(uuid); + case PNI -> new PniServiceIdentifier(uuid); + }; + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java index 22103d27e..fb62e3c50 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Account.java @@ -25,6 +25,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.auth.StoredRegistrationLock; import org.whispersystems.textsecuregcm.entities.AccountAttributes; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Device.DeviceCapabilities; import org.whispersystems.textsecuregcm.util.ByteArrayBase64UrlAdapter; @@ -105,6 +106,12 @@ public class Account { @JsonIgnore private boolean stale; + public UUID getIdentifier(final IdentityType identityType) { + return switch (identityType) { + case ACI -> getUuid(); + case PNI -> getPhoneNumberIdentifier(); + }; + } public UUID getUuid() { // this is the one method that may be called on a stale account @@ -325,12 +332,29 @@ public class Account { this.identityKey = identityKey; } + public IdentityKey getIdentityKey(final IdentityType identityType) { + requireNotStale(); + + return switch (identityType) { + case ACI -> identityKey; + case PNI -> phoneNumberIdentityKey; + }; + } + + /** + * @deprecated Please use {@link #getIdentityKey(IdentityType)} instead. + */ + @Deprecated public IdentityKey getIdentityKey() { requireNotStale(); return identityKey; } + /** + * @deprecated Please use {@link #getIdentityKey(IdentityType)} instead. + */ + @Deprecated public IdentityKey getPhoneNumberIdentityKey() { return phoneNumberIdentityKey; } 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 742718d65..6284dd3f9 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/AccountsManager.java @@ -811,6 +811,13 @@ public class AccountsManager { }; } + public CompletableFuture> getByServiceIdentifierAsync(final ServiceIdentifier serviceIdentifier) { + return switch (serviceIdentifier.identityType()) { + case ACI -> getByAccountIdentifierAsync(serviceIdentifier.uuid()); + case PNI -> getByPhoneNumberIdentifierAsync(serviceIdentifier.uuid()); + }; + } + public Optional getByAccountIdentifier(final UUID uuid) { return checkRedisThenAccounts( getByUuidTimer, diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java index 56cdcfe3a..a14f5760e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/Device.java @@ -14,6 +14,7 @@ import java.util.stream.LongStream; import javax.annotation.Nullable; import org.whispersystems.textsecuregcm.auth.SaltedTokenHash; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.util.Util; public class Device { @@ -230,6 +231,17 @@ public class Device { this.phoneNumberIdentityRegistrationId = phoneNumberIdentityRegistrationId; } + public ECSignedPreKey getSignedPreKey(final IdentityType identityType) { + return switch (identityType) { + case ACI -> signedPreKey; + case PNI -> phoneNumberIdentitySignedPreKey; + }; + } + + /** + * @deprecated Please use {@link #getSignedPreKey(IdentityType)} instead. + */ + @Deprecated public ECSignedPreKey getSignedPreKey() { return signedPreKey; } @@ -238,6 +250,10 @@ public class Device { this.signedPreKey = signedPreKey; } + /** + * @deprecated Please use {@link #getSignedPreKey(IdentityType)} instead. + */ + @Deprecated public ECSignedPreKey getPhoneNumberIdentitySignedPreKey() { return phoneNumberIdentitySignedPreKey; } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java index 81cf7e68e..dc0640616 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java @@ -27,7 +27,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.signal.chat.common.EcPreKey; import org.signal.chat.common.EcSignedPreKey; -import org.signal.chat.common.IdentityType; import org.signal.chat.common.KemSignedPreKey; import org.signal.chat.common.ServiceIdentifier; import org.signal.chat.keys.GetPreKeysAnonymousRequest; @@ -39,6 +38,8 @@ import org.signal.libsignal.protocol.ecc.ECKeyPair; import org.whispersystems.textsecuregcm.entities.ECPreKey; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; @@ -86,9 +87,9 @@ class KeysAnonymousGrpcServiceTest { when(targetAccount.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(targetDevice)); when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); - when(targetAccount.getUuid()).thenReturn(identifier); - when(targetAccount.getIdentityKey()).thenReturn(identityKey); - when(accountsManager.getByAccountIdentifierAsync(identifier)) + when(targetAccount.getIdentifier(IdentityType.ACI)).thenReturn(identifier); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier))) .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); final ECPreKey ecPreKey = new ECPreKey(1, Curve.generateKeyPair().getPublicKey()); @@ -97,11 +98,11 @@ class KeysAnonymousGrpcServiceTest { when(keysManager.takeEC(identifier, Device.MASTER_ID)).thenReturn(CompletableFuture.completedFuture(Optional.of(ecPreKey))); when(keysManager.takePQ(identifier, Device.MASTER_ID)).thenReturn(CompletableFuture.completedFuture(Optional.of(kemSignedPreKey))); - when(targetDevice.getSignedPreKey()).thenReturn(ecSignedPreKey); + when(targetDevice.getSignedPreKey(IdentityType.ACI)).thenReturn(ecSignedPreKey); final GetPreKeysResponse response = keysAnonymousStub.getPreKeys(GetPreKeysAnonymousRequest.newBuilder() .setTargetIdentifier(ServiceIdentifier.newBuilder() - .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) .setUuid(UUIDUtil.toByteString(identifier)) .build()) .setDeviceId(Device.MASTER_ID) @@ -144,15 +145,15 @@ class KeysAnonymousGrpcServiceTest { when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); when(targetAccount.getUuid()).thenReturn(identifier); - when(targetAccount.getIdentityKey()).thenReturn(identityKey); - when(accountsManager.getByAccountIdentifierAsync(identifier)) + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(identifier))) .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); @SuppressWarnings("ResultOfMethodCallIgnored") final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class, () -> keysAnonymousStub.getPreKeys(GetPreKeysAnonymousRequest.newBuilder() .setTargetIdentifier(ServiceIdentifier.newBuilder() - .setIdentityType(IdentityType.IDENTITY_TYPE_ACI) + .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) .setUuid(UUIDUtil.toByteString(identifier)) .build()) .setDeviceId(Device.MASTER_ID) @@ -163,7 +164,7 @@ class KeysAnonymousGrpcServiceTest { @Test void getPreKeysAccountNotFound() { - when(accountsManager.getByAccountIdentifierAsync(any())) + when(accountsManager.getByServiceIdentifierAsync(any())) .thenReturn(CompletableFuture.completedFuture(Optional.empty())); @SuppressWarnings("ResultOfMethodCallIgnored") final StatusRuntimeException exception = @@ -188,12 +189,12 @@ class KeysAnonymousGrpcServiceTest { final Account targetAccount = mock(Account.class); when(targetAccount.getUuid()).thenReturn(accountIdentifier); - when(targetAccount.getIdentityKey()).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); when(targetAccount.getDevices()).thenReturn(Collections.emptyList()); when(targetAccount.getDevice(anyLong())).thenReturn(Optional.empty()); when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey)); - when(accountsManager.getByAccountIdentifierAsync(accountIdentifier)) + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier))) .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); @SuppressWarnings("ResultOfMethodCallIgnored") final StatusRuntimeException exception = diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java index d2431a281..93b7ce60d 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcServiceTest.java @@ -11,6 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -59,6 +60,8 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; import org.whispersystems.textsecuregcm.entities.ECPreKey; import org.whispersystems.textsecuregcm.entities.ECSignedPreKey; import org.whispersystems.textsecuregcm.entities.KEMSignedPreKey; +import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier; +import org.whispersystems.textsecuregcm.identity.IdentityType; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; import org.whispersystems.textsecuregcm.storage.Account; @@ -109,8 +112,10 @@ class KeysGrpcServiceTest { final Account authenticatedAccount = mock(Account.class); when(authenticatedAccount.getUuid()).thenReturn(AUTHENTICATED_ACI); when(authenticatedAccount.getPhoneNumberIdentifier()).thenReturn(AUTHENTICATED_PNI); - when(authenticatedAccount.getIdentityKey()).thenReturn(new IdentityKey(ACI_IDENTITY_KEY_PAIR.getPublicKey())); - when(authenticatedAccount.getPhoneNumberIdentityKey()).thenReturn(new IdentityKey(PNI_IDENTITY_KEY_PAIR.getPublicKey())); + when(authenticatedAccount.getIdentifier(IdentityType.ACI)).thenReturn(AUTHENTICATED_ACI); + when(authenticatedAccount.getIdentifier(IdentityType.PNI)).thenReturn(AUTHENTICATED_PNI); + when(authenticatedAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(ACI_IDENTITY_KEY_PAIR.getPublicKey())); + when(authenticatedAccount.getIdentityKey(IdentityType.PNI)).thenReturn(new IdentityKey(PNI_IDENTITY_KEY_PAIR.getPublicKey())); when(authenticatedAccount.getDevice(AUTHENTICATED_DEVICE_ID)).thenReturn(Optional.of(authenticatedDevice)); final MockAuthenticationInterceptor mockAuthenticationInterceptor = new MockAuthenticationInterceptor(); @@ -172,7 +177,7 @@ class KeysGrpcServiceTest { .toList()) .build()); - final UUID expectedIdentifier = switch (IdentityType.fromGrpcIdentityType(identityType)) { + final UUID expectedIdentifier = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { case ACI -> AUTHENTICATED_ACI; case PNI -> AUTHENTICATED_PNI; }; @@ -218,7 +223,7 @@ class KeysGrpcServiceTest { @ParameterizedTest @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) void setOneTimeKemSignedPreKeys(final org.signal.chat.common.IdentityType identityType) { - final ECKeyPair identityKeyPair = switch (IdentityType.fromGrpcIdentityType(identityType)) { + final ECKeyPair identityKeyPair = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { case ACI -> ACI_IDENTITY_KEY_PAIR; case PNI -> PNI_IDENTITY_KEY_PAIR; }; @@ -245,7 +250,7 @@ class KeysGrpcServiceTest { .toList()) .build()); - final UUID expectedIdentifier = switch (IdentityType.fromGrpcIdentityType(identityType)) { + final UUID expectedIdentifier = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { case ACI -> AUTHENTICATED_ACI; case PNI -> AUTHENTICATED_PNI; }; @@ -317,7 +322,7 @@ class KeysGrpcServiceTest { when(keysManager.storeEcSignedPreKeys(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - final ECKeyPair identityKeyPair = switch (IdentityType.fromGrpcIdentityType(identityType)) { + final ECKeyPair identityKeyPair = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { case ACI -> ACI_IDENTITY_KEY_PAIR; case PNI -> PNI_IDENTITY_KEY_PAIR; }; @@ -401,7 +406,7 @@ class KeysGrpcServiceTest { void setLastResortPreKey(final org.signal.chat.common.IdentityType identityType) { when(keysManager.storePqLastResort(any(), any())).thenReturn(CompletableFuture.completedFuture(null)); - final ECKeyPair identityKeyPair = switch (IdentityType.fromGrpcIdentityType(identityType)) { + final ECKeyPair identityKeyPair = switch (IdentityTypeUtil.fromGrpcIdentityType(identityType)) { case ACI -> ACI_IDENTITY_KEY_PAIR; case PNI -> PNI_IDENTITY_KEY_PAIR; }; @@ -478,25 +483,20 @@ class KeysGrpcServiceTest { @ParameterizedTest @EnumSource(value = org.signal.chat.common.IdentityType.class, names = {"IDENTITY_TYPE_ACI", "IDENTITY_TYPE_PNI"}) - void getPreKeys(final org.signal.chat.common.IdentityType identityType) { + void getPreKeys(final org.signal.chat.common.IdentityType grpcIdentityType) { final Account targetAccount = mock(Account.class); final ECKeyPair identityKeyPair = Curve.generateKeyPair(); final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey()); final UUID identifier = UUID.randomUUID(); - if (identityType == org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) { - when(targetAccount.getUuid()).thenReturn(identifier); - when(targetAccount.getIdentityKey()).thenReturn(identityKey); - when(accountsManager.getByAccountIdentifierAsync(identifier)) - .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); - } else { - when(targetAccount.getUuid()).thenReturn(UUID.randomUUID()); - when(targetAccount.getPhoneNumberIdentifier()).thenReturn(identifier); - when(targetAccount.getPhoneNumberIdentityKey()).thenReturn(identityKey); - when(accountsManager.getByPhoneNumberIdentifierAsync(identifier)) - .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); - } + final IdentityType identityType = IdentityTypeUtil.fromGrpcIdentityType(grpcIdentityType); + + when(targetAccount.getUuid()).thenReturn(UUID.randomUUID()); + when(targetAccount.getIdentifier(identityType)).thenReturn(identifier); + when(targetAccount.getIdentityKey(identityType)).thenReturn(identityKey); + when(accountsManager.getByServiceIdentifierAsync(argThat(serviceIdentifier -> serviceIdentifier.uuid().equals(identifier)))) + .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); final Map ecOneTimePreKeys = new HashMap<>(); final Map kemPreKeys = new HashMap<>(); @@ -512,12 +512,7 @@ class KeysGrpcServiceTest { final Device device = mock(Device.class); when(device.getId()).thenReturn(deviceId); when(device.isEnabled()).thenReturn(true); - - if (identityType == org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) { - when(device.getSignedPreKey()).thenReturn(ecSignedPreKeys.get(deviceId)); - } else { - when(device.getPhoneNumberIdentitySignedPreKey()).thenReturn(ecSignedPreKeys.get(deviceId)); - } + when(device.getSignedPreKey(identityType)).thenReturn(ecSignedPreKeys.get(deviceId)); devices.put(deviceId, device); when(targetAccount.getDevice(deviceId)).thenReturn(Optional.of(device)); @@ -534,7 +529,7 @@ class KeysGrpcServiceTest { { final GetPreKeysResponse response = keysStub.getPreKeys(GetPreKeysRequest.newBuilder() .setTargetIdentifier(ServiceIdentifier.newBuilder() - .setIdentityType(identityType) + .setIdentityType(grpcIdentityType) .setUuid(UUIDUtil.toByteString(identifier)) .build()) .setDeviceId(1) @@ -569,7 +564,7 @@ class KeysGrpcServiceTest { { final GetPreKeysResponse response = keysStub.getPreKeys(GetPreKeysRequest.newBuilder() .setTargetIdentifier(ServiceIdentifier.newBuilder() - .setIdentityType(identityType) + .setIdentityType(grpcIdentityType) .setUuid(UUIDUtil.toByteString(identifier)) .build()) .build()); @@ -607,7 +602,7 @@ class KeysGrpcServiceTest { @Test void getPreKeysAccountNotFound() { - when(accountsManager.getByAccountIdentifierAsync(any())) + when(accountsManager.getByServiceIdentifierAsync(any())) .thenReturn(CompletableFuture.completedFuture(Optional.empty())); @SuppressWarnings("ResultOfMethodCallIgnored") final StatusRuntimeException exception = @@ -628,11 +623,11 @@ class KeysGrpcServiceTest { final Account targetAccount = mock(Account.class); when(targetAccount.getUuid()).thenReturn(accountIdentifier); - when(targetAccount.getIdentityKey()).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); when(targetAccount.getDevices()).thenReturn(Collections.emptyList()); when(targetAccount.getDevice(anyLong())).thenReturn(Optional.empty()); - when(accountsManager.getByAccountIdentifierAsync(accountIdentifier)) + when(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(accountIdentifier))) .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); @SuppressWarnings("ResultOfMethodCallIgnored") final StatusRuntimeException exception = @@ -651,11 +646,11 @@ class KeysGrpcServiceTest { void getPreKeysRateLimited() { final Account targetAccount = mock(Account.class); when(targetAccount.getUuid()).thenReturn(UUID.randomUUID()); - when(targetAccount.getIdentityKey()).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); + when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(new IdentityKey(Curve.generateKeyPair().getPublicKey())); when(targetAccount.getDevices()).thenReturn(Collections.emptyList()); when(targetAccount.getDevice(anyLong())).thenReturn(Optional.empty()); - when(accountsManager.getByAccountIdentifierAsync(any())) + when(accountsManager.getByServiceIdentifierAsync(any())) .thenReturn(CompletableFuture.completedFuture(Optional.of(targetAccount))); final Duration retryAfterDuration = Duration.ofMinutes(7); 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 19cec8183..643c4fa52 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/AccountsManagerTest.java @@ -218,7 +218,7 @@ class AccountsManagerTest { when(commands.get(eq("AccountMap::" + pni))).thenReturn(aci.toString()); when(commands.get(eq("Account3::" + aci))).thenReturn( - "{\"number\": \"+14152222222\", \"pni\": \"de24dc73-fbd8-41be-a7d5-764c70d9da7e\"}"); + "{\"number\": \"+14152222222\", \"pni\": \"" + pni + "\"}"); assertTrue(accountsManager.getByServiceIdentifier(new AciServiceIdentifier(aci)).isPresent()); assertTrue(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(pni)).isPresent()); @@ -226,6 +226,29 @@ class AccountsManagerTest { assertFalse(accountsManager.getByServiceIdentifier(new PniServiceIdentifier(aci)).isPresent()); } + @Test + void testGetByServiceIdentifierAsync() { + final UUID aci = UUID.randomUUID(); + final UUID pni = UUID.randomUUID(); + + when(asyncCommands.get(eq("AccountMap::" + pni))).thenReturn(MockRedisFuture.completedFuture(aci.toString())); + when(asyncCommands.get(eq("Account3::" + aci))).thenReturn(MockRedisFuture.completedFuture( + "{\"number\": \"+14152222222\", \"pni\": \"" + pni + "\"}")); + + when(asyncCommands.setex(any(), anyLong(), any())).thenReturn(MockRedisFuture.completedFuture("OK")); + + when(accounts.getByAccountIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + when(accounts.getByPhoneNumberIdentifierAsync(any())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + assertTrue(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(aci)).join().isPresent()); + assertTrue(accountsManager.getByServiceIdentifierAsync(new PniServiceIdentifier(pni)).join().isPresent()); + assertFalse(accountsManager.getByServiceIdentifierAsync(new AciServiceIdentifier(pni)).join().isPresent()); + assertFalse(accountsManager.getByServiceIdentifierAsync(new PniServiceIdentifier(aci)).join().isPresent()); + } + @Test void testGetAccountByNumberInCache() { UUID uuid = UUID.randomUUID(); @@ -315,7 +338,7 @@ class AccountsManagerTest { } @Test - void testGetByPniInCache() { + void testGetAccountByPniInCache() { UUID uuid = UUID.randomUUID(); UUID pni = UUID.randomUUID(); @@ -337,7 +360,7 @@ class AccountsManagerTest { } @Test - void testGetByPniInCacheAsync() { + void testGetAccountByPniInCacheAsync() { UUID uuid = UUID.randomUUID(); UUID pni = UUID.randomUUID(); @@ -363,7 +386,7 @@ class AccountsManagerTest { } @Test - void testGetByUsernameHashInCache() { + void testGetAccountByUsernameHashInCache() { UUID uuid = UUID.randomUUID(); when(commands.get(eq("UAccountMap::" + BASE_64_URL_USERNAME_HASH_1))).thenReturn(uuid.toString()); when(commands.get(eq("Account3::" + uuid))).thenReturn(