mirror of
https://github.com/signalapp/libsignal.git
synced 2024-09-20 03:52:17 +02:00
java: Implement GroupEndorsement APIs
This commit is contained in:
parent
cdef8228a2
commit
2aa3c34088
@ -0,0 +1,228 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.zkgroup.integrationtests;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.Test;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.protocol.util.Hex;
|
||||
import org.signal.libsignal.zkgroup.SecureRandomTest;
|
||||
import org.signal.libsignal.zkgroup.ServerPublicParams;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
|
||||
|
||||
public final class GroupSendEndorsementTest extends SecureRandomTest {
|
||||
private static final byte[] TEST_ARRAY_32 =
|
||||
Hex.fromStringCondensedAssert(
|
||||
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
|
||||
|
||||
private static final byte[] TEST_ARRAY_32_1 =
|
||||
Hex.fromStringCondensedAssert(
|
||||
"6465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283");
|
||||
|
||||
private static final byte[] TEST_ARRAY_32_2 =
|
||||
Hex.fromStringCondensedAssert(
|
||||
"c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7");
|
||||
|
||||
@Test
|
||||
public void testGroupSendIntegration() throws Exception {
|
||||
ServiceId.Aci aliceServiceId =
|
||||
ServiceId.Aci.parseFromString("38381c3b-2606-4ca7-9310-7cb927f2ab4a");
|
||||
ServiceId.Aci bobServiceId =
|
||||
ServiceId.Aci.parseFromString("e80f7bbe-5b94-471e-bd8c-2173654ea3d1");
|
||||
ServiceId.Aci eveServiceId =
|
||||
ServiceId.Aci.parseFromString("3f0f4734-e331-4434-bd4f-6d8f6ea6dcc7");
|
||||
ServiceId.Aci malloryServiceId =
|
||||
ServiceId.Aci.parseFromString("5d088142-6fd7-4dbd-af00-fdda1b3ce988");
|
||||
|
||||
// SERVER
|
||||
// Generate keys
|
||||
ServerSecretParams serverSecretParams =
|
||||
ServerSecretParams.generate(createSecureRandom(TEST_ARRAY_32));
|
||||
ServerPublicParams serverPublicParams = serverSecretParams.getPublicParams();
|
||||
|
||||
// CLIENT
|
||||
// Generate keys
|
||||
GroupMasterKey masterKey = new GroupMasterKey(TEST_ARRAY_32_1);
|
||||
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
|
||||
|
||||
// Set up group state
|
||||
UuidCiphertext aliceCiphertext =
|
||||
new ClientZkGroupCipher(groupSecretParams).encrypt(aliceServiceId);
|
||||
List<UuidCiphertext> groupCiphertexts =
|
||||
Stream.of(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId)
|
||||
.map((next) -> new ClientZkGroupCipher(groupSecretParams).encrypt(next))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// SERVER
|
||||
// Issue endorsements
|
||||
Instant expiration = Instant.now().truncatedTo(ChronoUnit.DAYS).plus(2, ChronoUnit.DAYS);
|
||||
GroupSendDerivedKeyPair keyPair =
|
||||
GroupSendDerivedKeyPair.forExpiration(expiration, serverSecretParams);
|
||||
GroupSendEndorsementsResponse response =
|
||||
GroupSendEndorsementsResponse.issue(groupCiphertexts, keyPair);
|
||||
|
||||
// CLIENT
|
||||
// Gets stored endorsements
|
||||
GroupSendEndorsementsResponse.ReceivedEndorsements receivedEndorsements =
|
||||
response.receive(
|
||||
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
|
||||
aliceServiceId,
|
||||
groupSecretParams,
|
||||
serverPublicParams);
|
||||
|
||||
assertThrows(
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
response.receive(
|
||||
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
|
||||
aliceServiceId,
|
||||
groupSecretParams,
|
||||
serverPublicParams));
|
||||
assertThrows(
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
response.receive(
|
||||
Arrays.asList(aliceServiceId, eveServiceId, malloryServiceId),
|
||||
aliceServiceId,
|
||||
groupSecretParams,
|
||||
serverPublicParams));
|
||||
|
||||
// Try receive with ciphertexts instead.
|
||||
{
|
||||
GroupSendEndorsementsResponse.ReceivedEndorsements repeatReceivedEndorsements =
|
||||
response.receive(groupCiphertexts, aliceCiphertext, serverPublicParams);
|
||||
assertEquals(receivedEndorsements.endorsements, repeatReceivedEndorsements.endorsements);
|
||||
assertEquals(
|
||||
receivedEndorsements.combinedEndorsement, repeatReceivedEndorsements.combinedEndorsement);
|
||||
|
||||
assertThrows(
|
||||
"missing local user",
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
response.receive(
|
||||
groupCiphertexts.stream().skip(1).collect(Collectors.toList()),
|
||||
aliceCiphertext,
|
||||
serverPublicParams));
|
||||
assertThrows(
|
||||
"missing another user",
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
response.receive(
|
||||
groupCiphertexts.stream().limit(3).collect(Collectors.toList()),
|
||||
aliceCiphertext,
|
||||
serverPublicParams));
|
||||
}
|
||||
|
||||
GroupSendEndorsement.Token combinedToken =
|
||||
receivedEndorsements.combinedEndorsement.toToken(groupSecretParams);
|
||||
GroupSendFullToken fullCombinedToken = combinedToken.toFullToken(response.getExpiration());
|
||||
|
||||
// SERVER
|
||||
// Verify token
|
||||
GroupSendDerivedKeyPair verifyKey =
|
||||
GroupSendDerivedKeyPair.forExpiration(
|
||||
fullCombinedToken.getExpiration(), serverSecretParams);
|
||||
|
||||
fullCombinedToken.verify(
|
||||
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId), verifyKey);
|
||||
fullCombinedToken.verify(
|
||||
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
|
||||
Instant.now().plus(1, ChronoUnit.HOURS),
|
||||
verifyKey);
|
||||
|
||||
assertThrows(
|
||||
"included extra user",
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
fullCombinedToken.verify(
|
||||
Arrays.asList(aliceServiceId, bobServiceId, eveServiceId, malloryServiceId),
|
||||
verifyKey));
|
||||
assertThrows(
|
||||
"missing user",
|
||||
VerificationFailedException.class,
|
||||
() -> fullCombinedToken.verify(Arrays.asList(eveServiceId, malloryServiceId), verifyKey));
|
||||
|
||||
assertThrows(
|
||||
"expired",
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
fullCombinedToken.verify(
|
||||
Arrays.asList(bobServiceId, eveServiceId, malloryServiceId),
|
||||
Instant.now()
|
||||
.truncatedTo(ChronoUnit.DAYS)
|
||||
.plus(2, ChronoUnit.DAYS)
|
||||
.plus(1, ChronoUnit.SECONDS),
|
||||
verifyKey));
|
||||
|
||||
// Excluding a user
|
||||
{
|
||||
// CLIENT
|
||||
GroupSendEndorsement everybodyButMallory =
|
||||
receivedEndorsements.combinedEndorsement.byRemoving(
|
||||
receivedEndorsements.endorsements.get(3));
|
||||
GroupSendFullToken fullEverybodyButMalloryToken =
|
||||
everybodyButMallory.toToken(groupSecretParams).toFullToken(response.getExpiration());
|
||||
|
||||
// SERVER
|
||||
GroupSendDerivedKeyPair everybodyButMalloryKey =
|
||||
GroupSendDerivedKeyPair.forExpiration(
|
||||
fullEverybodyButMalloryToken.getExpiration(), serverSecretParams);
|
||||
|
||||
fullEverybodyButMalloryToken.verify(
|
||||
Arrays.asList(bobServiceId, eveServiceId), everybodyButMalloryKey);
|
||||
}
|
||||
|
||||
// Custom combine
|
||||
{
|
||||
// CLIENT
|
||||
GroupSendEndorsement bobAndEve =
|
||||
GroupSendEndorsement.combine(
|
||||
Arrays.asList(
|
||||
receivedEndorsements.endorsements.get(1),
|
||||
receivedEndorsements.endorsements.get(2)));
|
||||
GroupSendFullToken fullBobAndEveToken =
|
||||
bobAndEve.toToken(groupSecretParams).toFullToken(response.getExpiration());
|
||||
|
||||
// SERVER
|
||||
GroupSendDerivedKeyPair bobAndEveKey =
|
||||
GroupSendDerivedKeyPair.forExpiration(
|
||||
fullBobAndEveToken.getExpiration(), serverSecretParams);
|
||||
|
||||
fullBobAndEveToken.verify(Arrays.asList(bobServiceId, eveServiceId), bobAndEveKey);
|
||||
}
|
||||
|
||||
// Single-user
|
||||
{
|
||||
// CLIENT
|
||||
GroupSendEndorsement bobEndorsement = receivedEndorsements.endorsements.get(1);
|
||||
GroupSendFullToken fullBobToken =
|
||||
bobEndorsement.toToken(groupSecretParams).toFullToken(response.getExpiration());
|
||||
|
||||
// SERVER
|
||||
GroupSendDerivedKeyPair bobKey =
|
||||
GroupSendDerivedKeyPair.forExpiration(fullBobToken.getExpiration(), serverSecretParams);
|
||||
|
||||
fullBobToken.verify(Arrays.asList(bobServiceId), bobKey);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.zkgroup.groupsend;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.time.Instant;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.ServerSecretParams;
|
||||
import org.signal.libsignal.zkgroup.internal.ByteArray;
|
||||
|
||||
/**
|
||||
* The key pair used to issue and verify group send endorsements.
|
||||
*
|
||||
* <p>Group send endorsements use a different key pair depending on the endorsement's expiration
|
||||
* (but not the user ID being endorsed). The server may cache these keys to avoid the (small) cost
|
||||
* of deriving them from the root key in {@link ServerSecretParams}. The key object stores the
|
||||
* expiration so that it doesn't need to be provided again when issuing endorsements.
|
||||
*
|
||||
* @see GroupSendEndorsementsResponse#issue
|
||||
* @see GroupSendFullToken#verify
|
||||
*/
|
||||
public final class GroupSendDerivedKeyPair extends ByteArray {
|
||||
public GroupSendDerivedKeyPair(byte[] contents) throws InvalidInputException {
|
||||
super(contents);
|
||||
filterExceptions(
|
||||
InvalidInputException.class,
|
||||
() -> Native.GroupSendDerivedKeyPair_CheckValidContents(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives a new key for group send endorsements that expire at {@code expiration}.
|
||||
*
|
||||
* <p>{@code expiration} must be day-aligned as a protection against fingerprinting by the issuing
|
||||
* server.
|
||||
*/
|
||||
public static GroupSendDerivedKeyPair forExpiration(
|
||||
Instant expiration, ServerSecretParams params) {
|
||||
byte[] newContents =
|
||||
Native.GroupSendDerivedKeyPair_ForExpiration(
|
||||
expiration.getEpochSecond(), params.getInternalContentsForJNI());
|
||||
return filterExceptions(() -> new GroupSendDerivedKeyPair(newContents));
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.zkgroup.groupsend;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.internal.ByteArray;
|
||||
|
||||
/**
|
||||
* An endorsement for a user or set of users in a group.
|
||||
*
|
||||
* <p>GroupSendEndorsements provide a form of authorization by demonstrating that the holder of the
|
||||
* endorsement is in a group with a particular user or set of users. They can be {@link #combine
|
||||
* combined} and {@link #byRemoving removed} in a set-like fashion.
|
||||
*
|
||||
* <p>The endorsement "flow" starts with receiving a {@link GroupSendEndorsementsResponse} from the
|
||||
* group server, which contains endorsements for all members in a group (including the local user).
|
||||
* The response object provides the single expiration for all the endorsements. From there, the
|
||||
* {@code receive} method produces a {@link GroupSendEndorsementsResponse.ReceivedEndorsements},
|
||||
* which exposes the individual endorsements as well as a combined endorsement for everyone but the
|
||||
* local user. Clients should save these endorsements and the expiration with the group state.
|
||||
*
|
||||
* <p>When it comes time to send a message to an individual user, clients should check to see if
|
||||
* they have a {@link GroupSendEndorsement.Token} for that user, and generate and cache one using
|
||||
* {@link #toToken} if not. The token should then be converted to a full token using {@link
|
||||
* GroupSendEndorsement.Token#toFullToken}, providing the expiration saved previously. Finally, the
|
||||
* serialized full token can be used as authorization in a request to the chat server.
|
||||
*
|
||||
* <p>Similarly, when it comes time to send a message to the group, clients should start by {@link
|
||||
* #byRemoving removing} the endorsements of any users they are excluding (say, because they need a
|
||||
* Sender Key Distribution Message first), and then converting the resulting endorsement to a token.
|
||||
* From there, the token can be converted to a full token and serialized as for an individual send.
|
||||
* (Saving the repeated work of converting to a token is left to the clients here; worst case, it's
|
||||
* still cheaper than a usual zkgroup presentation.)
|
||||
*/
|
||||
public final class GroupSendEndorsement extends ByteArray {
|
||||
public GroupSendEndorsement(byte[] contents) throws InvalidInputException {
|
||||
super(contents);
|
||||
filterExceptions(
|
||||
InvalidInputException.class,
|
||||
() -> Native.GroupSendEndorsement_CheckValidContents(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines several endorsements into one.
|
||||
*
|
||||
* <p>For example, if you have endorsements to send to Meredith and Aruna individually, then you
|
||||
* can combine them to produce an endorsement to send a multi-recipient message to the two of
|
||||
* them.
|
||||
*/
|
||||
public static GroupSendEndorsement combine(List<GroupSendEndorsement> endorsements) {
|
||||
ByteBuffer[] buffers = new ByteBuffer[endorsements.size()];
|
||||
int nextOffset = 0;
|
||||
for (GroupSendEndorsement next : endorsements) {
|
||||
byte[] nextEndorsementRaw = next.getInternalContentsForJNI();
|
||||
buffers[nextOffset] = ByteBuffer.allocateDirect(nextEndorsementRaw.length);
|
||||
buffers[nextOffset].put(nextEndorsementRaw);
|
||||
++nextOffset;
|
||||
}
|
||||
|
||||
byte[] rawCombinedEndorsement = Native.GroupSendEndorsement_Combine(buffers);
|
||||
return filterExceptions(() -> new GroupSendEndorsement(rawCombinedEndorsement));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an endorsement (individual or combined) from this combined endorsement.
|
||||
*
|
||||
* <p>If {@code this} is <em>not</em> a combined endorsement, or {@code toRemove} includes
|
||||
* endorsements that were not combined into {@code this}, the result will not generate valid
|
||||
* tokens.
|
||||
*/
|
||||
public GroupSendEndorsement byRemoving(GroupSendEndorsement toRemove) {
|
||||
byte[] rawResult =
|
||||
Native.GroupSendEndorsement_Remove(
|
||||
getInternalContentsForJNI(), toRemove.getInternalContentsForJNI());
|
||||
return filterExceptions(() -> new GroupSendEndorsement(rawResult));
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal cacheable representation of an endorsement.
|
||||
*
|
||||
* <p>This contains the minimal information needed to represent this specific endorsement; it must
|
||||
* be converted to a {@link GroupSendFullToken} before sending to the chat server. (It is valid to
|
||||
* do this immediately; it just uses up extra space.)
|
||||
*
|
||||
* <p>Generated by {@link GroupSendEndorsement#toToken}.
|
||||
*/
|
||||
public static class Token extends ByteArray {
|
||||
public Token(byte[] contents) throws InvalidInputException {
|
||||
super(contents);
|
||||
filterExceptions(
|
||||
InvalidInputException.class, () -> Native.GroupSendToken_CheckValidContents(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this token to a "full token", which can be sent to the chat server as
|
||||
* authentication.
|
||||
*
|
||||
* <p>{@code expiration} must be the same expiration that was in the original {@link
|
||||
* GroupSendEndorsementsResponse}, or the resulting token will fail to verify.
|
||||
*/
|
||||
public GroupSendFullToken toFullToken(Instant expiration) {
|
||||
byte[] rawResult =
|
||||
Native.GroupSendToken_ToFullToken(
|
||||
getInternalContentsForJNI(), expiration.getEpochSecond());
|
||||
return filterExceptions(() -> new GroupSendFullToken(rawResult));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a cacheable token used to authenticate sends.
|
||||
*
|
||||
* <p>The token is no longer associated with the group; it merely identifies the user or set of
|
||||
* users referenced by this endorsement. (Of course, a set of users is a pretty good stand-in for
|
||||
* a group.)
|
||||
*
|
||||
* @see Token
|
||||
*/
|
||||
public Token toToken(GroupSecretParams groupParams) {
|
||||
byte[] rawResult =
|
||||
Native.GroupSendEndorsement_ToToken(
|
||||
getInternalContentsForJNI(), groupParams.getInternalContentsForJNI());
|
||||
return filterExceptions(() -> new Token(rawResult));
|
||||
}
|
||||
}
|
@ -0,0 +1,231 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.zkgroup.groupsend;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
import static org.signal.libsignal.zkgroup.internal.Constants.RANDOM_LENGTH;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.ServerPublicParams;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
||||
import org.signal.libsignal.zkgroup.internal.ByteArray;
|
||||
|
||||
/**
|
||||
* A set of endorsements of the members in a group, along with a proof of their validity.
|
||||
*
|
||||
* <p>Issued by the group server based on the group's member ciphertexts. The endorsements will
|
||||
* eventually be verified by the chat server in the form of {@link GroupSendFullToken}s. See {@link
|
||||
* GroupSendEndorsement} for a full description of the endorsement flow from the client's
|
||||
* perspective.
|
||||
*/
|
||||
public final class GroupSendEndorsementsResponse extends ByteArray {
|
||||
public GroupSendEndorsementsResponse(byte[] contents) throws InvalidInputException {
|
||||
super(contents);
|
||||
filterExceptions(
|
||||
InvalidInputException.class,
|
||||
() -> Native.GroupSendEndorsementsResponse_CheckValidContents(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a new set of endorsements for {@code groupMembers}.
|
||||
*
|
||||
* <p>{@code groupMembers} should include {@code requestingUser} as well.
|
||||
*/
|
||||
public static GroupSendEndorsementsResponse issue(
|
||||
List<UuidCiphertext> groupMembers, GroupSendDerivedKeyPair keyPair) {
|
||||
return issue(groupMembers, keyPair, new SecureRandom());
|
||||
}
|
||||
|
||||
/**
|
||||
* Issues a new set of endorsements for {@code groupMembers}.
|
||||
*
|
||||
* <p>{@code groupMembers} should include {@code requestingUser} as well.
|
||||
*/
|
||||
public static GroupSendEndorsementsResponse issue(
|
||||
List<UuidCiphertext> groupMembers,
|
||||
GroupSendDerivedKeyPair keyPair,
|
||||
SecureRandom secureRandom) {
|
||||
byte[] random = new byte[RANDOM_LENGTH];
|
||||
secureRandom.nextBytes(random);
|
||||
|
||||
byte[] newContents =
|
||||
filterExceptions(
|
||||
() ->
|
||||
Native.GroupSendEndorsementsResponse_IssueDeterministic(
|
||||
UuidCiphertext.serializeAndConcatenate(groupMembers),
|
||||
keyPair.getInternalContentsForJNI(),
|
||||
random));
|
||||
return filterExceptions(() -> new GroupSendEndorsementsResponse(newContents));
|
||||
}
|
||||
|
||||
/** Returns the expiration for the contained endorsements. */
|
||||
public Instant getExpiration() {
|
||||
return Instant.ofEpochSecond(
|
||||
Native.GroupSendEndorsementsResponse_GetExpiration(getInternalContentsForJNI()));
|
||||
}
|
||||
|
||||
/**
|
||||
* A collection of endorsements known to be valid.
|
||||
*
|
||||
* <p>The result of the {@code receive} operations on {@link GroupSendEndorsementsResponse}.
|
||||
* Contains an endorsement for each member of the group, in the same order they were originally
|
||||
* provided, plus a combined endorsement for "everyone but me", intended for multi-recipient
|
||||
* sends.
|
||||
*/
|
||||
public class ReceivedEndorsements {
|
||||
/**
|
||||
* One endorsement per member of the group, in the same order the members were originally
|
||||
* provided.
|
||||
*/
|
||||
public List<GroupSendEndorsement> endorsements;
|
||||
|
||||
/** An endorsement for everyone in the group but the local user, for multi-recipient sends. */
|
||||
public GroupSendEndorsement combinedEndorsement;
|
||||
|
||||
<T> ReceivedEndorsements(
|
||||
List<GroupSendEndorsement> endorsements, List<T> members, T localMember) {
|
||||
this.endorsements = endorsements;
|
||||
|
||||
int memberCount = members.size();
|
||||
assert endorsements.size() == memberCount;
|
||||
|
||||
ByteBuffer[] buffers = new ByteBuffer[memberCount - 1];
|
||||
int nextOffset = 0;
|
||||
for (int i = 0; i < memberCount; ++i) {
|
||||
if (members.get(i).equals(localMember)) {
|
||||
continue;
|
||||
}
|
||||
if (nextOffset == memberCount - 1) {
|
||||
throw new IllegalArgumentException("member list did not contain the local user");
|
||||
}
|
||||
byte[] nextEndorsementRaw = endorsements.get(i).getInternalContentsForJNI();
|
||||
buffers[nextOffset] = ByteBuffer.allocateDirect(nextEndorsementRaw.length);
|
||||
buffers[nextOffset].put(nextEndorsementRaw);
|
||||
++nextOffset;
|
||||
}
|
||||
|
||||
byte[] rawCombinedEndorsement = Native.GroupSendEndorsement_Combine(buffers);
|
||||
this.combinedEndorsement =
|
||||
filterExceptions(() -> new GroupSendEndorsement(rawCombinedEndorsement));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives, validates, and extracts the endorsements from a response.
|
||||
*
|
||||
* <p>Note that the {@code receive} operation is provided for both {@link ServiceId}s and {@link
|
||||
* UuidCiphertext}s. If you already have the ciphertexts for the group members available, {@link
|
||||
* #receive(List, UuidCiphertext, ServerPublicParams)} will be <em>significantly</em> faster; if
|
||||
* you don't, this method is faster than generating the ciphertexts and throwing them away
|
||||
* afterwards.
|
||||
*
|
||||
* <p>{@code localUser} should be included in {@code groupMembers}.
|
||||
*
|
||||
* @throws VerificationFailedException if the endorsements are not valid for any reason
|
||||
*/
|
||||
public ReceivedEndorsements receive(
|
||||
List<ServiceId> groupMembers,
|
||||
ServiceId.Aci localUser,
|
||||
GroupSecretParams groupParams,
|
||||
ServerPublicParams serverParams)
|
||||
throws VerificationFailedException {
|
||||
return receive(groupMembers, localUser, Instant.now(), groupParams, serverParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives, validates, and extracts the endorsements from a response, assuming a specific current
|
||||
* time.
|
||||
*
|
||||
* <p>This should only be used for testing purposes.
|
||||
*
|
||||
* @see #receive(List, ServiceId.Aci, GroupSecretParams, ServerPublicParams)
|
||||
*/
|
||||
public ReceivedEndorsements receive(
|
||||
List<ServiceId> groupMembers,
|
||||
ServiceId.Aci localUser,
|
||||
Instant now,
|
||||
GroupSecretParams groupParams,
|
||||
ServerPublicParams serverParams)
|
||||
throws VerificationFailedException {
|
||||
byte[][] endorsementContents =
|
||||
filterExceptions(
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
(byte[][])
|
||||
Native.GroupSendEndorsementsResponse_ReceiveWithServiceIds(
|
||||
getInternalContentsForJNI(),
|
||||
ServiceId.toConcatenatedFixedWidthBinary(groupMembers),
|
||||
now.getEpochSecond(),
|
||||
groupParams.getInternalContentsForJNI(),
|
||||
serverParams.getInternalContentsForJNI()));
|
||||
|
||||
List<GroupSendEndorsement> endorsements = new ArrayList<>(endorsementContents.length);
|
||||
for (byte[] contents : endorsementContents) {
|
||||
endorsements.add(filterExceptions(() -> new GroupSendEndorsement(contents)));
|
||||
}
|
||||
return new ReceivedEndorsements(endorsements, groupMembers, localUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives, validates, and extracts the endorsements from a response.
|
||||
*
|
||||
* <p>Note that the {@code receive} operation is provided for both {@link ServiceId}s and {@link
|
||||
* UuidCiphertext}s. If you already have the ciphertexts for the group members available, this
|
||||
* method will be <em>significantly</em> faster; if you don't, {@link #receive(List,
|
||||
* ServiceId.Aci, GroupSecretParams, ServerPublicParams)} is faster than generating the
|
||||
* ciphertexts and throwing them away afterwards.
|
||||
*
|
||||
* <p>{@code localUser} should be included in {@code groupMembers}.
|
||||
*
|
||||
* @throws VerificationFailedException if the endorsements are not valid for any reason
|
||||
*/
|
||||
public ReceivedEndorsements receive(
|
||||
List<UuidCiphertext> groupMembers, UuidCiphertext localUser, ServerPublicParams serverParams)
|
||||
throws VerificationFailedException {
|
||||
return receive(groupMembers, localUser, Instant.now(), serverParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives, validates, and extracts the endorsements from a response, assuming a specific current
|
||||
* time.
|
||||
*
|
||||
* <p>This should only be used for testing purposes.
|
||||
*
|
||||
* @see #receive(List, UuidCiphertext, ServerPublicParams)
|
||||
*/
|
||||
public ReceivedEndorsements receive(
|
||||
List<UuidCiphertext> groupMembers,
|
||||
UuidCiphertext localUser,
|
||||
Instant now,
|
||||
ServerPublicParams serverParams)
|
||||
throws VerificationFailedException {
|
||||
byte[][] endorsementContents =
|
||||
filterExceptions(
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
(byte[][])
|
||||
Native.GroupSendEndorsementsResponse_ReceiveWithCiphertexts(
|
||||
getInternalContentsForJNI(),
|
||||
UuidCiphertext.serializeAndConcatenate(groupMembers),
|
||||
now.getEpochSecond(),
|
||||
serverParams.getInternalContentsForJNI()));
|
||||
|
||||
List<GroupSendEndorsement> endorsements = new ArrayList<>(endorsementContents.length);
|
||||
for (byte[] contents : endorsementContents) {
|
||||
endorsements.add(filterExceptions(() -> new GroupSendEndorsement(contents)));
|
||||
}
|
||||
return new ReceivedEndorsements(endorsements, groupMembers, localUser);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright 2024 Signal Messenger, LLC.
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
//
|
||||
|
||||
package org.signal.libsignal.zkgroup.groupsend;
|
||||
|
||||
import static org.signal.libsignal.internal.FilterExceptions.filterExceptions;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import org.signal.libsignal.internal.Native;
|
||||
import org.signal.libsignal.protocol.ServiceId;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.VerificationFailedException;
|
||||
import org.signal.libsignal.zkgroup.internal.ByteArray;
|
||||
|
||||
/**
|
||||
* A token representing a particular {@link GroupSendEndorsement}, along with the endorsement's
|
||||
* expiration.
|
||||
*
|
||||
* <p>Generated by {@link GroupSendEndorsement.Token#toFullToken}, and verified by the chat server.
|
||||
*/
|
||||
public final class GroupSendFullToken extends ByteArray {
|
||||
public GroupSendFullToken(byte[] contents) throws InvalidInputException {
|
||||
super(contents);
|
||||
filterExceptions(
|
||||
InvalidInputException.class, () -> Native.GroupSendFullToken_CheckValidContents(contents));
|
||||
}
|
||||
|
||||
/** Gets the expiration embedded in the token. */
|
||||
public Instant getExpiration() {
|
||||
return Instant.ofEpochSecond(
|
||||
Native.GroupSendFullToken_GetExpiration(getInternalContentsForJNI()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that this token was generated from an endorsement of {@code userIds} by {@code
|
||||
* keyPair}.
|
||||
*
|
||||
* <p>The correct {@code keyPair} must be selected based on {@link #getExpiration}.
|
||||
*
|
||||
* @throws VerificationFailedException if the token is invalid.
|
||||
*/
|
||||
public void verify(List<ServiceId> userIds, GroupSendDerivedKeyPair keyPair)
|
||||
throws VerificationFailedException {
|
||||
verify(userIds, Instant.now(), keyPair);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that this token was generated from an endorsement of {@code userIds} by {@code
|
||||
* keyPair}, assuming a specific current time.
|
||||
*
|
||||
* <p>This should only be used for testing purposes.
|
||||
*
|
||||
* @see #verify(List, GroupSendDerivedKeyPair)
|
||||
*/
|
||||
public void verify(List<ServiceId> userIds, Instant now, GroupSendDerivedKeyPair keyPair)
|
||||
throws VerificationFailedException {
|
||||
filterExceptions(
|
||||
VerificationFailedException.class,
|
||||
() ->
|
||||
Native.GroupSendFullToken_Verify(
|
||||
getInternalContentsForJNI(),
|
||||
ServiceId.toConcatenatedFixedWidthBinary(userIds),
|
||||
now.getEpochSecond(),
|
||||
keyPair.getInternalContentsForJNI()));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user