GroupsV2 service changes.

master
Alan Evans 2020-04-01 14:33:23 -03:00 committed by Greyson Parrelli
parent 6b2bc924dd
commit 48c33f3dcd
34 changed files with 1450 additions and 153 deletions

View File

@ -30,7 +30,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import java.io.Closeable;
import java.security.SecureRandom;

View File

@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.KeyBackupService;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
/**
* Location for storing and retrieving application-scoped singletons. Users must call
@ -186,6 +187,7 @@ public class ApplicationDependencies {
}
public interface Provider {
@NonNull GroupsV2Operations provideGroupsV2Operations();
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender();
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver();

View File

@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
import org.thoughtcrime.securesms.gcm.MessageRetriever;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JobMigrator;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
@ -32,6 +31,8 @@ import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
@ -54,11 +55,21 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
this.networkAccess = networkAccess;
}
private @NonNull ClientZkOperations provideClientZkOperations() {
return ClientZkOperations.create(networkAccess.getConfiguration(context));
}
@Override
public @NonNull GroupsV2Operations provideGroupsV2Operations() {
return new GroupsV2Operations(provideClientZkOperations());
}
@Override
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() {
return new SignalServiceAccountManager(networkAccess.getConfiguration(context),
new DynamicCredentialsProvider(context),
BuildConfig.SIGNAL_AGENT);
BuildConfig.SIGNAL_AGENT,
provideGroupsV2Operations());
}
@Override
@ -70,7 +81,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
TextSecurePreferences.isMultiDevice(context),
Optional.fromNullable(IncomingMessageObserver.getPipe()),
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
Optional.of(new SecurityEventListener(context)));
Optional.of(new SecurityEventListener(context)),
provideClientZkOperations().getProfileOperations());
}
@Override
@ -81,7 +93,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
new DynamicCredentialsProvider(context),
BuildConfig.SIGNAL_AGENT,
new PipeConnectivityListener(),
sleepTimer);
sleepTimer,
provideClientZkOperations().getProfileOperations());
}
@Override

View File

@ -9,7 +9,10 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
@ -22,20 +25,26 @@ import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
@ -97,6 +106,7 @@ public class SignalServiceAccountManager {
private final PushServiceSocket pushServiceSocket;
private final CredentialsProvider credentials;
private final String userAgent;
private final GroupsV2Operations groupsV2Operations;
/**
* Construct a SignalServiceAccountManager.
@ -111,16 +121,21 @@ public class SignalServiceAccountManager {
UUID uuid, String e164, String password,
String signalAgent)
{
this(configuration, new StaticCredentialsProvider(uuid, e164, password, null), signalAgent);
this(configuration,
new StaticCredentialsProvider(uuid, e164, password, null),
signalAgent,
new GroupsV2Operations(ClientZkOperations.create(configuration)));
}
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
CredentialsProvider credentialsProvider,
String signalAgent)
String signalAgent,
GroupsV2Operations groupsV2Operations)
{
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent);
this.credentials = credentialsProvider;
this.userAgent = signalAgent;
this.groupsV2Operations = groupsV2Operations;
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations());
this.credentials = credentialsProvider;
this.userAgent = signalAgent;
}
public byte[] getSenderCertificate() throws IOException {
@ -664,7 +679,7 @@ public class SignalServiceAccountManager {
* @return The avatar URL path, if one was written.
*/
public Optional<String> setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, StreamDetails avatar)
throws IOException
throws IOException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
@ -690,6 +705,12 @@ public class SignalServiceAccountManager {
profileAvatarData);
}
public Optional<ProfileKeyCredential> resolveProfileKeyCredential(UUID uuid, ProfileKey profileKey)
throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException
{
return this.pushServiceSocket.retrieveProfile(uuid, profileKey, Optional.absent()).getProfileKeyCredential();
}
public void setUsername(String username) throws IOException {
this.pushServiceSocket.setUsername(username);
}
@ -729,5 +750,11 @@ public class SignalServiceAccountManager {
return tokenMap;
}
public GroupsV2Api getGroupsV2Api() {
return new GroupsV2Api(pushServiceSocket, groupsV2Operations);
}
public GroupsV2Authorization createGroupsV2Authorization(UUID self) {
return new GroupsV2Authorization(self, pushServiceSocket, groupsV2Operations.getAuthOperations());
}
}

View File

@ -168,7 +168,7 @@ public class SignalServiceMessagePipe {
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType)
throws IOException
throws IOException
{
try {
List<String> headers = new LinkedList<>();

View File

@ -6,7 +6,6 @@
package org.whispersystems.signalservice.api;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
@ -17,6 +16,7 @@ import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream;
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
@ -25,6 +25,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifes
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.SleepTimer;
import org.whispersystems.signalservice.api.util.UuidUtil;
@ -61,7 +63,7 @@ public class SignalServiceMessageReceiver {
private final String signalAgent;
private final ConnectivityListener connectivityListener;
private final SleepTimer sleepTimer;
private final ClientZkProfileOperations clientZkProfile;
private final ClientZkProfileOperations clientZkProfileOperations;
/**
* Construct a SignalServiceMessageReceiver.
@ -76,9 +78,10 @@ public class SignalServiceMessageReceiver {
UUID uuid, String e164, String password,
String signalingKey, String signalAgent,
ConnectivityListener listener,
SleepTimer timer)
SleepTimer timer,
ClientZkProfileOperations clientZkProfileOperations)
{
this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), signalAgent, listener, timer);
this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), signalAgent, listener, timer, clientZkProfileOperations);
}
/**
@ -91,15 +94,16 @@ public class SignalServiceMessageReceiver {
CredentialsProvider credentials,
String signalAgent,
ConnectivityListener listener,
SleepTimer timer)
SleepTimer timer,
ClientZkProfileOperations clientZkProfileOperations)
{
this.urls = urls;
this.credentialsProvider = credentials;
this.socket = new PushServiceSocket(urls, credentials, signalAgent);
this.signalAgent = signalAgent;
this.connectivityListener = listener;
this.sleepTimer = timer;
this.clientZkProfile = FeatureFlags.ZK_GROUPS ? new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams())) : null;
this.urls = urls;
this.credentialsProvider = credentials;
this.socket = new PushServiceSocket(urls, credentials, signalAgent, clientZkProfileOperations);
this.signalAgent = signalAgent;
this.connectivityListener = listener;
this.sleepTimer = timer;
this.clientZkProfileOperations = clientZkProfileOperations;
}
/**
@ -123,7 +127,7 @@ public class SignalServiceMessageReceiver {
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType)
throws IOException, VerificationFailedException
throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException
{
Optional<UUID> uuid = address.getUuid();
@ -143,12 +147,19 @@ public class SignalServiceMessageReceiver {
}
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
throws IOException
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
}
public FileInputStream retrieveGroupsV2ProfileAvatar(String path, File destination, long maxSizeBytes)
throws IOException
{
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
return new FileInputStream(destination);
}
/**
* Retrieves a SignalServiceAttachment.
*
@ -224,7 +235,7 @@ public class SignalServiceMessageReceiver {
urls.getNetworkInterceptors(),
urls.getDns());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations);
}
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
@ -235,7 +246,7 @@ public class SignalServiceMessageReceiver {
urls.getNetworkInterceptors(),
urls.getDns());
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations);
}
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {

View File

@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.SessionBuilder;
import org.whispersystems.libsignal.SignalProtocolAddress;
@ -21,6 +22,7 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.messages.SendMessageResult;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -37,8 +39,8 @@ import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage;
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@ -127,9 +129,10 @@ public class SignalServiceMessageSender {
boolean isMultiDevice,
Optional<SignalServiceMessagePipe> pipe,
Optional<SignalServiceMessagePipe> unidentifiedPipe,
Optional<EventListener> eventListener)
Optional<EventListener> eventListener,
ClientZkProfileOperations clientZkProfileOperations)
{
this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener);
this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations);
}
public SignalServiceMessageSender(SignalServiceConfiguration urls,
@ -139,9 +142,10 @@ public class SignalServiceMessageSender {
boolean isMultiDevice,
Optional<SignalServiceMessagePipe> pipe,
Optional<SignalServiceMessagePipe> unidentifiedPipe,
Optional<EventListener> eventListener)
Optional<EventListener> eventListener,
ClientZkProfileOperations clientZkProfileOperations)
{
this.socket = new PushServiceSocket(urls, credentialsProvider, signalAgent);
this.socket = new PushServiceSocket(urls, credentialsProvider, signalAgent, clientZkProfileOperations);
this.store = store;
this.localAddress = new SignalServiceAddress(credentialsProvider.getUuid(), credentialsProvider.getE164());
this.pipe = new AtomicReference<>(pipe);

View File

@ -0,0 +1,13 @@
package org.whispersystems.signalservice.api.crypto;
import java.io.OutputStream;
/**
* Use when the stream is already encrypted.
*/
public final class NoCipherOutputStream extends DigestingOutputStream {
public NoCipherOutputStream(OutputStream outputStream) {
super(outputStream);
}
}

View File

@ -0,0 +1,42 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
/**
* Contains access to all ZK group operations for the client.
* <p>
* Authorization and profile operations.
*/
public final class ClientZkOperations {
private final ClientZkAuthOperations clientZkAuthOperations;
private final ClientZkProfileOperations clientZkProfileOperations;
private final ServerPublicParams serverPublicParams;
public ClientZkOperations(ServerPublicParams serverPublicParams) {
this.serverPublicParams = serverPublicParams;
this.clientZkAuthOperations = new ClientZkAuthOperations (serverPublicParams);
this.clientZkProfileOperations = new ClientZkProfileOperations(serverPublicParams);
}
public static ClientZkOperations create(SignalServiceConfiguration configuration) {
return FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(configuration.getZkGroupServerPublicParams()))
: new ClientZkOperations(null);
}
public ClientZkAuthOperations getAuthOperations() {
return clientZkAuthOperations;
}
public ClientZkProfileOperations getProfileOperations() {
return clientZkProfileOperations;
}
public ServerPublicParams getServerPublicParams() {
return serverPublicParams;
}
}

View File

@ -0,0 +1,13 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.fasterxml.jackson.annotation.JsonProperty;
public class CredentialResponse {
@JsonProperty
private TemporalCredential[] credentials;
public TemporalCredential[] getCredentials() {
return credentials;
}
}

View File

@ -0,0 +1,30 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
/**
* Pair of a {@link DecryptedGroup} and the {@link DecryptedGroupChange} for that version.
*/
public final class DecryptedGroupHistoryEntry {
private final DecryptedGroup group;
private final DecryptedGroupChange change;
DecryptedGroupHistoryEntry(DecryptedGroup group, DecryptedGroupChange change) {
if (group.getVersion() != change.getVersion()) {
throw new AssertionError();
}
this.group = group;
this.change = change;
}
public DecryptedGroup getGroup() {
return group;
}
public DecryptedGroupChange getChange() {
return change;
}
}

View File

@ -1,4 +1,4 @@
package org.whispersystems.signalservice.internal.groupsv2;
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;

View File

@ -0,0 +1,70 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
/**
* Represents a potential new member of a group.
* <p>
* The entry may or may not have a {@link ProfileKeyCredential}.
* <p>
* If it does not, then this user can only be invited.
* <p>
* Equality by UUID only used to makes sure Sets only contain one copy.
*/
public final class GroupCandidate {
private final UUID uuid;
private final Optional<ProfileKeyCredential> profileKeyCredential;
public GroupCandidate(UUID uuid, Optional<ProfileKeyCredential> profileKeyCredential) {
this.uuid = uuid;
this.profileKeyCredential = profileKeyCredential;
}
public UUID getUuid() {
return uuid;
}
public Optional<ProfileKeyCredential> getProfileKeyCredential() {
return profileKeyCredential;
}
public boolean hasProfileKeyCredential() {
return profileKeyCredential.isPresent();
}
public GroupCandidate withProfileKeyCredential(Optional<ProfileKeyCredential> profileKeyCredential) {
return new GroupCandidate(uuid, profileKeyCredential);
}
public static List<UUID> toUuidList(Collection<GroupCandidate> candidates) {
final List<UUID> uuidList = new ArrayList<>(candidates.size());
for (GroupCandidate candidate : candidates) {
uuidList.add(candidate.getUuid());
}
return uuidList;
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != getClass()) {
return false;
}
GroupCandidate other = (GroupCandidate) obj;
return other.uuid == uuid;
}
@Override
public int hashCode() {
return uuid.hashCode();
}
}

View File

@ -1,4 +1,4 @@
package org.whispersystems.signalservice.internal.groupsv2;
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;

View File

@ -0,0 +1,110 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public final class GroupsV2Api {
private final PushServiceSocket socket;
private final GroupsV2Operations groupsOperations;
public GroupsV2Api(PushServiceSocket socket, GroupsV2Operations groupsOperations) {
this.socket = socket;
this.groupsOperations = groupsOperations;
}
public void putNewGroup(GroupsV2Operations.NewGroup newGroup,
GroupsV2Authorization authorization)
throws IOException, VerificationFailedException
{
Group group = newGroup.getNewGroupMessage();
if (newGroup.getAvatar().isPresent()) {
String cdnKey = uploadAvatar(newGroup.getAvatar().get(), newGroup.getGroupSecretParams(), authorization);
group = Group.newBuilder(group)
.setAvatar(cdnKey)
.build();
}
socket.putNewGroupsV2Group(group, authorization.getAuthorizationForToday(newGroup.getGroupSecretParams()));
}
public DecryptedGroup getGroup(GroupSecretParams groupSecretParams,
GroupsV2Authorization authorization)
throws IOException, InvalidGroupStateException, VerificationFailedException
{
Group group = socket.getGroupsV2Group(authorization.getAuthorizationForToday(groupSecretParams));
return groupsOperations.forGroup(groupSecretParams)
.decryptGroup(group);
}
public List<DecryptedGroupHistoryEntry> getGroupHistory(GroupSecretParams groupSecretParams,
int fromRevision,
GroupsV2Authorization authorization)
throws IOException, InvalidGroupStateException, VerificationFailedException
{
GroupChanges group = socket.getGroupsV2GroupHistory(fromRevision, authorization.getAuthorizationForToday(groupSecretParams));
List<GroupChanges.GroupChangeState> changesList = group.getGroupChangesList();
ArrayList<DecryptedGroupHistoryEntry> result = new ArrayList<>(changesList.size());
GroupsV2Operations.GroupOperations groupOperations = groupsOperations.forGroup(groupSecretParams);
for (GroupChanges.GroupChangeState change : changesList) {
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(change.getGroupState());
DecryptedGroupChange decryptedChange = groupOperations.decryptChange(change.getGroupChange(), false);
if (decryptedChange.getVersion() != decryptedGroup.getVersion()) {
throw new InvalidGroupStateException();
}
result.add(new DecryptedGroupHistoryEntry(decryptedGroup, decryptedChange));
}
return result;
}
public String uploadAvatar(byte[] avatar,
GroupSecretParams groupSecretParams,
GroupsV2Authorization authorization)
throws IOException, VerificationFailedException
{
AvatarUploadAttributes form = socket.getGroupsV2AvatarUploadForm(authorization.getAuthorizationForToday(groupSecretParams));
byte[] cipherText;
try {
cipherText = new ClientZkGroupCipher(groupSecretParams).encryptBlob(avatar);
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
socket.uploadGroupV2Avatar(cipherText, form);
return form.getKey();
}
public DecryptedGroupChange patchGroup(GroupChange.Actions groupChange,
GroupSecretParams groupSecretParams,
GroupsV2Authorization authorization)
throws IOException, VerificationFailedException, InvalidGroupStateException
{
String authorizationBasic = authorization.getAuthorizationForToday(groupSecretParams);
GroupChange groupChanges = socket.patchGroupsV2Group(groupChange, authorizationBasic);
return groupsOperations.forGroup(groupSecretParams)
.decryptChange(groupChanges, true);
}
}

View File

@ -0,0 +1,146 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.auth.AuthCredential;
import org.signal.zkgroup.auth.AuthCredentialPresentation;
import org.signal.zkgroup.auth.AuthCredentialResponse;
import org.signal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import okhttp3.Credentials;
public final class GroupsV2Authorization {
private static final String TAG = GroupsV2Authorization.class.getSimpleName();
private final UUID self;
private final PushServiceSocket socket;
private final ClientZkAuthOperations authOperations;
private AuthorizationFactory currentFactory;
public GroupsV2Authorization(UUID self, PushServiceSocket socket, ClientZkAuthOperations authOperations) {
this.self = self;
this.socket = socket;
this.authOperations = authOperations;
}
String getAuthorizationForToday(GroupSecretParams groupSecretParams)
throws IOException, VerificationFailedException
{
final int today = AuthorizationFactory.currentTimeDays();
final AuthorizationFactory currentFactory = getCurrentFactory();
if (currentFactory != null) {
try {
return currentFactory.getAuthorization(groupSecretParams, today);
} catch (NoCredentialForRedemptionTimeException e) {
Log.i(TAG, "Auth out of date, will update auth and try again");
}
}
Log.i(TAG, "Getting new auth tokens");
setCurrentFactory(createFactory(socket.retrieveGroupsV2Credentials(today)));
try {
return getCurrentFactory().getAuthorization(groupSecretParams, today);
} catch (NoCredentialForRedemptionTimeException e) {
Log.w(TAG, "The credentials returned did not include the day requested");
throw new IOException("Failed to get credentials");
}
}
private AuthorizationFactory createFactory(CredentialResponse credentialResponse)
throws IOException, VerificationFailedException
{
HashMap<Integer, AuthCredentialResponse> credentials = new HashMap<>();
for (TemporalCredential credential : credentialResponse.getCredentials()) {
AuthCredentialResponse authCredentialResponse;
try {
authCredentialResponse = new AuthCredentialResponse(credential.getCredential());
} catch (InvalidInputException e) {
throw new IOException(e);
}
credentials.put(credential.getRedemptionTime(), authCredentialResponse);
}
return new AuthorizationFactory(self, authOperations, credentials);
}
private synchronized AuthorizationFactory getCurrentFactory() {
return currentFactory;
}
private synchronized void setCurrentFactory(AuthorizationFactory currentFactory) {
this.currentFactory = currentFactory;
}
private static class AuthorizationFactory {
private final SecureRandom random;
private final ClientZkAuthOperations clientZkAuthOperations;
private final Map<Integer, AuthCredential> credentials;
AuthorizationFactory(UUID self,
ClientZkAuthOperations clientZkAuthOperations,
Map<Integer, AuthCredentialResponse> credentialResponseMap)
throws VerificationFailedException
{
this.random = new SecureRandom();
this.clientZkAuthOperations = clientZkAuthOperations;
this.credentials = verifyCredentials(self, clientZkAuthOperations, credentialResponseMap);
}
static int currentTimeDays() {
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
}
String getAuthorization(GroupSecretParams groupSecretParams, int redemptionTime)
throws NoCredentialForRedemptionTimeException
{
AuthCredential authCredential = credentials.get(redemptionTime);
if (authCredential == null) {
throw new NoCredentialForRedemptionTimeException();
}
AuthCredentialPresentation authCredentialPresentation = clientZkAuthOperations.createAuthCredentialPresentation(random, groupSecretParams, authCredential);
String username = Hex.toStringCondensed(groupSecretParams.getPublicParams().serialize());
String password = Hex.toStringCondensed(authCredentialPresentation.serialize());
return Credentials.basic(username, password);
}
private static Map<Integer, AuthCredential> verifyCredentials(UUID self,
ClientZkAuthOperations clientZkAuthOperations,
Map<Integer, AuthCredentialResponse> credentialResponseMap)
throws VerificationFailedException
{
Map<Integer, AuthCredential> credentials = new HashMap<>(credentialResponseMap.size());
for (Map.Entry<Integer, AuthCredentialResponse> entry : credentialResponseMap.entrySet()) {
int redemptionTime = entry.getKey();
AuthCredentialResponse authCredentialResponse = entry.getValue();
AuthCredential authCredential = clientZkAuthOperations.receiveAuthCredential(self, redemptionTime, authCredentialResponse);
credentials.put(redemptionTime, authCredential);
}
return credentials;
}
}
}

View File

@ -0,0 +1,562 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.DisappearingMessagesTimer;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.PendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.NotarySignature;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.groups.ProfileKeyCiphertext;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.zkgroup.util.UUIDUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
* Contains operations to create, modify and validate groups and group changes.
*/
public final class GroupsV2Operations {
/** Used for undecryptable pending invites */
public static final UUID UNKNOWN_UUID = new UUID(0, 0);
private final ServerPublicParams serverPublicParams;
private final ClientZkProfileOperations clientZkProfileOperations;
private final ClientZkAuthOperations clientZkAuthOperations;
private final SecureRandom random;
public GroupsV2Operations(ClientZkOperations clientZkOperations) {
this.serverPublicParams = clientZkOperations.getServerPublicParams();
this.clientZkProfileOperations = clientZkOperations.getProfileOperations();
this.clientZkAuthOperations = clientZkOperations.getAuthOperations();
this.random = new SecureRandom();
}
/**
* Creates a new group with the title and avatar.
*
* @param self You will be member 0 and the only admin.
* @param members Members must not contain self. Members will be non-admin members of the group.
*/
public NewGroup createNewGroup(final String title,
final Optional<byte[]> avatar,
final GroupCandidate self,
final Set<GroupCandidate> members) {
if (members.contains(self)) {
throw new IllegalArgumentException("Members must not contain self");
}
final GroupSecretParams groupSecretParams = GroupSecretParams.generate(random);
final GroupOperations groupOperations = forGroup(groupSecretParams);
Group.Builder group = Group.newBuilder()
.setVersion(0)
.setPublicKey(ByteString.copyFrom(groupSecretParams.getPublicParams().serialize()))
.setTitle(groupOperations.encryptTitle(title))
.setDisappearingMessagesTimer(groupOperations.encryptTimer(0))
.setAccessControl(AccessControl.newBuilder()
.setAttributes(AccessControl.AccessRequired.MEMBER)
.setMembers(AccessControl.AccessRequired.MEMBER));
group.addMembers(groupOperations.member(self.getProfileKeyCredential().get(), Member.Role.ADMINISTRATOR));
for (GroupCandidate credential : members) {
Member.Role newMemberRole = Member.Role.DEFAULT;
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
if (profileKeyCredential != null) {
group.addMembers(groupOperations.member(profileKeyCredential, newMemberRole));
} else {
group.addPendingMembers(groupOperations.invitee(credential.getUuid(), newMemberRole));
}
}
return new NewGroup(groupSecretParams, group.build(), avatar);
}
public GroupOperations forGroup(final GroupSecretParams groupSecretParams) {
return new GroupOperations(groupSecretParams);
}
public ClientZkProfileOperations getProfileOperations() {
return clientZkProfileOperations;
}
public ClientZkAuthOperations getAuthOperations() {
return clientZkAuthOperations;
}
/**
* Operations on a single group.
*/
public final class GroupOperations {
private final GroupSecretParams groupSecretParams;
private final ClientZkGroupCipher clientZkGroupCipher;
private GroupOperations(GroupSecretParams groupSecretParams) {
this.groupSecretParams = groupSecretParams;
this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
}
public GroupChange.Actions.Builder createModifyGroupTitleAndMembershipChange(final Optional<String> title,
final Set<GroupCandidate> membersToAdd,
final Set<UUID> membersToRemove)
{
if (!Collections.disjoint(GroupCandidate.toUuidList(membersToAdd), membersToRemove)) {
throw new IllegalArgumentException("Overlap between add and remove sets");
}
final GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
if (title.isPresent()) {
actions.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder()
.setTitle(encryptTitle(title.get())));
}
for (GroupCandidate credential : membersToAdd) {
Member.Role newMemberRole = Member.Role.DEFAULT;
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
if (profileKeyCredential != null) {
actions.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder()
.setAdded(groupOperations.member(profileKeyCredential, newMemberRole)));
} else {
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder()
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)));
}
}
for (UUID remove: membersToRemove) {
actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder()
.setDeletedUserId(encryptUuid(remove)));
}
return actions;
}
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
return GroupChange.Actions
.newBuilder()
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder()
.setTimer(encryptTimer(timerDurationSeconds)));
}
public GroupChange.Actions.Builder createUpdateProfileKeyCredentialChange(ProfileKeyCredential profileKeyCredential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, profileKeyCredential);
return GroupChange.Actions
.newBuilder()
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize())));
}
public GroupChange.Actions.Builder createAcceptInviteChange(ProfileKeyCredential profileKeyCredential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, profileKeyCredential);
return GroupChange.Actions
.newBuilder()
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize())));
}
public GroupChange.Actions.Builder createRemoveInvitationChange(final Set<byte[]> uuidCipherTextsFromInvitesToRemove) {
GroupChange.Actions.Builder builder = GroupChange.Actions
.newBuilder();
for (byte[] uuidCipherText: uuidCipherTextsFromInvitesToRemove) {
builder.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().setDeletedUserId(ByteString.copyFrom(uuidCipherText)));
}
return builder;
}
private Member.Builder member(ProfileKeyCredential credential, Member.Role role) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential);
return Member.newBuilder().setRole(role)
.setPresentation(ByteString.copyFrom(presentation.serialize()));
}
public PendingMember.Builder invitee(UUID uuid, Member.Role role) {
UuidCiphertext uuidCiphertext = clientZkGroupCipher.encryptUuid(uuid);
Member member = Member.newBuilder().setRole(role)
.setUserId(ByteString.copyFrom(uuidCiphertext.serialize()))
.build();
return PendingMember.newBuilder().setMember(member);
}
public DecryptedGroup decryptGroup(Group group)
throws VerificationFailedException, InvalidGroupStateException, InvalidProtocolBufferException
{
List<Member> membersList = group.getMembersList();
List<PendingMember> pendingMembersList = group.getPendingMembersList();
List<DecryptedMember> decryptedMembers = new ArrayList<>(membersList.size());
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
for (Member member : membersList) {
decryptedMembers.add(decryptMember(member));
}
for (PendingMember member : pendingMembersList) {
decryptedPendingMembers.add(decryptMember(member));
}
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder()
.setTitle(decryptTitle(group.getTitle()))
.setAvatar(group.getAvatar())
.setAccessControl(group.getAccessControl())
.setVersion(group.getVersion())
.addAllMembers(decryptedMembers)
.addAllPendingMembers(decryptedPendingMembers);
DisappearingMessagesTimer messagesTimer = decryptDisappearingMessagesTimer(group.getDisappearingMessagesTimer());
if (messagesTimer != null) {
builder.setDisappearingMessagesTimer(messagesTimer);
}
return builder.build();
}
/**
* @param verify You might want to avoid verification if you already know it's correct, or you
* are not going to pass to other clients.
* <p>
* Also, if you know it's version 0, do not verify because changes for version 0
* are not signed, but should be empty.
*/
public DecryptedGroupChange decryptChange(GroupChange groupChange, boolean verify)
throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException
{
GroupChange.Actions actions = verify ? getVerifiedActions(groupChange) : getActions(groupChange);
return decryptChange(actions);
}
public DecryptedGroupChange decryptChange(GroupChange.Actions actions)
throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException
{
DecryptedGroupChange.Builder builder = DecryptedGroupChange.newBuilder();
// Field 1
builder.setEditor(decryptUuidToByteString(actions.getSourceUuid()));
// Field 2
builder.setVersion(actions.getVersion());
// Field 3
for (GroupChange.Actions.AddMemberAction addMemberAction : actions.getAddMembersList()) {
UUID uuid = decryptUuid(addMemberAction.getAdded().getUserId());
builder.addNewMembers(DecryptedMember.newBuilder()
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
.setProfileKey(decryptProfileKeyToByteString(addMemberAction.getAdded().getProfileKey(), uuid)));
}
// Field 4
for (GroupChange.Actions.DeleteMemberAction deleteMemberAction : actions.getDeleteMembersList()) {
builder.addDeleteMembers(decryptUuidToByteString(deleteMemberAction.getDeletedUserId()));
}
// Field 5
for (GroupChange.Actions.ModifyMemberRoleAction modifyMemberRoleAction : actions.getModifyMemberRolesList()) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(modifyMemberRoleAction.getRole())
.setUuid(decryptUuidToByteString(modifyMemberRoleAction.getUserId())));
}
// Field 6
for (GroupChange.Actions.ModifyMemberProfileKeyAction modifyMemberProfileKeyAction : actions.getModifyMemberProfileKeysList()) {
try {
ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(modifyMemberProfileKeyAction.getPresentation().toByteArray());
presentation.getProfileKeyCiphertext();
UUID uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize()));
builder.addModifiedProfileKeys(DecryptedMember.newBuilder()
.setRole(Member.Role.UNKNOWN)
.setJoinedAtVersion(-1)
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
.setProfileKey(ByteString.copyFrom(decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid).serialize())));
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
// Field 7
for (GroupChange.Actions.AddPendingMemberAction addPendingMemberAction:actions.getAddPendingMembersList()) {
PendingMember added = addPendingMemberAction.getAdded();
Member member = added.getMember();
ByteString uuidCipherText = member.getUserId();
UUID uuid = decryptUuidOrUnknown(uuidCipherText);
builder.addNewPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
.setUuidCipherText(uuidCipherText)
.setRole(member.getRole())
.setAddedByUuid(decryptUuidToByteString(added.getAddedByUserId()))
.setTimestamp(added.getTimestamp()));
}
// Field 8
for (GroupChange.Actions.DeletePendingMemberAction deletePendingMemberAction : actions.getDeletePendingMembersList()) {
ByteString uuidCipherText = deletePendingMemberAction.getDeletedUserId();
UUID uuid = decryptUuidOrUnknown(uuidCipherText);
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
.setUuidCipherText(uuidCipherText));
}
// Field 9
for (GroupChange.Actions.PromotePendingMemberAction promotePendingMemberAction : actions.getPromotePendingMembersList()) {
ProfileKeyCredentialPresentation profileKeyCredentialPresentation;
try {
profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(promotePendingMemberAction.getPresentation().toByteArray());
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
builder.addPromotePendingMembers(UuidUtil.toByteString(uuid));
}
// Field 10
if (actions.hasModifyTitle()) {
builder.setNewTitle(DecryptedString.newBuilder().setValue(decryptTitle(actions.getModifyTitle().getTitle())));
}
// Field 11
if (actions.hasModifyAvatar()) {
builder.setNewAvatar(DecryptedString.newBuilder().setValue(actions.getModifyAvatar().getAvatar()));
}
// Field 12
if (actions.hasModifyDisappearingMessagesTimer()) {
int duration = decryptDisappearingMessagesTimer(actions.getModifyDisappearingMessagesTimer().getTimer()).getDuration();
builder.setNewTimer(DisappearingMessagesTimer.newBuilder().setDuration(duration));
}
// Field 13
if (actions.hasModifyAttributesAccess()) {
builder.setNewAttributeAccess(actions.getModifyAttributesAccess().getAttributesAccess());
}
// Field 14
if (actions.hasModifyMemberAccess()) {
builder.setNewMemberAccess(actions.getModifyMemberAccess().getMembersAccess());
}
return builder.build();
}
private DecryptedMember decryptMember(Member member)
throws InvalidGroupStateException, VerificationFailedException
{
ByteString userId = member.getUserId();
UUID uuid = decryptUuid(userId);
return DecryptedMember.newBuilder()
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
.setRole(member.getRole())
.build();
}
private DecryptedPendingMember decryptMember(PendingMember member)
throws InvalidGroupStateException, VerificationFailedException
{
ByteString userIdCipherText = member.getMember().getUserId();
UUID uuid = decryptUuidOrUnknown(userIdCipherText);
UUID addedBy = decryptUuid(member.getAddedByUserId());
return DecryptedPendingMember.newBuilder()
.setUuid(ByteString.copyFrom(UUIDUtil.serialize(uuid)))
.setUuidCipherText(userIdCipherText)
.setAddedByUuid(ByteString.copyFrom(UUIDUtil.serialize(addedBy)))
.setRole(member.getMember().getRole())
.build();
}
private ProfileKey decryptProfileKey(ByteString profileKey, UUID uuid) throws VerificationFailedException, InvalidGroupStateException {
try {
ProfileKeyCiphertext profileKeyCiphertext = new ProfileKeyCiphertext(profileKey.toByteArray());
return clientZkGroupCipher.decryptProfileKey(profileKeyCiphertext, uuid);
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
private ByteString decryptProfileKeyToByteString(ByteString profileKey, UUID uuid) throws VerificationFailedException, InvalidGroupStateException {
return ByteString.copyFrom(decryptProfileKey(profileKey, uuid).serialize());
}
private ByteString decryptUuidToByteString(ByteString userId) throws InvalidGroupStateException, VerificationFailedException {
return ByteString.copyFrom(UUIDUtil.serialize(decryptUuid(userId)));
}
private ByteString encryptUuid(UUID uuid) {
return ByteString.copyFrom(clientZkGroupCipher.encryptUuid(uuid).serialize());
}
private UUID decryptUuid(ByteString userId) throws InvalidGroupStateException, VerificationFailedException {
try {
return clientZkGroupCipher.decryptUuid(new UuidCiphertext(userId.toByteArray()));
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
/**
* Attempts to decrypt a UUID, but will return {@link #UNKNOWN_UUID} if it cannot.
*/
private UUID decryptUuidOrUnknown(ByteString userId) {
try {
return clientZkGroupCipher.decryptUuid(new UuidCiphertext(userId.toByteArray()));
} catch (InvalidInputException | VerificationFailedException e) {
return UNKNOWN_UUID;
}
}
private ByteString encryptTitle(String title) {
try {
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob((title == null ? "" : title).getBytes(StandardCharsets.UTF_8)));
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
private String decryptTitle(ByteString cipherText) throws VerificationFailedException {
return new String(decryptBlob(cipherText), StandardCharsets.UTF_8);
}
private DisappearingMessagesTimer decryptDisappearingMessagesTimer(ByteString encryptedTimerMessage)
throws VerificationFailedException, InvalidProtocolBufferException
{
return DisappearingMessagesTimer.parseFrom(decryptBlob(encryptedTimerMessage));
}
private byte[] decryptBlob(ByteString blob) throws VerificationFailedException {
return decryptBlob(blob.toByteArray());
}
public byte[] decryptAvatar(byte[] bytes) throws VerificationFailedException {
return decryptBlob(bytes);
}
private byte[] decryptBlob(byte[] bytes) throws VerificationFailedException {
// TODO GV2: Minimum field length checking should be responsibility of clientZkGroupCipher#decryptBlob
if (bytes == null) return null;
if (bytes.length == 0) return bytes;
if (bytes.length < 28) throw new VerificationFailedException();
return clientZkGroupCipher.decryptBlob(bytes);
}
private ByteString encryptTimer(int timerDurationSeconds) {
try {
DisappearingMessagesTimer timer = DisappearingMessagesTimer.newBuilder()
.setDuration(timerDurationSeconds)
.build();
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(timer.toByteArray()));
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
/**
* Verifies signature and parses actions on a group change.
*/
private GroupChange.Actions getVerifiedActions(GroupChange groupChange)
throws VerificationFailedException, InvalidProtocolBufferException
{
byte[] actionsByteArray = groupChange.getActions().toByteArray();
NotarySignature signature;
try {
signature = new NotarySignature(groupChange.getServerSignature().toByteArray());
} catch (InvalidInputException e) {
throw new VerificationFailedException();
}
serverPublicParams.verifySignature(actionsByteArray, signature);
return GroupChange.Actions.parseFrom(actionsByteArray);
}
/**
* Parses actions on a group change without verification.
*/
private GroupChange.Actions getActions(GroupChange groupChange)
throws InvalidProtocolBufferException
{
return GroupChange.Actions.parseFrom(groupChange.getActions());
}
public GroupChange.Actions.Builder createChangeMembershipRights(AccessControl.AccessRequired newRights) {
return GroupChange.Actions.newBuilder()
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder()
.setMembersAccess(newRights));
}
public GroupChange.Actions.Builder createChangeAttributesRights(AccessControl.AccessRequired newRights) {
return GroupChange.Actions.newBuilder()
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder()
.setAttributesAccess(newRights));
}
}
public static class NewGroup {
private final GroupSecretParams groupSecretParams;
private final Group newGroupMessage;
private final Optional<byte[]> avatar;
private NewGroup(GroupSecretParams groupSecretParams, Group newGroupMessage, Optional<byte[]> avatar) {
this.groupSecretParams = groupSecretParams;
this.newGroupMessage = newGroupMessage;
this.avatar = avatar;
}
public GroupSecretParams getGroupSecretParams() {
return groupSecretParams;
}
public Group getNewGroupMessage() {
return newGroupMessage;
}
public Optional<byte[]> getAvatar() {
return avatar;
}
}
}

View File

@ -0,0 +1,17 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.InvalidInputException;
/**
* Thrown when a group has some data that cannot be decrypted, or is in some other way in an
* unexpected state.
*/
public final class InvalidGroupStateException extends Exception {
InvalidGroupStateException(InvalidInputException e) {
super(e);
}
InvalidGroupStateException() {
}
}

View File

@ -0,0 +1,7 @@
package org.whispersystems.signalservice.api.groupsv2;
/**
* Thrown when we do not have a credential locally for a given time.
*/
public final class NoCredentialForRedemptionTimeException extends Exception {
}

View File

@ -0,0 +1,20 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.fasterxml.jackson.annotation.JsonProperty;
public class TemporalCredential {
@JsonProperty
private byte[] credential;
@JsonProperty
private int redemptionTime;
public byte[] getCredential() {
return credential;
}
public int getRedemptionTime() {
return redemptionTime;
}
}

View File

@ -0,0 +1,24 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.profiles.ProfileKey;
import java.util.UUID;
public final class UuidProfileKey {
private final UUID uuid;
private final ProfileKey profileKey;
public UuidProfileKey(UUID uuid, ProfileKey profileKey) {
this.uuid = uuid;
this.profileKey = profileKey;
}
public UUID getUuid() {
return uuid;
}
public ProfileKey getProfileKey() {
return profileKey;
}
}

View File

@ -0,0 +1,24 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import java.util.UUID;
public final class UuidProfileKeyCredential {
private final UUID uuid;
private final ProfileKeyCredential profileKeyCredential;
public UuidProfileKeyCredential(UUID uuid, ProfileKeyCredential profileKeyCredential) {
this.uuid = uuid;
this.profileKeyCredential = profileKeyCredential;
}
public UUID getUuid() {
return uuid;
}
public ProfileKeyCredential getProfileKeyCredential() {
return profileKeyCredential;
}
}

View File

@ -194,7 +194,7 @@ public final class SignalServiceContent {
* Takes internal protobuf serialization format and processes it into a {@link SignalServiceContent}.
*/
public static SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto)
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
{
SignalServiceMetadata metadata = SignalServiceMetadataProtobufSerializer.fromProtobuf(serviceContentProto.getMetadata());
SignalServiceAddress localAddress = SignalServiceAddressProtobufSerializer.fromProtobuf(serviceContentProto.getLocalAddress());

View File

@ -27,13 +27,13 @@ public final class SignalServiceGroupContext {
}
static Optional<SignalServiceGroupContext> createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
throws InvalidMessageException
throws InvalidMessageException
{
return Optional.fromNullable(create(groupV1, groupV2));
}
public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
throws InvalidMessageException
throws InvalidMessageException
{
if (groupV1 == null && groupV2 == null) {
return null;

View File

@ -49,12 +49,12 @@ public final class SignalServiceGroupV2 {
this.masterKey = masterKey;
}
Builder withRevision(int revision) {
public Builder withRevision(int revision) {
this.revision = revision;
return this;
}
Builder withSignedGroupChange(byte[] signedGroupChange) {
public Builder withSignedGroupChange(byte[] signedGroupChange) {
this.signedGroupChange = signedGroupChange;
return this;
}

View File

@ -0,0 +1,10 @@
package org.whispersystems.signalservice.api.push.exceptions;
/**
* Represents a 409 http conflict error.
*/
public class ConflictException extends NonSuccessfulResponseCodeException {
public ConflictException() {
super("Conflict");
}
}

View File

@ -1,6 +1,6 @@
package org.whispersystems.signalservice.api.push.exceptions;
public class ContactManifestMismatchException extends NonSuccessfulResponseCodeException {
public class ContactManifestMismatchException extends ConflictException {
private final byte[] responseBody;

View File

@ -56,4 +56,14 @@ public final class UuidUtil {
public static UUID fromByteString(ByteString bytes) {
return parseOrThrow(bytes.toByteArray());
}
public static List<UUID> fromByteStrings(Collection<ByteString> byteStringCollection) {
ArrayList<UUID> result = new ArrayList<>(byteStringCollection.size());
for (ByteString byteString : byteStringCollection) {
result.add(fromByteString(byteString));
}
return result;
}
}

View File

@ -1,17 +0,0 @@
package org.whispersystems.signalservice.internal.groupsv2;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
public final class ClientZkOperations {
private final ClientZkProfileOperations clientZkProfileOperations;
public ClientZkOperations(ServerPublicParams serverPublicParams) {
clientZkProfileOperations = new ClientZkProfileOperations(serverPublicParams);
}
public ClientZkProfileOperations getProfileOperations() {
return clientZkProfileOperations;
}
}

View File

@ -8,13 +8,20 @@ package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupChanges;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.signal.zkgroup.profiles.ProfileKeyVersion;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.ecc.ECPublicKey;
@ -26,6 +33,7 @@ import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.groupsv2.CredentialResponse;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
@ -37,6 +45,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
import org.whispersystems.signalservice.api.push.exceptions.ContactManifestMismatchException;
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
@ -59,11 +68,11 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest;
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
import org.whispersystems.signalservice.internal.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
import org.whispersystems.signalservice.internal.push.http.NoCipherOutputStreamFactory;
import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory;
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
import org.whispersystems.signalservice.internal.storage.protos.StorageItems;
@ -75,6 +84,7 @@ import org.whispersystems.signalservice.internal.util.JsonUtil;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
@ -84,8 +94,6 @@ import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collections;
@ -100,7 +108,6 @@ import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.Call;
@ -164,10 +171,16 @@ public class PushServiceSocket {
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
private static final String AVATAR_UPLOAD_PATH = "";
private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto";
private static final String STICKER_PATH = "stickers/%s/full/%d";
private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d";
private static final String GROUPSV2_GROUP = "/v1/groups/";
private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s";
private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form";
private static final Map<String, String> NO_HEADERS = Collections.emptyMap();
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
@ -180,21 +193,25 @@ public class PushServiceSocket {
private final ConnectionHolder[] keyBackupServiceClients;
private final ConnectionHolder[] storageClients;
private final CredentialsProvider credentialsProvider;
private final String signalAgent;
private final SecureRandom random;
private final ClientZkOperations clientZkOperations;
private final CredentialsProvider credentialsProvider;
private final String signalAgent;
private final SecureRandom random;
private final ClientZkProfileOperations clientZkProfileOperations;
public PushServiceSocket(SignalServiceConfiguration serviceConfig, CredentialsProvider credentialsProvider, String signalAgent) {
this.credentialsProvider = credentialsProvider;
this.signalAgent = signalAgent;
this.serviceClients = createServiceConnectionHolders(serviceConfig.getSignalServiceUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
this.cdnClients = createConnectionHolders(serviceConfig.getSignalCdnUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
this.contactDiscoveryClients = createConnectionHolders(serviceConfig.getSignalContactDiscoveryUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
this.keyBackupServiceClients = createConnectionHolders(serviceConfig.getSignalKeyBackupServiceUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
this.storageClients = createConnectionHolders(serviceConfig.getSignalStorageUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
this.random = new SecureRandom();
this.clientZkOperations = FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(serviceConfig.getZkGroupServerPublicParams())) : null;
public PushServiceSocket(SignalServiceConfiguration configuration,
CredentialsProvider credentialsProvider,
String signalAgent,
ClientZkProfileOperations clientZkProfileOperations)
{
this.credentialsProvider = credentialsProvider;
this.signalAgent = signalAgent;
this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
this.cdnClients = createConnectionHolders(configuration.getSignalCdnUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
this.random = new SecureRandom();
this.clientZkProfileOperations = clientZkProfileOperations;
}
public void requestSmsVerificationCode(boolean androidSmsRetriever, Optional<String> captchaToken, Optional<String> challenge) throws IOException {
@ -561,27 +578,27 @@ public class PushServiceSocket {
}
public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess)
throws NonSuccessfulResponseCodeException, VerificationFailedException
throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
}
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target);
ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target, profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
String version = profileKeyIdentifier.serialize();
String credentialRequest = Hex.toStringCondensed(request.serialize());
String subPath = String.format("%s/%s/%s", target, version, credentialRequest);
String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess);
try {
ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target);
ProfileKeyCredentialRequestContext requestContext = clientZkOperations.getProfileOperations().createProfileKeyCredentialRequestContext(random, target, profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
String version = profileKeyIdentifier.serialize();
String credentialRequest = Hex.toStringCondensed(request.serialize());
String subPath = String.format("%s/%s/%s", target, version, credentialRequest);
String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess);
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response, SignalServiceProfile.class);
ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null
? clientZkOperations.getProfileOperations().receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
? clientZkProfileOperations.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
: null;
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential));
@ -623,7 +640,7 @@ public class PushServiceSocket {
}
if (profileAvatar != null) {
uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(),
uploadToCdn(AVATAR_UPLOAD_PATH, formAttributes.getAcl(), formAttributes.getKey(),
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
formAttributes.getCredential(), formAttributes.getDate(),
formAttributes.getSignature(), profileAvatar.getData(),
@ -640,7 +657,7 @@ public class PushServiceSocket {
* @return The avatar URL path, if one was written.
*/
public Optional<String> writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
throws NonSuccessfulResponseCodeException, PushNetworkException
throws NonSuccessfulResponseCodeException, PushNetworkException
{
if (!FeatureFlags.VERSIONED_PROFILES) {
throw new AssertionError();
@ -659,7 +676,7 @@ public class PushServiceSocket {
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
}
uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(),
uploadToCdn(AVATAR_UPLOAD_PATH, formAttributes.getAcl(), formAttributes.getKey(),
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
formAttributes.getCredential(), formAttributes.getDate(),
formAttributes.getSignature(), profileAvatar.getData(),
@ -727,7 +744,7 @@ public class PushServiceSocket {
}
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
throws IOException
throws IOException
{
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body();
@ -793,38 +810,38 @@ public class PushServiceSocket {
}
public StorageManifest getStorageManifest(String authToken) throws IOException {
Response response = makeStorageRequest(authToken, "/v1/storage/manifest", "GET", null);
ResponseBody response = makeStorageRequest(authToken, "/v1/storage/manifest", "GET", null);
if (response.body() == null) {
if (response == null) {
throw new IOException("Missing body!");
}
return StorageManifest.parseFrom(response.body().bytes());
return StorageManifest.parseFrom(response.bytes());
}
public StorageManifest getStorageManifestIfDifferentVersion(String authToken, long version) throws IOException {
Response response = makeStorageRequest(authToken, "/v1/storage/manifest/version/" + version, "GET", null);
ResponseBody response = makeStorageRequest(authToken, "/v1/storage/manifest/version/" + version, "GET", null);
if (response.body() == null) {
if (response == null) {
throw new IOException("Missing body!");
}
return StorageManifest.parseFrom(response.body().bytes());
return StorageManifest.parseFrom(response.bytes());
}
public StorageItems readStorageItems(String authToken, ReadOperation operation) throws IOException {
Response response = makeStorageRequest(authToken, "/v1/storage/read", "PUT", operation.toByteArray());
ResponseBody response = makeStorageRequest(authToken, "/v1/storage/read", "PUT", protobufRequestBody(operation));
if (response.body() == null) {
if (response == null) {
throw new IOException("Missing body!");
}
return StorageItems.parseFrom(response.body().bytes());
return StorageItems.parseFrom(response.bytes());
}
public Optional<StorageManifest> writeStorageContacts(String authToken, WriteOperation writeOperation) throws IOException {
try {
makeStorageRequest(authToken, "/v1/storage", "PUT", writeOperation.toByteArray());
makeStorageRequest(authToken, "/v1/storage", "PUT", protobufRequestBody(writeOperation));
return Optional.absent();
} catch (ContactManifestMismatchException e) {
return Optional.of(StorageManifest.parseFrom(e.getResponseBody()));
@ -860,6 +877,19 @@ public class PushServiceSocket {
}
}
public byte[] uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes)
throws IOException
{
return uploadToCdn(AVATAR_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(),
uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(),
uploadAttributes.getCredential(), uploadAttributes.getDate(),
uploadAttributes.getSignature(),
new ByteArrayInputStream(avatarCipherText),
"application/octet-stream", avatarCipherText.length,
new NoCipherOutputStreamFactory(),
null, null);
}
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentUploadAttributes uploadAttributes)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
@ -1016,46 +1046,66 @@ public class PushServiceSocket {
}
}
private String makeServiceRequest(String urlFragment, String method, String body)
private String makeServiceRequest(String urlFragment, String method, String jsonBody)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
return makeServiceRequest(urlFragment, method, body, NO_HEADERS, NO_HANDLER, Optional.<UnidentifiedAccess>absent());
return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, Optional.<UnidentifiedAccess>absent());
}
private String makeServiceRequest(String urlFragment, String method, String body, Map<String, String> headers)
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
return makeServiceRequest(urlFragment, method, body, headers, NO_HANDLER, Optional.<UnidentifiedAccess>absent());
return makeServiceRequest(urlFragment, method, jsonBody, headers, NO_HANDLER, Optional.<UnidentifiedAccess>absent());
}
private String makeServiceRequest(String urlFragment, String method, String body, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
return makeServiceRequest(urlFragment, method, body, headers, responseCodeHandler, Optional.<UnidentifiedAccess>absent());
return makeServiceRequest(urlFragment, method, jsonBody, headers, responseCodeHandler, Optional.<UnidentifiedAccess>absent());
}
private String makeServiceRequest(String urlFragment, String method, String body, Map<String, String> headers, Optional<UnidentifiedAccess> unidentifiedAccessKey)
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, Optional<UnidentifiedAccess> unidentifiedAccessKey)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
return makeServiceRequest(urlFragment, method, body, headers, NO_HANDLER, unidentifiedAccessKey);
return makeServiceRequest(urlFragment, method, jsonBody, headers, NO_HANDLER, unidentifiedAccessKey);
}
private String makeServiceRequest(String urlFragment, String method, String body, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, Optional<UnidentifiedAccess> unidentifiedAccessKey)
private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, Optional<UnidentifiedAccess> unidentifiedAccessKey)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
ResponseBody responseBody = makeServiceBodyRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, unidentifiedAccessKey);
try {
return responseBody.string();
} catch (IOException e) {
throw new PushNetworkException(e);
}
}
private static RequestBody jsonRequestBody(String jsonBody) {
return jsonBody != null
? RequestBody.create(MediaType.parse("application/json"), jsonBody)
: null;
}
private static RequestBody protobufRequestBody(MessageLite protobufBody) {
return protobufBody != null
? RequestBody.create(MediaType.parse("application/x-protobuf"), protobufBody.toByteArray())
: null;
}
private ResponseBody makeServiceBodyRequest(String urlFragment,
String method,
RequestBody body,
Map<String, String> headers,
ResponseCodeHandler responseCodeHandler,
Optional<UnidentifiedAccess> unidentifiedAccessKey)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey);
int responseCode;
String responseMessage;
String responseBody;
try {
responseCode = response.code();
responseMessage = response.message();
responseBody = response.body().string();
} catch (IOException ioe) {
throw new PushNetworkException(ioe);
}
int responseCode = response.code();
String responseMessage = response.message();
ResponseBody responseBody = response.body();
responseCodeHandler.handle(responseCode);
@ -1071,7 +1121,7 @@ public class PushServiceSocket {
MismatchedDevices mismatchedDevices;
try {
mismatchedDevices = JsonUtil.fromJson(responseBody, MismatchedDevices.class);
mismatchedDevices = JsonUtil.fromJson(responseBody.string(), MismatchedDevices.class);
} catch (JsonProcessingException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
@ -1084,7 +1134,7 @@ public class PushServiceSocket {
StaleDevices staleDevices;
try {
staleDevices = JsonUtil.fromJson(responseBody, StaleDevices.class);
staleDevices = JsonUtil.fromJson(responseBody.string(), StaleDevices.class);
} catch (JsonProcessingException e) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
@ -1096,7 +1146,7 @@ public class PushServiceSocket {
DeviceLimit deviceLimit;
try {
deviceLimit = JsonUtil.fromJson(responseBody, DeviceLimit.class);
deviceLimit = JsonUtil.fromJson(responseBody.string(), DeviceLimit.class);
} catch (JsonProcessingException e) {
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
} catch (IOException e) {
@ -1110,7 +1160,7 @@ public class PushServiceSocket {
RegistrationLockFailure accountLockFailure;
try {
accountLockFailure = JsonUtil.fromJson(responseBody, RegistrationLockFailure.class);
accountLockFailure = JsonUtil.fromJson(responseBody.string(), RegistrationLockFailure.class);
} catch (JsonProcessingException e) {
Log.w(TAG, e);
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
@ -1133,7 +1183,7 @@ public class PushServiceSocket {
return responseBody;
}
private Response getServiceConnection(String urlFragment, String method, String body, Map<String, String> headers, Optional<UnidentifiedAccess> unidentifiedAccess)
private Response getServiceConnection(String urlFragment, String method, RequestBody body, Map<String, String> headers, Optional<UnidentifiedAccess> unidentifiedAccess)
throws PushNetworkException
{
try {
@ -1149,12 +1199,7 @@ public class PushServiceSocket {
Request.Builder request = new Request.Builder();
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
if (body != null) {
request.method(method, RequestBody.create(MediaType.parse("application/json"), body));
} else {
request.method(method, null);
}
request.method(method, body);
for (Map.Entry<String, String> header : headers.entrySet()) {
request.addHeader(header.getKey(), header.getValue());
@ -1277,7 +1322,7 @@ public class PushServiceSocket {
throw new NonSuccessfulResponseCodeException("Response: " + response);
}
private Response makeStorageRequest(String authorization, String path, String method, byte[] body)
private ResponseBody makeStorageRequest(String authorization, String path, String method, RequestBody body)
throws PushNetworkException, NonSuccessfulResponseCodeException
{
ConnectionHolder connectionHolder = getRandom(storageClients, random);
@ -1290,12 +1335,7 @@ public class PushServiceSocket {
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path));
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
if (body != null) {
request.method(method, RequestBody.create(MediaType.parse("application/x-protobuf"), body));
} else {
request.method(method, null);
}
request.method(method, body);
if (connectionHolder.getHostHeader().isPresent()) {
request.addHeader("Host", connectionHolder.getHostHeader().get());
@ -1317,7 +1357,7 @@ public class PushServiceSocket {
response = call.execute();
if (response.isSuccessful() && response.code() != 204) {
return response;
return response.body();
}
} catch (IOException e) {
throw new PushNetworkException(e);
@ -1337,11 +1377,9 @@ public class PushServiceSocket {
throw new NotFoundException("Not found");
case 409:
if (response.body() != null) {
try {
throw new ContactManifestMismatchException(response.body().bytes());
} catch (IOException e) {
throw new PushNetworkException(e);
}
throw new ContactManifestMismatchException(readBodyBytes(response.body()));
} else {
throw new ConflictException();
}
case 429:
throw new RateLimitException("Rate limit exceeded: " + response.code());
@ -1387,6 +1425,10 @@ public class PushServiceSocket {
.connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.RESTRICTED_TLS)))
.dns(dns.or(Dns.SYSTEM));
builder.sslSocketFactory(new Tls12SocketFactory(context.getSocketFactory()), (X509TrustManager)trustManagers[0])
.connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.RESTRICTED_TLS)))
.build();
for (Interceptor interceptor : interceptors) {
builder.addInterceptor(interceptor);
}
@ -1410,6 +1452,23 @@ public class PushServiceSocket {
return connections[random.nextInt(connections.length)];
}
public ProfileKeyCredential parseResponse(UUID uuid, ProfileKey profileKey, ProfileKeyCredentialResponse profileKeyCredentialResponse) throws VerificationFailedException {
ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, uuid, profileKey);
return clientZkProfileOperations.receiveProfileKeyCredential(profileKeyCredentialRequestContext, profileKeyCredentialResponse);
}
/**
* Converts {@link IOException} on body byte reading to {@link PushNetworkException}.
*/
private static byte[] readBodyBytes(ResponseBody response) throws PushNetworkException {
try {
return response.bytes();
} catch (IOException e) {
throw new PushNetworkException(e);
}
}
private static class GcmRegistrationId {
@JsonProperty
@ -1508,4 +1567,87 @@ public class PushServiceSocket {
}
public enum ClientSet { ContactDiscovery, KeyBackup }
public CredentialResponse retrieveGroupsV2Credentials(int today)
throws IOException
{
int todayPlus7 = today + 7;
String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7),
"GET",
null,
NO_HEADERS,
Optional.absent());
return JsonUtil.fromJson(response, CredentialResponse.class);
}
public void putNewGroupsV2Group(Group group, String authorization)
throws NonSuccessfulResponseCodeException, PushNetworkException
{
makeStorageRequest(authorization,
GROUPSV2_GROUP,
"PUT",
protobufRequestBody(group));
}
public Group getGroupsV2Group(String authorization)
throws IOException
{
ResponseBody response = makeStorageRequest(authorization,
GROUPSV2_GROUP,
"GET",
null);
try {
return Group.parseFrom(response.bytes());
} catch (InvalidProtocolBufferException e) {
throw new IOException("Cannot read protobuf", e);
}
}
public AvatarUploadAttributes getGroupsV2AvatarUploadForm(String authorization)
throws IOException
{
ResponseBody response = makeStorageRequest(authorization,
GROUPSV2_AVATAR_REQUEST,
"GET",
null);
try {
return AvatarUploadAttributes.parseFrom(response.bytes());
} catch (InvalidProtocolBufferException e) {
throw new IOException("Cannot read protobuf", e);
}
}
public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization)
throws IOException
{
ResponseBody response = makeStorageRequest(authorization,
GROUPSV2_GROUP,
"PATCH",
protobufRequestBody(groupChange));
try {
return GroupChange.parseFrom(response.bytes());
} catch (InvalidProtocolBufferException e) {
throw new IOException("Cannot read protobuf", e);
}
}
public GroupChanges getGroupsV2GroupHistory(int fromVersion, String authorization)
throws IOException
{
ResponseBody response = makeStorageRequest(authorization,
String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion),
"GET",
null);
try {
return GroupChanges.parseFrom(response.bytes());
} catch (InvalidProtocolBufferException e) {
throw new IOException("Cannot read protobuf", e);
}
}
}

View File

@ -0,0 +1,17 @@
package org.whispersystems.signalservice.internal.push.http;
import org.whispersystems.signalservice.api.crypto.DigestingOutputStream;
import org.whispersystems.signalservice.api.crypto.NoCipherOutputStream;
import java.io.OutputStream;
/**
* See {@link NoCipherOutputStream}.
*/
public final class NoCipherOutputStreamFactory implements OutputStreamFactory {
@Override
public DigestingOutputStream createFor(OutputStream wrap) {
return new NoCipherOutputStream(wrap);
}
}

View File

@ -1,4 +1,4 @@
package org.whispersystems.signalservice.internal.groupsv2;
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;

View File

@ -1,4 +1,4 @@
package org.whispersystems.signalservice.internal.groupsv2;
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Test;
import org.signal.storageservice.protos.groups.GroupChange;

View File

@ -1,4 +1,4 @@
package org.whispersystems.signalservice.internal.groupsv2;
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;