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 247e11761..924671a9e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcService.java @@ -6,19 +6,24 @@ package org.whispersystems.textsecuregcm.grpc; import com.google.protobuf.ByteString; -import io.grpc.Status; -import io.grpc.StatusException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Clock; import java.util.Arrays; +import java.util.List; +import org.signal.chat.errors.FailedUnidentifiedAuthorization; +import org.signal.chat.errors.NotFound; import org.signal.chat.keys.CheckIdentityKeyRequest; import org.signal.chat.keys.CheckIdentityKeyResponse; import org.signal.chat.keys.GetPreKeysAnonymousRequest; -import org.signal.chat.keys.GetPreKeysResponse; +import org.signal.chat.keys.GetPreKeysAnonymousResponse; import org.signal.chat.keys.ReactorKeysAnonymousGrpc; import org.signal.libsignal.protocol.IdentityKey; +import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.ServerSecretParams; +import org.signal.libsignal.zkgroup.VerificationFailedException; +import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair; +import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken; import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Account; @@ -32,17 +37,19 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony private final AccountsManager accountsManager; private final KeysManager keysManager; - private final GroupSendTokenUtil groupSendTokenUtil; + private final ServerSecretParams serverSecretParams; + private final Clock clock; public KeysAnonymousGrpcService( final AccountsManager accountsManager, final KeysManager keysManager, final ServerSecretParams serverSecretParams, final Clock clock) { this.accountsManager = accountsManager; this.keysManager = keysManager; - this.groupSendTokenUtil = new GroupSendTokenUtil(serverSecretParams, clock); -} + this.serverSecretParams = serverSecretParams; + this.clock = clock; + } @Override - public Mono getPreKeys(final GetPreKeysAnonymousRequest request) { + public Mono getPreKeys(final GetPreKeysAnonymousRequest request) { final ServiceIdentifier serviceIdentifier = ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getRequest().getTargetIdentifier()); @@ -53,23 +60,35 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony return switch (request.getAuthorizationCase()) { case GROUP_SEND_TOKEN -> { try { - groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), serviceIdentifier); + final GroupSendFullToken token = new GroupSendFullToken(request.getGroupSendToken().toByteArray()); + token.verify(List.of(serviceIdentifier.toLibsignal()), clock.instant(), + GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams)); - yield lookUpAccount(serviceIdentifier, Status.NOT_FOUND) - .flatMap(targetAccount -> KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager)); - } catch (final StatusException e) { - yield Mono.error(e); + yield lookUpAccount(serviceIdentifier) + .flatMap(targetAccount -> KeysGrpcHelper + .getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager)) + .map(preKeys -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(preKeys).build()) + .switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder() + .setTargetNotFound(NotFound.getDefaultInstance()) + .build())); + } catch (InvalidInputException e) { + throw GrpcExceptions.fieldViolation("group_send_token", "malformed group send token"); + } catch (VerificationFailedException e) { + yield Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build()); } } + case UNIDENTIFIED_ACCESS_KEY -> lookUpAccount(serviceIdentifier) + .filter(targetAccount -> + UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray())) + .flatMap(targetAccount -> KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager)) + .map(preKeys -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(preKeys).build()) + .switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder() + .setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance()) + .build())); - case UNIDENTIFIED_ACCESS_KEY -> - lookUpAccount(serviceIdentifier, Status.UNAUTHENTICATED) - .flatMap(targetAccount -> - UnidentifiedAccessUtil.checkUnidentifiedAccess(targetAccount, request.getUnidentifiedAccessKey().toByteArray()) - ? KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager) - : Mono.error(Status.UNAUTHENTICATED.asException())); - - default -> Mono.error(Status.INVALID_ARGUMENT.asException()); + default -> Mono.error(GrpcExceptions.fieldViolation("authorization", "invalid authorization type")); }; } @@ -92,10 +111,9 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony ); } - private Mono lookUpAccount(final ServiceIdentifier serviceIdentifier, final Status onNotFound) { + private Mono lookUpAccount(final ServiceIdentifier serviceIdentifier) { return Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(serviceIdentifier)) - .flatMap(Mono::justOrEmpty) - .switchIfEmpty(Mono.error(onNotFound.asException())); + .flatMap(Mono::justOrEmpty); } private static boolean fingerprintMatches(final IdentityKey identityKey, final byte[] fingerprint) { 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 71cf58ca7..99717cf43 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcHelper.java @@ -6,11 +6,12 @@ package org.whispersystems.textsecuregcm.grpc; import com.google.protobuf.ByteString; -import io.grpc.Status; import org.signal.chat.common.EcPreKey; import org.signal.chat.common.EcSignedPreKey; import org.signal.chat.common.KemSignedPreKey; -import org.signal.chat.keys.GetPreKeysResponse; +import org.signal.chat.keys.AccountPreKeyBundles; +import org.signal.chat.keys.DevicePreKeyBundle; +import org.signal.libsignal.protocol.IdentityKey; import org.whispersystems.textsecuregcm.identity.ServiceIdentifier; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; @@ -19,12 +20,22 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; +import java.util.Optional; class KeysGrpcHelper { static final byte ALL_DEVICES = 0; - static Mono getPreKeys(final Account targetAccount, + /** + * Fetch {@link AccountPreKeyBundles} from the targetAccount + * + * @param targetAccount The targetAccount to fetch pre-key bundles from + * @param targetServiceIdentifier The serviceIdentifier used to lookup the targetAccount + * @param targetDeviceId The deviceId to retrieve pre-key bundles for, or ALL_DEVICES if all devices should be retrieved + * @param keysManager The {@link KeysManager} to lookup pre-keys from + * @return The requested bundles, or an empty Mono if the keys for the targetAccount do not exist + */ + static Mono getPreKeys(final Account targetAccount, final ServiceIdentifier targetServiceIdentifier, final byte targetDeviceId, final KeysManager keysManager) { @@ -41,7 +52,7 @@ class KeysGrpcHelper { .fromFuture(keysManager.takeDevicePreKeys(device.getId(), targetServiceIdentifier, userAgent)) .flatMap(Mono::justOrEmpty) .map(devicePreKeys -> { - final GetPreKeysResponse.PreKeyBundle.Builder builder = GetPreKeysResponse.PreKeyBundle.newBuilder() + final DevicePreKeyBundle.Builder builder = DevicePreKeyBundle.newBuilder() .setEcSignedPreKey(EcSignedPreKey.newBuilder() .setKeyId(devicePreKeys.ecSignedPreKey().keyId()) .setPublicKey(ByteString.copyFrom(devicePreKeys.ecSignedPreKey().serializedPublicKey())) @@ -54,21 +65,25 @@ class KeysGrpcHelper { .build()) .setRegistrationId(registrationId); devicePreKeys.ecPreKey().ifPresent(ecPreKey -> builder.setEcOneTimePreKey(EcPreKey.newBuilder() - .setKeyId(ecPreKey.keyId()) - .setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey())) - .build())); + .setKeyId(ecPreKey.keyId()) + .setPublicKey(ByteString.copyFrom(ecPreKey.serializedPublicKey())) + .build())); // Cast device IDs to `int` to match data types in the response object’s protobuf definition return Tuples.of((int) device.getId(), builder.build()); }); }) - // If there were no devices with valid prekey bundles in the account, the account is gone - .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())) .collectMap(Tuple2::getT1, Tuple2::getT2) - .map(preKeyBundles -> GetPreKeysResponse.newBuilder() - .setIdentityKey(ByteString - .copyFrom(targetAccount.getIdentityKey(targetServiceIdentifier.identityType()) - .serialize())) - .putAllPreKeys(preKeyBundles) - .build()); + .flatMap(preKeyBundles -> { + if (preKeyBundles.isEmpty()) { + // If there were no devices with valid prekey bundles in the account, the account is gone + return Mono.empty(); + } + + final IdentityKey targetIdentityKey = targetAccount.getIdentityKey(targetServiceIdentifier.identityType()); + return Mono.just(AccountPreKeyBundles.newBuilder() + .setIdentityKey(ByteString.copyFrom(targetIdentityKey.serialize())) + .putAllDevicePreKeys(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 3799a5771..32530ab9c 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/KeysGrpcService.java @@ -5,16 +5,15 @@ package org.whispersystems.textsecuregcm.grpc; -import io.grpc.Status; import io.grpc.StatusRuntimeException; import java.util.List; -import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import org.signal.chat.common.EcPreKey; import org.signal.chat.common.EcSignedPreKey; import org.signal.chat.common.KemSignedPreKey; +import org.signal.chat.errors.NotFound; import org.signal.chat.keys.GetPreKeyCountRequest; import org.signal.chat.keys.GetPreKeyCountResponse; import org.signal.chat.keys.GetPreKeysRequest; @@ -50,13 +49,11 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { private final KeysManager keysManager; private final RateLimiters rateLimiters; - private static final StatusRuntimeException INVALID_PUBLIC_KEY_EXCEPTION = Status.fromCode(Status.Code.INVALID_ARGUMENT) - .withDescription("Invalid public key") - .asRuntimeException(); + private static final StatusRuntimeException INVALID_PUBLIC_KEY_EXCEPTION = + GrpcExceptions.fieldViolation("pre_keys", "invalid public key"); - private static final StatusRuntimeException INVALID_SIGNATURE_EXCEPTION = Status.fromCode(Status.Code.INVALID_ARGUMENT) - .withDescription("Invalid signature") - .asRuntimeException(); + private static final StatusRuntimeException INVALID_SIGNATURE_EXCEPTION = + GrpcExceptions.fieldViolation("pre_keys", "pre-key signature did not match account identity key"); private enum PreKeyType { EC, @@ -75,10 +72,8 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { @Override public Mono getPreKeyCount(final GetPreKeyCountRequest request) { return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) - .flatMap(authenticatedDevice -> Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedDevice.accountIdentifier())) - .map(maybeAccount -> maybeAccount - .map(account -> Tuples.of(account, authenticatedDevice.deviceId())) - .orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))) + .flatMap(authenticatedDevice -> getAuthenticatedAccount(authenticatedDevice.accountIdentifier()) + .zipWith(Mono.just(authenticatedDevice.deviceId()))) .flatMapMany(accountAndDeviceId -> Flux.just( Tuples.of(IdentityType.ACI, accountAndDeviceId.getT1().getUuid(), accountAndDeviceId.getT2()), Tuples.of(IdentityType.PNI, accountAndDeviceId.getT1().getPhoneNumberIdentifier(), accountAndDeviceId.getT2()) @@ -132,15 +127,22 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { deviceId; return rateLimiters.getPreKeysLimiter().validateReactive(rateLimitKey) - .then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier)) - .flatMap(Mono::justOrEmpty)) - .switchIfEmpty(Mono.error(Status.NOT_FOUND.asException())) - .flatMap(targetAccount -> - KeysGrpcHelper.getPreKeys(targetAccount, targetIdentifier, deviceId, keysManager)); + .then(Mono.fromFuture(() -> accountsManager.getByServiceIdentifierAsync(targetIdentifier))) + .flatMap(Mono::justOrEmpty) + .flatMap(targetAccount -> KeysGrpcHelper.getPreKeys(targetAccount, targetIdentifier, deviceId, keysManager)) + .map(bundles -> GetPreKeysResponse.newBuilder() + .setPreKeys(bundles) + .build()) + .switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysResponse.newBuilder() + .setTargetNotFound(NotFound.getDefaultInstance()) + .build())); } @Override public Mono setOneTimeEcPreKeys(final SetOneTimeEcPreKeysRequest request) { + if (request.getPreKeysList().isEmpty()) { + throw GrpcExceptions.fieldViolation("pre_keys", "pre_keys must be non-empty"); + } return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) .flatMap(authenticatedDevice -> storeOneTimePreKeys(authenticatedDevice.accountIdentifier(), request.getPreKeysList(), @@ -151,6 +153,9 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { @Override public Mono setOneTimeKemSignedPreKeys(final SetOneTimeKemSignedPreKeysRequest request) { + if (request.getPreKeysList().isEmpty()) { + throw GrpcExceptions.fieldViolation("pre_keys", "pre_keys must be non-empty"); + } return Mono.fromSupplier(AuthenticationUtil::requireAuthenticatedDevice) .flatMap(authenticatedDevice -> storeOneTimePreKeys(authenticatedDevice.accountIdentifier(), request.getPreKeysList(), @@ -165,17 +170,12 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { final BiFunction extractPreKeyFunction, final BiFunction, CompletableFuture> storeKeysFunction) { - return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid)) - .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + return getAuthenticatedAccount(authenticatedAccountUuid) .map(account -> { final List preKeys = requestPreKeys.stream() .map(requestPreKey -> extractPreKeyFunction.apply(requestPreKey, account.getIdentityKey(identityType))) .toList(); - if (preKeys.isEmpty()) { - throw Status.INVALID_ARGUMENT.asRuntimeException(); - } - return Tuples.of(account.getIdentifier(identityType), preKeys); }) .flatMap(identifierAndPreKeys -> Mono.fromFuture(() -> storeKeysFunction.apply(identifierAndPreKeys.getT1(), identifierAndPreKeys.getT2()))) @@ -218,8 +218,7 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { final BiFunction extractKeyFunction, final BiFunction> storeKeyFunction) { - return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid)) - .map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException)) + return getAuthenticatedAccount(authenticatedAccountUuid) .map(account -> { final IdentityKey identityKey = account.getIdentityKey(IdentityTypeUtil.fromGrpcIdentityType(identityType)); final K key = extractKeyFunction.apply(storeKeyRequest, identityKey); @@ -269,4 +268,9 @@ public class KeysGrpcService extends ReactorKeysGrpc.KeysImplBase { throw INVALID_PUBLIC_KEY_EXCEPTION; } } + + private Mono getAuthenticatedAccount(final UUID authenticatedAccountId) { + return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountId)) + .map(maybeAccount -> maybeAccount.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials"))); + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java index e89bee75b..35d2f9f89 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/grpc/ServiceIdentifierUtil.java @@ -24,7 +24,7 @@ public class ServiceIdentifierUtil { try { uuid = UUIDUtil.fromByteString(serviceIdentifier.getUuid()); } catch (final IllegalArgumentException e) { - throw Status.INVALID_ARGUMENT.asRuntimeException(); + throw GrpcExceptions.invalidArguments("invalid service identifier"); } return switch (IdentityTypeUtil.fromGrpcIdentityType(serviceIdentifier.getIdentityType())) { diff --git a/service/src/main/proto/org/signal/chat/errors.proto b/service/src/main/proto/org/signal/chat/errors.proto index a90dbb492..356f8aa99 100644 --- a/service/src/main/proto/org/signal/chat/errors.proto +++ b/service/src/main/proto/org/signal/chat/errors.proto @@ -25,3 +25,10 @@ message FailedZkAuthentication { // An optional description with additional information about the failure. string description = 1; } + +// Response message that indicates authorization to perform an unidentified +// operation via an endorsement or access key failed +message FailedUnidentifiedAuthorization { + // An optional description with additional information about the failure. + string description = 1; +} diff --git a/service/src/main/proto/org/signal/chat/keys.proto b/service/src/main/proto/org/signal/chat/keys.proto index 097103760..4f72a21c9 100644 --- a/service/src/main/proto/org/signal/chat/keys.proto +++ b/service/src/main/proto/org/signal/chat/keys.proto @@ -10,6 +10,7 @@ option java_multiple_files = true; package org.signal.chat.keys; import "org/signal/chat/common.proto"; +import "org/signal/chat/errors.proto"; // Provides methods for working with pre-keys. service Keys { @@ -22,49 +23,30 @@ service Keys { // device or devices. Note that callers with an unidentified access key for // the targeted account should use the version of this method in // `KeysAnonymous` instead. - // - // This RPC may fail with a `NOT_FOUND` status if the target account was not - // found, if no active device with the given ID (if specified) was found on - // the target account, or if the account has no active devices. It may also - // fail with a `RESOURCE_EXHAUSTED` if a rate limit for fetching keys has been - // exceeded, in which case a `retry-after` header containing an ISO 8601 - // duration string will be present in the response trailers. rpc GetPreKeys(GetPreKeysRequest) returns (GetPreKeysResponse) {} // Uploads a new set of one-time EC pre-keys for the authenticated device, // clearing any previously-stored pre-keys. Note that all keys submitted via // a single call to this method _must_ have the same identity type (i.e. if // the first key has an ACI identity type, then all other keys in the same - // stream must also have an ACI identity type). - // - // This RPC may fail with an `INVALID_ARGUMENT` status if one or more of the - // given pre-keys was structurally invalid or if the list of pre-keys was - // empty. + // stream must also have an ACI identity type). The provided list of pre-keys + // must be non-empty. rpc SetOneTimeEcPreKeys (SetOneTimeEcPreKeysRequest) returns (SetPreKeyResponse) {} // Uploads a new set of one-time KEM pre-keys for the authenticated device, // clearing any previously-stored pre-keys. Note that all keys submitted via // a single call to this method _must_ have the same identity type (i.e. if // the first key has an ACI identity type, then all other keys in the same - // stream must also have an ACI identity type). - // - // This RPC may fail with an `INVALID_ARGUMENT` status if one or more of the - // given pre-keys was structurally invalid, had an invalid signature, or if - // the list of pre-keys was empty. + // stream must also have an ACI identity type). The provided list of pre-keys + // must be non-empty. rpc SetOneTimeKemSignedPreKeys (SetOneTimeKemSignedPreKeysRequest) returns (SetPreKeyResponse) {} // Sets the signed EC pre-key for one identity (i.e. ACI or PNI) associated // with the authenticated device. - // - // This RPC may fail with an `INVALID_ARGUMENT` status if the given pre-key - // was structurally invalid, had a bad signature, or was missing entirely. rpc SetEcSignedPreKey (SetEcSignedPreKeyRequest) returns (SetPreKeyResponse) {} // Sets the last-resort KEM pre-key for one identity (i.e. ACI or PNI) // associated with the authenticated device. - // - // This RPC may fail with an `INVALID_ARGUMENT` status if the given pre-key - // was structurally invalid, had a bad signature, or was missing entirely. rpc SetKemLastResortPreKey (SetKemLastResortPreKeyRequest) returns (SetPreKeyResponse) {} } @@ -78,14 +60,7 @@ service KeysAnonymous { // unidentified access key as an anonymous authentication mechanism. Callers // without an unidentified access key should use the equivalent, authenticated // method in `Keys` instead. - // - // This RPC may fail with an `UNAUTHENTICATED` status if the given - // unidentified access key did not match the target account's unidentified - // access key or if the target account was not found. It may also fail with a - // `NOT_FOUND` status if no active device with the given ID (if specified) was - // found on the target account, or if the target account has no active - // devices. - rpc GetPreKeys(GetPreKeysAnonymousRequest) returns (GetPreKeysResponse) {} + rpc GetPreKeys(GetPreKeysAnonymousRequest) returns (GetPreKeysAnonymousResponse) {} // Checks identity key fingerprints of the target accounts. // @@ -139,29 +114,54 @@ message GetPreKeysAnonymousRequest { } } -message GetPreKeysResponse { - message PreKeyBundle { - // The EC signed pre-key associated with the targeted - // account/device/identity. - common.EcSignedPreKey ec_signed_pre_key = 1; +message DevicePreKeyBundle { + // The EC signed pre-key associated with the targeted + // account/device/identity. + common.EcSignedPreKey ec_signed_pre_key = 1; - // A one-time EC pre-key for the targeted account/device/identity. May not - // be set if no one-time EC pre-keys are available. - common.EcPreKey ec_one_time_pre_key = 2; + // A one-time EC pre-key for the targeted account/device/identity. May not + // be set if no one-time EC pre-keys are available. + common.EcPreKey ec_one_time_pre_key = 2; - // A one-time KEM pre-key (or a last-resort KEM pre-key) for the targeted - // account/device/identity. - common.KemSignedPreKey kem_one_time_pre_key = 3; + // A one-time KEM pre-key (or a last-resort KEM pre-key) for the targeted + // account/device/identity. + common.KemSignedPreKey kem_one_time_pre_key = 3; - // The registration ID for the targeted account/device/identity. - uint32 registration_id = 4; - } + // The registration ID for the targeted account/device/identity. + uint32 registration_id = 4; +} +message AccountPreKeyBundles { // The identity key associated with the targeted account/identity. bytes identity_key = 1; // A map of device IDs to pre-key "bundles" for the targeted account. - map pre_keys = 2; + map device_pre_keys = 2; +} + +message GetPreKeysResponse { + oneof response { + // The requested pre-key bundles + AccountPreKeyBundles pre_keys = 1; + + // Either the target account was not found, no active device with the given + // ID (if specified) was found on the target account. + errors.NotFound target_not_found = 2; + } +} + +message GetPreKeysAnonymousResponse { + oneof response { + // The requested pre-key bundles + AccountPreKeyBundles pre_keys = 1; + + // Either the target account was not found, no active device with the given + // ID (if specified) was found on the target account. + errors.NotFound target_not_found = 2; + + // The provided anonymous authorization credential was invalid + errors.FailedUnidentifiedAuthorization failed_unidentified_authorization = 3; + } } message SetOneTimeEcPreKeysRequest { 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 23756e902..d3a6af8ce 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/grpc/KeysAnonymousGrpcServiceTest.java @@ -6,6 +6,7 @@ package org.whispersystems.textsecuregcm.grpc; 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.anyByte; import static org.mockito.ArgumentMatchers.eq; @@ -33,10 +34,12 @@ 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.AccountPreKeyBundles; import org.signal.chat.keys.CheckIdentityKeyRequest; +import org.signal.chat.keys.DevicePreKeyBundle; import org.signal.chat.keys.GetPreKeysAnonymousRequest; +import org.signal.chat.keys.GetPreKeysAnonymousResponse; import org.signal.chat.keys.GetPreKeysRequest; -import org.signal.chat.keys.GetPreKeysResponse; import org.signal.chat.keys.KeysAnonymousGrpc; import org.signal.chat.keys.ReactorKeysAnonymousGrpc; import org.signal.libsignal.protocol.IdentityKey; @@ -107,20 +110,21 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest devicePreKeysMap = new HashMap<>(); final Map devices = new HashMap<>(); - final Map expectedPreKeyBundles = new HashMap<>(); + final Map expectedPreKeyBundles = new HashMap<>(); final byte deviceId1 = 1; final byte deviceId2 = 2; @@ -498,7 +501,7 @@ class KeysGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + final GetPreKeysResponse response = authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() .setTargetIdentifier(ServiceIdentifier.newBuilder() .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) .setUuid(UUIDUtil.toByteString(UUID.randomUUID())) .build()) - .build())); + .build()); + assertTrue(response.hasTargetNotFound()); } @Test @@ -592,13 +598,14 @@ class KeysGrpcServiceTest extends SimpleBaseGrpcTest authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() + final GetPreKeysResponse response = authenticatedServiceStub().getPreKeys(GetPreKeysRequest.newBuilder() .setTargetIdentifier(ServiceIdentifier.newBuilder() .setIdentityType(org.signal.chat.common.IdentityType.IDENTITY_TYPE_ACI) .setUuid(UUIDUtil.toByteString(accountIdentifier)) .build()) .setDeviceId(Device.PRIMARY_ID) - .build())); + .build()); + assertTrue(response.hasTargetNotFound()); } @Test