Update error model in keys.proto

This commit is contained in:
Ravi Khadiwala
2026-02-02 08:39:40 -06:00
parent 8804f28cb8
commit 1009f3ba51
8 changed files with 236 additions and 172 deletions

View File

@@ -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<GetPreKeysResponse> getPreKeys(final GetPreKeysAnonymousRequest request) {
public Mono<GetPreKeysAnonymousResponse> 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<Account> lookUpAccount(final ServiceIdentifier serviceIdentifier, final Status onNotFound) {
private Mono<Account> 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) {

View File

@@ -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<GetPreKeysResponse> 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<AccountPreKeyBundles> 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 objects 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());
});
}
}

View File

@@ -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<GetPreKeyCountResponse> 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<SetPreKeyResponse> 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<SetPreKeyResponse> 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<R, IdentityKey, K> extractPreKeyFunction,
final BiFunction<UUID, List<K>, CompletableFuture<Void>> storeKeysFunction) {
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountUuid))
.map(maybeAccount -> maybeAccount.orElseThrow(Status.UNAUTHENTICATED::asRuntimeException))
return getAuthenticatedAccount(authenticatedAccountUuid)
.map(account -> {
final List<K> 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<R, IdentityKey, K> extractKeyFunction,
final BiFunction<Account, K, Mono<?>> 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<Account> getAuthenticatedAccount(final UUID authenticatedAccountId) {
return Mono.fromFuture(() -> accountsManager.getByAccountIdentifierAsync(authenticatedAccountId))
.map(maybeAccount -> maybeAccount.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials")));
}
}

View File

@@ -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())) {