GroupsV2 service changes.
This commit is contained in:
parent
6b2bc924dd
commit
48c33f3dcd
34 changed files with 1450 additions and 153 deletions
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
|||
package org.whispersystems.signalservice.internal.groupsv2;
|
||||
package org.whispersystems.signalservice.api.groupsv2;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
Loading…
Add table
Reference in a new issue