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.messages.SignalServiceAttachmentPointer;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
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.io.Closeable;
|
||||||
import java.security.SecureRandom;
|
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.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Location for storing and retrieving application-scoped singletons. Users must call
|
* Location for storing and retrieving application-scoped singletons. Users must call
|
||||||
|
@ -186,6 +187,7 @@ public class ApplicationDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Provider {
|
public interface Provider {
|
||||||
|
@NonNull GroupsV2Operations provideGroupsV2Operations();
|
||||||
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
|
@NonNull SignalServiceAccountManager provideSignalServiceAccountManager();
|
||||||
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender();
|
@NonNull SignalServiceMessageSender provideSignalServiceMessageSender();
|
||||||
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver();
|
@NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver();
|
||||||
|
|
|
@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||||
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
import org.thoughtcrime.securesms.gcm.MessageRetriever;
|
||||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||||
import org.thoughtcrime.securesms.jobmanager.JobMigrator;
|
import org.thoughtcrime.securesms.jobmanager.JobMigrator;
|
||||||
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
|
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.SignalServiceAccountManager;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
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.CredentialsProvider;
|
||||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||||
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
|
||||||
|
@ -54,11 +55,21 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||||
this.networkAccess = networkAccess;
|
this.networkAccess = networkAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NonNull ClientZkOperations provideClientZkOperations() {
|
||||||
|
return ClientZkOperations.create(networkAccess.getConfiguration(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull GroupsV2Operations provideGroupsV2Operations() {
|
||||||
|
return new GroupsV2Operations(provideClientZkOperations());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() {
|
public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() {
|
||||||
return new SignalServiceAccountManager(networkAccess.getConfiguration(context),
|
return new SignalServiceAccountManager(networkAccess.getConfiguration(context),
|
||||||
new DynamicCredentialsProvider(context),
|
new DynamicCredentialsProvider(context),
|
||||||
BuildConfig.SIGNAL_AGENT);
|
BuildConfig.SIGNAL_AGENT,
|
||||||
|
provideGroupsV2Operations());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -70,7 +81,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||||
TextSecurePreferences.isMultiDevice(context),
|
TextSecurePreferences.isMultiDevice(context),
|
||||||
Optional.fromNullable(IncomingMessageObserver.getPipe()),
|
Optional.fromNullable(IncomingMessageObserver.getPipe()),
|
||||||
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()),
|
||||||
Optional.of(new SecurityEventListener(context)));
|
Optional.of(new SecurityEventListener(context)),
|
||||||
|
provideClientZkOperations().getProfileOperations());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -81,7 +93,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr
|
||||||
new DynamicCredentialsProvider(context),
|
new DynamicCredentialsProvider(context),
|
||||||
BuildConfig.SIGNAL_AGENT,
|
BuildConfig.SIGNAL_AGENT,
|
||||||
new PipeConnectivityListener(),
|
new PipeConnectivityListener(),
|
||||||
sleepTimer);
|
sleepTimer,
|
||||||
|
provideClientZkOperations().getProfileOperations());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -9,7 +9,10 @@ package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
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.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
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.InvalidCiphertextException;
|
||||||
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
|
||||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
|
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageId;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
|
||||||
import org.whispersystems.signalservice.api.storage.StorageKey;
|
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.calls.TurnServerInfo;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
|
||||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
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.NotFoundException;
|
||||||
|
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
||||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
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.storage.StorageManifestKey;
|
||||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||||
|
@ -97,6 +106,7 @@ public class SignalServiceAccountManager {
|
||||||
private final PushServiceSocket pushServiceSocket;
|
private final PushServiceSocket pushServiceSocket;
|
||||||
private final CredentialsProvider credentials;
|
private final CredentialsProvider credentials;
|
||||||
private final String userAgent;
|
private final String userAgent;
|
||||||
|
private final GroupsV2Operations groupsV2Operations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a SignalServiceAccountManager.
|
* Construct a SignalServiceAccountManager.
|
||||||
|
@ -111,16 +121,21 @@ public class SignalServiceAccountManager {
|
||||||
UUID uuid, String e164, String password,
|
UUID uuid, String e164, String password,
|
||||||
String signalAgent)
|
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,
|
public SignalServiceAccountManager(SignalServiceConfiguration configuration,
|
||||||
CredentialsProvider credentialsProvider,
|
CredentialsProvider credentialsProvider,
|
||||||
String signalAgent)
|
String signalAgent,
|
||||||
|
GroupsV2Operations groupsV2Operations)
|
||||||
{
|
{
|
||||||
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent);
|
this.groupsV2Operations = groupsV2Operations;
|
||||||
this.credentials = credentialsProvider;
|
this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations());
|
||||||
this.userAgent = signalAgent;
|
this.credentials = credentialsProvider;
|
||||||
|
this.userAgent = signalAgent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getSenderCertificate() throws IOException {
|
public byte[] getSenderCertificate() throws IOException {
|
||||||
|
@ -664,7 +679,7 @@ public class SignalServiceAccountManager {
|
||||||
* @return The avatar URL path, if one was written.
|
* @return The avatar URL path, if one was written.
|
||||||
*/
|
*/
|
||||||
public Optional<String> setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, StreamDetails avatar)
|
public Optional<String> setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, StreamDetails avatar)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
if (!FeatureFlags.VERSIONED_PROFILES) {
|
if (!FeatureFlags.VERSIONED_PROFILES) {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -690,6 +705,12 @@ public class SignalServiceAccountManager {
|
||||||
profileAvatarData);
|
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 {
|
public void setUsername(String username) throws IOException {
|
||||||
this.pushServiceSocket.setUsername(username);
|
this.pushServiceSocket.setUsername(username);
|
||||||
}
|
}
|
||||||
|
@ -729,5 +750,11 @@ public class SignalServiceAccountManager {
|
||||||
return tokenMap;
|
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<ProfileKey> profileKey,
|
||||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||||
SignalServiceProfile.RequestType requestType)
|
SignalServiceProfile.RequestType requestType)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
List<String> headers = new LinkedList<>();
|
List<String> headers = new LinkedList<>();
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
package org.whispersystems.signalservice.api;
|
package org.whispersystems.signalservice.api;
|
||||||
|
|
||||||
import org.signal.zkgroup.ServerPublicParams;
|
|
||||||
import org.signal.zkgroup.VerificationFailedException;
|
import org.signal.zkgroup.VerificationFailedException;
|
||||||
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
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.AttachmentCipherInputStream;
|
||||||
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
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.SignalServiceAttachment.ProgressListener;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
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.ProfileAndCredential;
|
||||||
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
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.CredentialsProvider;
|
||||||
import org.whispersystems.signalservice.api.util.SleepTimer;
|
import org.whispersystems.signalservice.api.util.SleepTimer;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
@ -61,7 +63,7 @@ public class SignalServiceMessageReceiver {
|
||||||
private final String signalAgent;
|
private final String signalAgent;
|
||||||
private final ConnectivityListener connectivityListener;
|
private final ConnectivityListener connectivityListener;
|
||||||
private final SleepTimer sleepTimer;
|
private final SleepTimer sleepTimer;
|
||||||
private final ClientZkProfileOperations clientZkProfile;
|
private final ClientZkProfileOperations clientZkProfileOperations;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a SignalServiceMessageReceiver.
|
* Construct a SignalServiceMessageReceiver.
|
||||||
|
@ -76,9 +78,10 @@ public class SignalServiceMessageReceiver {
|
||||||
UUID uuid, String e164, String password,
|
UUID uuid, String e164, String password,
|
||||||
String signalingKey, String signalAgent,
|
String signalingKey, String signalAgent,
|
||||||
ConnectivityListener listener,
|
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,
|
CredentialsProvider credentials,
|
||||||
String signalAgent,
|
String signalAgent,
|
||||||
ConnectivityListener listener,
|
ConnectivityListener listener,
|
||||||
SleepTimer timer)
|
SleepTimer timer,
|
||||||
|
ClientZkProfileOperations clientZkProfileOperations)
|
||||||
{
|
{
|
||||||
this.urls = urls;
|
this.urls = urls;
|
||||||
this.credentialsProvider = credentials;
|
this.credentialsProvider = credentials;
|
||||||
this.socket = new PushServiceSocket(urls, credentials, signalAgent);
|
this.socket = new PushServiceSocket(urls, credentials, signalAgent, clientZkProfileOperations);
|
||||||
this.signalAgent = signalAgent;
|
this.signalAgent = signalAgent;
|
||||||
this.connectivityListener = listener;
|
this.connectivityListener = listener;
|
||||||
this.sleepTimer = timer;
|
this.sleepTimer = timer;
|
||||||
this.clientZkProfile = FeatureFlags.ZK_GROUPS ? new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams())) : null;
|
this.clientZkProfileOperations = clientZkProfileOperations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -123,7 +127,7 @@ public class SignalServiceMessageReceiver {
|
||||||
Optional<ProfileKey> profileKey,
|
Optional<ProfileKey> profileKey,
|
||||||
Optional<UnidentifiedAccess> unidentifiedAccess,
|
Optional<UnidentifiedAccess> unidentifiedAccess,
|
||||||
SignalServiceProfile.RequestType requestType)
|
SignalServiceProfile.RequestType requestType)
|
||||||
throws IOException, VerificationFailedException
|
throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException
|
||||||
{
|
{
|
||||||
Optional<UUID> uuid = address.getUuid();
|
Optional<UUID> uuid = address.getUuid();
|
||||||
|
|
||||||
|
@ -143,12 +147,19 @@ public class SignalServiceMessageReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
|
public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
|
socket.retrieveProfileAvatar(path, destination, maxSizeBytes);
|
||||||
return new ProfileCipherInputStream(new FileInputStream(destination), profileKey);
|
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.
|
* Retrieves a SignalServiceAttachment.
|
||||||
*
|
*
|
||||||
|
@ -224,7 +235,7 @@ public class SignalServiceMessageReceiver {
|
||||||
urls.getNetworkInterceptors(),
|
urls.getNetworkInterceptors(),
|
||||||
urls.getDns());
|
urls.getDns());
|
||||||
|
|
||||||
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
|
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
|
public SignalServiceMessagePipe createUnidentifiedMessagePipe() {
|
||||||
|
@ -235,7 +246,7 @@ public class SignalServiceMessageReceiver {
|
||||||
urls.getNetworkInterceptors(),
|
urls.getNetworkInterceptors(),
|
||||||
urls.getDns());
|
urls.getDns());
|
||||||
|
|
||||||
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile);
|
return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {
|
public List<SignalServiceEnvelope> retrieveMessages() throws IOException {
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
|
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.whispersystems.libsignal.InvalidKeyException;
|
import org.whispersystems.libsignal.InvalidKeyException;
|
||||||
import org.whispersystems.libsignal.SessionBuilder;
|
import org.whispersystems.libsignal.SessionBuilder;
|
||||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
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.UnidentifiedAccess;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
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.SendMessageResult;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
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.calls.SignalServiceCallMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
|
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.KeysMessage;
|
||||||
|
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||||
|
@ -127,9 +129,10 @@ public class SignalServiceMessageSender {
|
||||||
boolean isMultiDevice,
|
boolean isMultiDevice,
|
||||||
Optional<SignalServiceMessagePipe> pipe,
|
Optional<SignalServiceMessagePipe> pipe,
|
||||||
Optional<SignalServiceMessagePipe> unidentifiedPipe,
|
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,
|
public SignalServiceMessageSender(SignalServiceConfiguration urls,
|
||||||
|
@ -139,9 +142,10 @@ public class SignalServiceMessageSender {
|
||||||
boolean isMultiDevice,
|
boolean isMultiDevice,
|
||||||
Optional<SignalServiceMessagePipe> pipe,
|
Optional<SignalServiceMessagePipe> pipe,
|
||||||
Optional<SignalServiceMessagePipe> unidentifiedPipe,
|
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.store = store;
|
||||||
this.localAddress = new SignalServiceAddress(credentialsProvider.getUuid(), credentialsProvider.getE164());
|
this.localAddress = new SignalServiceAddress(credentialsProvider.getUuid(), credentialsProvider.getE164());
|
||||||
this.pipe = new AtomicReference<>(pipe);
|
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;
|
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;
|
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}.
|
* Takes internal protobuf serialization format and processes it into a {@link SignalServiceContent}.
|
||||||
*/
|
*/
|
||||||
public static SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto)
|
public static SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto)
|
||||||
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
|
throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException
|
||||||
{
|
{
|
||||||
SignalServiceMetadata metadata = SignalServiceMetadataProtobufSerializer.fromProtobuf(serviceContentProto.getMetadata());
|
SignalServiceMetadata metadata = SignalServiceMetadataProtobufSerializer.fromProtobuf(serviceContentProto.getMetadata());
|
||||||
SignalServiceAddress localAddress = SignalServiceAddressProtobufSerializer.fromProtobuf(serviceContentProto.getLocalAddress());
|
SignalServiceAddress localAddress = SignalServiceAddressProtobufSerializer.fromProtobuf(serviceContentProto.getLocalAddress());
|
||||||
|
|
|
@ -27,13 +27,13 @@ public final class SignalServiceGroupContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Optional<SignalServiceGroupContext> createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
|
static Optional<SignalServiceGroupContext> createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
|
||||||
throws InvalidMessageException
|
throws InvalidMessageException
|
||||||
{
|
{
|
||||||
return Optional.fromNullable(create(groupV1, groupV2));
|
return Optional.fromNullable(create(groupV1, groupV2));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
|
public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2)
|
||||||
throws InvalidMessageException
|
throws InvalidMessageException
|
||||||
{
|
{
|
||||||
if (groupV1 == null && groupV2 == null) {
|
if (groupV1 == null && groupV2 == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -49,12 +49,12 @@ public final class SignalServiceGroupV2 {
|
||||||
this.masterKey = masterKey;
|
this.masterKey = masterKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
Builder withRevision(int revision) {
|
public Builder withRevision(int revision) {
|
||||||
this.revision = revision;
|
this.revision = revision;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
Builder withSignedGroupChange(byte[] signedGroupChange) {
|
public Builder withSignedGroupChange(byte[] signedGroupChange) {
|
||||||
this.signedGroupChange = signedGroupChange;
|
this.signedGroupChange = signedGroupChange;
|
||||||
return this;
|
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;
|
package org.whispersystems.signalservice.api.push.exceptions;
|
||||||
|
|
||||||
public class ContactManifestMismatchException extends NonSuccessfulResponseCodeException {
|
public class ContactManifestMismatchException extends ConflictException {
|
||||||
|
|
||||||
private final byte[] responseBody;
|
private final byte[] responseBody;
|
||||||
|
|
||||||
|
|
|
@ -56,4 +56,14 @@ public final class UuidUtil {
|
||||||
public static UUID fromByteString(ByteString bytes) {
|
public static UUID fromByteString(ByteString bytes) {
|
||||||
return parseOrThrow(bytes.toByteArray());
|
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.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
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.VerificationFailedException;
|
||||||
|
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
|
||||||
import org.signal.zkgroup.profiles.ProfileKeyVersion;
|
import org.signal.zkgroup.profiles.ProfileKeyVersion;
|
||||||
import org.whispersystems.libsignal.IdentityKey;
|
import org.whispersystems.libsignal.IdentityKey;
|
||||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
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.libsignal.util.guava.Optional;
|
||||||
import org.whispersystems.signalservice.FeatureFlags;
|
import org.whispersystems.signalservice.FeatureFlags;
|
||||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
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.SignalServiceAttachment.ProgressListener;
|
||||||
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
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.SignedPreKeyEntity;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException;
|
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.ContactManifestMismatchException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
|
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
|
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.KeyBackupRequest;
|
||||||
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse;
|
||||||
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse;
|
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.MismatchedDevicesException;
|
||||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||||
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
import org.whispersystems.signalservice.internal.push.http.CancelationSignal;
|
||||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
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.push.http.OutputStreamFactory;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItems;
|
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.signalservice.internal.util.Util;
|
||||||
import org.whispersystems.util.Base64;
|
import org.whispersystems.util.Base64;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
@ -84,8 +94,6 @@ import java.io.OutputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -100,7 +108,6 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManager;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
|
@ -164,10 +171,16 @@ public class PushServiceSocket {
|
||||||
|
|
||||||
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
|
private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d";
|
||||||
private static final String ATTACHMENT_UPLOAD_PATH = "attachments/";
|
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_MANIFEST_PATH = "stickers/%s/manifest.proto";
|
||||||
private static final String STICKER_PATH = "stickers/%s/full/%d";
|
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 Map<String, String> NO_HEADERS = Collections.emptyMap();
|
||||||
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
|
private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler();
|
||||||
|
|
||||||
|
@ -180,21 +193,25 @@ public class PushServiceSocket {
|
||||||
private final ConnectionHolder[] keyBackupServiceClients;
|
private final ConnectionHolder[] keyBackupServiceClients;
|
||||||
private final ConnectionHolder[] storageClients;
|
private final ConnectionHolder[] storageClients;
|
||||||
|
|
||||||
private final CredentialsProvider credentialsProvider;
|
private final CredentialsProvider credentialsProvider;
|
||||||
private final String signalAgent;
|
private final String signalAgent;
|
||||||
private final SecureRandom random;
|
private final SecureRandom random;
|
||||||
private final ClientZkOperations clientZkOperations;
|
private final ClientZkProfileOperations clientZkProfileOperations;
|
||||||
|
|
||||||
public PushServiceSocket(SignalServiceConfiguration serviceConfig, CredentialsProvider credentialsProvider, String signalAgent) {
|
public PushServiceSocket(SignalServiceConfiguration configuration,
|
||||||
this.credentialsProvider = credentialsProvider;
|
CredentialsProvider credentialsProvider,
|
||||||
this.signalAgent = signalAgent;
|
String signalAgent,
|
||||||
this.serviceClients = createServiceConnectionHolders(serviceConfig.getSignalServiceUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
|
ClientZkProfileOperations clientZkProfileOperations)
|
||||||
this.cdnClients = createConnectionHolders(serviceConfig.getSignalCdnUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
|
{
|
||||||
this.contactDiscoveryClients = createConnectionHolders(serviceConfig.getSignalContactDiscoveryUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
|
this.credentialsProvider = credentialsProvider;
|
||||||
this.keyBackupServiceClients = createConnectionHolders(serviceConfig.getSignalKeyBackupServiceUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
|
this.signalAgent = signalAgent;
|
||||||
this.storageClients = createConnectionHolders(serviceConfig.getSignalStorageUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns());
|
this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||||
this.random = new SecureRandom();
|
this.cdnClients = createConnectionHolders(configuration.getSignalCdnUrls(), configuration.getNetworkInterceptors(), configuration.getDns());
|
||||||
this.clientZkOperations = FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(serviceConfig.getZkGroupServerPublicParams())) : null;
|
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 {
|
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)
|
public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional<UnidentifiedAccess> unidentifiedAccess)
|
||||||
throws NonSuccessfulResponseCodeException, VerificationFailedException
|
throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException
|
||||||
{
|
{
|
||||||
if (!FeatureFlags.VERSIONED_PROFILES) {
|
if (!FeatureFlags.VERSIONED_PROFILES) {
|
||||||
throw new AssertionError();
|
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 {
|
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);
|
SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response, SignalServiceProfile.class);
|
||||||
|
|
||||||
ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null
|
ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null
|
||||||
? clientZkOperations.getProfileOperations().receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
|
? clientZkProfileOperations.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse())
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential));
|
return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential));
|
||||||
|
@ -623,7 +640,7 @@ public class PushServiceSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profileAvatar != null) {
|
if (profileAvatar != null) {
|
||||||
uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(),
|
uploadToCdn(AVATAR_UPLOAD_PATH, formAttributes.getAcl(), formAttributes.getKey(),
|
||||||
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
|
formAttributes.getPolicy(), formAttributes.getAlgorithm(),
|
||||||
formAttributes.getCredential(), formAttributes.getDate(),
|
formAttributes.getCredential(), formAttributes.getDate(),
|
||||||
formAttributes.getSignature(), profileAvatar.getData(),
|
formAttributes.getSignature(), profileAvatar.getData(),
|
||||||
|
@ -640,7 +657,7 @@ public class PushServiceSocket {
|
||||||
* @return The avatar URL path, if one was written.
|
* @return The avatar URL path, if one was written.
|
||||||
*/
|
*/
|
||||||
public Optional<String> writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
|
public Optional<String> writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar)
|
||||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||||
{
|
{
|
||||||
if (!FeatureFlags.VERSIONED_PROFILES) {
|
if (!FeatureFlags.VERSIONED_PROFILES) {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -659,7 +676,7 @@ public class PushServiceSocket {
|
||||||
throw new NonSuccessfulResponseCodeException("Unable to parse entity");
|
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.getPolicy(), formAttributes.getAlgorithm(),
|
||||||
formAttributes.getCredential(), formAttributes.getDate(),
|
formAttributes.getCredential(), formAttributes.getDate(),
|
||||||
formAttributes.getSignature(), profileAvatar.getData(),
|
formAttributes.getSignature(), profileAvatar.getData(),
|
||||||
|
@ -727,7 +744,7 @@ public class PushServiceSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
|
public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body();
|
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 {
|
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!");
|
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 {
|
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!");
|
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 {
|
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!");
|
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 {
|
public Optional<StorageManifest> writeStorageContacts(String authToken, WriteOperation writeOperation) throws IOException {
|
||||||
try {
|
try {
|
||||||
makeStorageRequest(authToken, "/v1/storage", "PUT", writeOperation.toByteArray());
|
makeStorageRequest(authToken, "/v1/storage", "PUT", protobufRequestBody(writeOperation));
|
||||||
return Optional.absent();
|
return Optional.absent();
|
||||||
} catch (ContactManifestMismatchException e) {
|
} catch (ContactManifestMismatchException e) {
|
||||||
return Optional.of(StorageManifest.parseFrom(e.getResponseBody()));
|
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)
|
public Pair<Long, byte[]> uploadAttachment(PushAttachmentData attachment, AttachmentUploadAttributes uploadAttributes)
|
||||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
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
|
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
|
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
|
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
|
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
|
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||||
{
|
{
|
||||||
Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey);
|
Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey);
|
||||||
|
|
||||||
int responseCode;
|
int responseCode = response.code();
|
||||||
String responseMessage;
|
String responseMessage = response.message();
|
||||||
String responseBody;
|
ResponseBody responseBody = response.body();
|
||||||
|
|
||||||
try {
|
|
||||||
responseCode = response.code();
|
|
||||||
responseMessage = response.message();
|
|
||||||
responseBody = response.body().string();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new PushNetworkException(ioe);
|
|
||||||
}
|
|
||||||
|
|
||||||
responseCodeHandler.handle(responseCode);
|
responseCodeHandler.handle(responseCode);
|
||||||
|
|
||||||
|
@ -1071,7 +1121,7 @@ public class PushServiceSocket {
|
||||||
MismatchedDevices mismatchedDevices;
|
MismatchedDevices mismatchedDevices;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mismatchedDevices = JsonUtil.fromJson(responseBody, MismatchedDevices.class);
|
mismatchedDevices = JsonUtil.fromJson(responseBody.string(), MismatchedDevices.class);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
||||||
|
@ -1084,7 +1134,7 @@ public class PushServiceSocket {
|
||||||
StaleDevices staleDevices;
|
StaleDevices staleDevices;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
staleDevices = JsonUtil.fromJson(responseBody, StaleDevices.class);
|
staleDevices = JsonUtil.fromJson(responseBody.string(), StaleDevices.class);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -1096,7 +1146,7 @@ public class PushServiceSocket {
|
||||||
DeviceLimit deviceLimit;
|
DeviceLimit deviceLimit;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
deviceLimit = JsonUtil.fromJson(responseBody, DeviceLimit.class);
|
deviceLimit = JsonUtil.fromJson(responseBody.string(), DeviceLimit.class);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -1110,7 +1160,7 @@ public class PushServiceSocket {
|
||||||
RegistrationLockFailure accountLockFailure;
|
RegistrationLockFailure accountLockFailure;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
accountLockFailure = JsonUtil.fromJson(responseBody, RegistrationLockFailure.class);
|
accountLockFailure = JsonUtil.fromJson(responseBody.string(), RegistrationLockFailure.class);
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
||||||
|
@ -1133,7 +1183,7 @@ public class PushServiceSocket {
|
||||||
return responseBody;
|
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
|
throws PushNetworkException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@ -1149,12 +1199,7 @@ public class PushServiceSocket {
|
||||||
|
|
||||||
Request.Builder request = new Request.Builder();
|
Request.Builder request = new Request.Builder();
|
||||||
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
|
||||||
|
request.method(method, body);
|
||||||
if (body != null) {
|
|
||||||
request.method(method, RequestBody.create(MediaType.parse("application/json"), body));
|
|
||||||
} else {
|
|
||||||
request.method(method, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||||
request.addHeader(header.getKey(), header.getValue());
|
request.addHeader(header.getKey(), header.getValue());
|
||||||
|
@ -1277,7 +1322,7 @@ public class PushServiceSocket {
|
||||||
throw new NonSuccessfulResponseCodeException("Response: " + response);
|
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
|
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||||
{
|
{
|
||||||
ConnectionHolder connectionHolder = getRandom(storageClients, random);
|
ConnectionHolder connectionHolder = getRandom(storageClients, random);
|
||||||
|
@ -1290,12 +1335,7 @@ public class PushServiceSocket {
|
||||||
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path));
|
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path));
|
||||||
|
|
||||||
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
|
||||||
|
request.method(method, body);
|
||||||
if (body != null) {
|
|
||||||
request.method(method, RequestBody.create(MediaType.parse("application/x-protobuf"), body));
|
|
||||||
} else {
|
|
||||||
request.method(method, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectionHolder.getHostHeader().isPresent()) {
|
if (connectionHolder.getHostHeader().isPresent()) {
|
||||||
request.addHeader("Host", connectionHolder.getHostHeader().get());
|
request.addHeader("Host", connectionHolder.getHostHeader().get());
|
||||||
|
@ -1317,7 +1357,7 @@ public class PushServiceSocket {
|
||||||
response = call.execute();
|
response = call.execute();
|
||||||
|
|
||||||
if (response.isSuccessful() && response.code() != 204) {
|
if (response.isSuccessful() && response.code() != 204) {
|
||||||
return response;
|
return response.body();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new PushNetworkException(e);
|
throw new PushNetworkException(e);
|
||||||
|
@ -1337,11 +1377,9 @@ public class PushServiceSocket {
|
||||||
throw new NotFoundException("Not found");
|
throw new NotFoundException("Not found");
|
||||||
case 409:
|
case 409:
|
||||||
if (response.body() != null) {
|
if (response.body() != null) {
|
||||||
try {
|
throw new ContactManifestMismatchException(readBodyBytes(response.body()));
|
||||||
throw new ContactManifestMismatchException(response.body().bytes());
|
} else {
|
||||||
} catch (IOException e) {
|
throw new ConflictException();
|
||||||
throw new PushNetworkException(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case 429:
|
case 429:
|
||||||
throw new RateLimitException("Rate limit exceeded: " + response.code());
|
throw new RateLimitException("Rate limit exceeded: " + response.code());
|
||||||
|
@ -1387,6 +1425,10 @@ public class PushServiceSocket {
|
||||||
.connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.RESTRICTED_TLS)))
|
.connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.RESTRICTED_TLS)))
|
||||||
.dns(dns.or(Dns.SYSTEM));
|
.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) {
|
for (Interceptor interceptor : interceptors) {
|
||||||
builder.addInterceptor(interceptor);
|
builder.addInterceptor(interceptor);
|
||||||
}
|
}
|
||||||
|
@ -1410,6 +1452,23 @@ public class PushServiceSocket {
|
||||||
return connections[random.nextInt(connections.length)];
|
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 {
|
private static class GcmRegistrationId {
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
@ -1508,4 +1567,87 @@ public class PushServiceSocket {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ClientSet { ContactDiscovery, KeyBackup }
|
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;
|
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.junit.Test;
|
||||||
import org.signal.storageservice.protos.groups.GroupChange;
|
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;
|
import com.google.protobuf.ByteString;
|
||||||
|
|
Loading…
Add table
Reference in a new issue