Add a oneof case for the unrestricted access path in gRPC services

This commit is contained in:
Ravi Khadiwala
2026-03-03 12:39:25 -06:00
committed by ravi-signal
parent a90fa5db02
commit 1cf3bf5ecf
6 changed files with 119 additions and 1 deletions

View File

@@ -13,6 +13,7 @@ import java.util.Arrays;
import java.util.concurrent.Flow;
import org.signal.chat.errors.FailedUnidentifiedAuthorization;
import org.signal.chat.errors.NotFound;
import org.signal.chat.keys.AccountPreKeyBundles;
import org.signal.chat.keys.CheckIdentityKeyRequest;
import org.signal.chat.keys.CheckIdentityKeyResponse;
import org.signal.chat.keys.GetPreKeysAnonymousRequest;
@@ -22,6 +23,7 @@ import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
import org.whispersystems.textsecuregcm.storage.AccountsManager;
import org.whispersystems.textsecuregcm.storage.KeysManager;
import reactor.adapter.JdkFlowAdapter;
@@ -74,7 +76,15 @@ public class KeysAnonymousGrpcService extends SimpleKeysAnonymousGrpc.KeysAnonym
.setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance())
.build());
default -> throw GrpcExceptions.fieldViolation("authorization", "invalid authorization type");
case UNRESTRICTED_ACCESS -> accountsManager.getByServiceIdentifier(serviceIdentifier)
.filter(Account::isUnrestrictedUnidentifiedAccess)
.flatMap(targetAccount -> KeysGrpcHelper.getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager))
.map(accountPreKeyBundles -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(accountPreKeyBundles).build())
.orElseGet(() -> GetPreKeysAnonymousResponse.newBuilder()
.setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance())
.build());
case AUTHORIZATION_NOT_SET -> throw GrpcExceptions.fieldViolation("authorization", "invalid authorization type");
};
}

View File

@@ -102,6 +102,8 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
}
case GROUP_SEND_TOKEN ->
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), destinationServiceIdentifier);
case UNRESTRICTED_ACCESS ->
maybeDestination.map(account -> account.isUnrestrictedUnidentifiedAccess()).orElse(false);
case AUTHORIZATION_NOT_SET ->
throw GrpcExceptions.fieldViolation("authorization", "expected authorization token not provided");
};

View File

@@ -9,6 +9,8 @@ option java_multiple_files = true;
package org.signal.chat.keys;
import "google/protobuf/empty.proto";
import "org/signal/chat/common.proto";
import "org/signal/chat/errors.proto";
@@ -111,6 +113,9 @@ message GetPreKeysAnonymousRequest {
// A group send endorsement token for the targeted account.
bytes group_send_token = 3;
// The destination account allows unrestricted unidentified access
google.protobuf.Empty unrestricted_access = 4;
}
}

View File

@@ -202,6 +202,9 @@ message SendSealedSenderMessageRequest {
// A group send endorsement token for the destination account.
bytes group_send_token = 6;
// The destination account allows unrestricted unidentified access
google.protobuf.Empty unrestricted_access = 7;
}
}

View File

@@ -18,6 +18,7 @@ import static org.mockito.Mockito.when;
import static org.whispersystems.textsecuregcm.grpc.GrpcTestUtils.assertStatusException;
import com.google.protobuf.ByteString;
import com.google.protobuf.Empty;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import java.security.MessageDigest;
@@ -36,6 +37,8 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junitpioneer.jupiter.cartesian.CartesianTest;
import org.mockito.Mock;
import org.signal.chat.common.EcPreKey;
import org.signal.chat.common.EcSignedPreKey;
@@ -189,6 +192,60 @@ class KeysAnonymousGrpcServiceTest extends SimpleBaseGrpcTest<KeysAnonymousGrpcS
assertEquals(expectedResponse, response);
}
@CartesianTest
void getPreKeysUnrestricted(@CartesianTest.Values(booleans = {true, false}) boolean includeUak) {
final Account targetAccount = mock(Account.class);
final Device targetDevice = DevicesHelper.createDevice(Device.PRIMARY_ID);
when(targetAccount.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(targetDevice));
final ECKeyPair identityKeyPair = ECKeyPair.generate();
final IdentityKey identityKey = new IdentityKey(identityKeyPair.getPublicKey());
final UUID uuid = UUID.randomUUID();
final AciServiceIdentifier identifier = new AciServiceIdentifier(uuid);
final byte[] unidentifiedAccessKey = TestRandomUtil.nextBytes(UnidentifiedAccessUtil.UNIDENTIFIED_ACCESS_KEY_LENGTH);
when(targetAccount.isUnrestrictedUnidentifiedAccess()).thenReturn(true);
when(targetAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(unidentifiedAccessKey));
when(targetAccount.getIdentifier(IdentityType.ACI)).thenReturn(uuid);
when(targetAccount.getIdentityKey(IdentityType.ACI)).thenReturn(identityKey);
when(accountsManager.getByServiceIdentifier(identifier))
.thenReturn(Optional.of(targetAccount));
final ECPreKey ecPreKey = new ECPreKey(1, ECKeyPair.generate().getPublicKey());
final ECSignedPreKey ecSignedPreKey = KeysHelper.signedECPreKey(2, identityKeyPair);
final KEMSignedPreKey kemSignedPreKey = KeysHelper.signedKEMPreKey(3, identityKeyPair);
final KeysManager.DevicePreKeys devicePreKeys =
new KeysManager.DevicePreKeys(ecSignedPreKey, Optional.of(ecPreKey), kemSignedPreKey);
when(keysManager.takeDevicePreKeys(eq(Device.PRIMARY_ID), eq(identifier), any()))
.thenReturn(CompletableFuture.completedFuture(Optional.of(devicePreKeys)));
final GetPreKeysAnonymousRequest.Builder request = GetPreKeysAnonymousRequest.newBuilder()
.setRequest(GetPreKeysRequest.newBuilder()
.setTargetIdentifier(ServiceIdentifierUtil.toGrpcServiceIdentifier(identifier))
.setDeviceId(Device.PRIMARY_ID));
if (includeUak) {
request.setUnidentifiedAccessKey(ByteString.copyFrom(TestRandomUtil.nextBytes(16)));
} else {
request.setUnrestrictedAccess(Empty.getDefaultInstance());
}
final GetPreKeysAnonymousResponse response = unauthenticatedServiceStub().getPreKeys(request.build());
final GetPreKeysAnonymousResponse expectedResponse = GetPreKeysAnonymousResponse.newBuilder()
.setPreKeys(AccountPreKeyBundles.newBuilder()
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.putDevicePreKeys(Device.PRIMARY_ID, DevicePreKeyBundle.newBuilder()
.setEcOneTimePreKey(toGrpcEcPreKey(ecPreKey))
.setEcSignedPreKey(toGrpcEcSignedPreKey(ecSignedPreKey))
.setKemOneTimePreKey(toGrpcKemSignedPreKey(kemSignedPreKey))
.build()))
.build();
assertEquals(expectedResponse, response);
}
@Test
void getPreKeysNoAuth() {
assertGetKeysFailure(Status.INVALID_ARGUMENT, GetPreKeysAnonymousRequest.newBuilder()

View File

@@ -250,6 +250,43 @@ class MessagesAnonymousGrpcServiceTest extends
generateRequest(serviceIdentifier, false, true, messages, UNIDENTIFIED_ACCESS_KEY, null)));
verifyNoInteractions(messageSender);
}
@CartesianTest
void sendUnrestrictedAccessMessage(
@CartesianTest.Values(booleans = {true, false}) final boolean useUak,
@CartesianTest.Values(booleans = {true, false}) final boolean isUua)
throws MessageTooLargeException, MismatchedDevicesException {
final byte deviceId = Device.PRIMARY_ID;
final int registrationId = 7;
final Device destinationDevice = DevicesHelper.createDevice(deviceId, CLOCK.millis(), registrationId);
final Account destinationAccount = mock(Account.class);
when(destinationAccount.getDevices()).thenReturn(List.of(destinationDevice));
when(destinationAccount.getDevice(deviceId)).thenReturn(Optional.of(destinationDevice));
when(destinationAccount.isUnrestrictedUnidentifiedAccess()).thenReturn(isUua);
when(destinationAccount.getUnidentifiedAccessKey()).thenReturn(Optional.of(UNIDENTIFIED_ACCESS_KEY));
final AciServiceIdentifier serviceIdentifier = new AciServiceIdentifier(UUID.randomUUID());
when(accountsManager.getByServiceIdentifier(serviceIdentifier)).thenReturn(Optional.of(destinationAccount));
final byte[] payload = TestRandomUtil.nextBytes(128);
final Map<Byte, IndividualRecipientMessageBundle.Message> messages =
Map.of(deviceId, IndividualRecipientMessageBundle.Message.newBuilder()
.setRegistrationId(registrationId)
.setPayload(ByteString.copyFrom(payload))
.setType(SendMessageType.UNIDENTIFIED_SENDER)
.build());
final SendSealedSenderMessageRequest request =
generateRequest(serviceIdentifier, false, true, messages, useUak ? TestRandomUtil.nextBytes(16) : null, null);
final SendMessageResponse response = unauthenticatedServiceStub().sendSingleRecipientMessage(request);
final SendMessageResponse.ResponseCase expectedResponse = isUua
? SendMessageResponse.ResponseCase.SUCCESS
: SendMessageResponse.ResponseCase.FAILED_UNIDENTIFIED_AUTHORIZATION;
assertEquals(expectedResponse, response.getResponseCase());
}
@Test
void mismatchedDevices() throws MessageTooLargeException, MismatchedDevicesException {
@@ -544,6 +581,10 @@ class MessagesAnonymousGrpcServiceTest extends
requestBuilder.setGroupSendToken(ByteString.copyFrom(groupSendToken));
}
if (groupSendToken == null && unidentifiedAccessKey == null) {
requestBuilder.setUnrestrictedAccess(Empty.getDefaultInstance());
}
return requestBuilder.build();
}
}