From 48c33f3dcdaa1e73de31aaae3a7a976381bf9127 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Wed, 1 Apr 2020 14:33:23 -0300 Subject: [PATCH] GroupsV2 service changes. --- .../securesms/database/GroupDatabase.java | 2 +- .../dependencies/ApplicationDependencies.java | 2 + .../ApplicationDependencyProvider.java | 21 +- .../api/SignalServiceAccountManager.java | 45 +- .../api/SignalServiceMessagePipe.java | 2 +- .../api/SignalServiceMessageReceiver.java | 43 +- .../api/SignalServiceMessageSender.java | 14 +- .../api/crypto/NoCipherOutputStream.java | 13 + .../api/groupsv2/ClientZkOperations.java | 42 ++ .../api/groupsv2/CredentialResponse.java | 13 + .../groupsv2/DecryptedGroupHistoryEntry.java | 30 + .../groupsv2/DecryptedGroupUtil.java | 2 +- .../api/groupsv2/GroupCandidate.java | 70 +++ .../groupsv2/GroupChangeUtil.java | 2 +- .../api/groupsv2/GroupsV2Api.java | 110 ++++ .../api/groupsv2/GroupsV2Authorization.java | 146 +++++ .../api/groupsv2/GroupsV2Operations.java | 562 ++++++++++++++++++ .../groupsv2/InvalidGroupStateException.java | 17 + ...oCredentialForRedemptionTimeException.java | 7 + .../api/groupsv2/TemporalCredential.java | 20 + .../api/groupsv2/UuidProfileKey.java | 24 + .../groupsv2/UuidProfileKeyCredential.java | 24 + .../api/messages/SignalServiceContent.java | 2 +- .../messages/SignalServiceGroupContext.java | 4 +- .../api/messages/SignalServiceGroupV2.java | 4 +- .../push/exceptions/ConflictException.java | 10 + .../ContactManifestMismatchException.java | 2 +- .../signalservice/api/util/UuidUtil.java | 10 + .../internal/groupsv2/ClientZkOperations.java | 17 - .../internal/push/PushServiceSocket.java | 320 +++++++--- .../http/NoCipherOutputStreamFactory.java | 17 + .../groupsv2/DecryptedGroupUtilTest.java | 2 +- .../groupsv2/GroupChangeUtilTest.java | 2 +- .../GroupChangeUtil_resolveConflict_Test.java | 2 +- 34 files changed, 1450 insertions(+), 153 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/NoCipherOutputStream.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ClientZkOperations.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/CredentialResponse.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupHistoryEntry.java rename libsignal/service/src/main/java/org/whispersystems/signalservice/{internal => api}/groupsv2/DecryptedGroupUtil.java (98%) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java rename libsignal/service/src/main/java/org/whispersystems/signalservice/{internal => api}/groupsv2/GroupChangeUtil.java (99%) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Authorization.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/InvalidGroupStateException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NoCredentialForRedemptionTimeException.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKey.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKeyCredential.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ConflictException.java delete mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/NoCipherOutputStreamFactory.java rename libsignal/service/src/test/java/org/whispersystems/signalservice/{internal => api}/groupsv2/DecryptedGroupUtilTest.java (95%) rename libsignal/service/src/test/java/org/whispersystems/signalservice/{internal => api}/groupsv2/GroupChangeUtilTest.java (99%) rename libsignal/service/src/test/java/org/whispersystems/signalservice/{internal => api}/groupsv2/GroupChangeUtil_resolveConflict_Test.java (99%) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index 76a0f12ebd..186901fa81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -30,7 +30,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; -import org.whispersystems.signalservice.internal.groupsv2.DecryptedGroupUtil; +import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import java.io.Closeable; import java.security.SecureRandom; diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 323330cf55..f9c4082f5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; /** * Location for storing and retrieving application-scoped singletons. Users must call @@ -186,6 +187,7 @@ public class ApplicationDependencies { } public interface Provider { + @NonNull GroupsV2Operations provideGroupsV2Operations(); @NonNull SignalServiceAccountManager provideSignalServiceAccountManager(); @NonNull SignalServiceMessageSender provideSignalServiceMessageSender(); @NonNull SignalServiceMessageReceiver provideSignalServiceMessageReceiver(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index 6503bc76ec..3f4ab575c9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.crypto.storage.SignalProtocolStoreImpl; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; import org.thoughtcrime.securesms.gcm.MessageRetriever; -import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.JobMigrator; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; @@ -32,6 +31,8 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; @@ -54,11 +55,21 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr this.networkAccess = networkAccess; } + private @NonNull ClientZkOperations provideClientZkOperations() { + return ClientZkOperations.create(networkAccess.getConfiguration(context)); + } + + @Override + public @NonNull GroupsV2Operations provideGroupsV2Operations() { + return new GroupsV2Operations(provideClientZkOperations()); + } + @Override public @NonNull SignalServiceAccountManager provideSignalServiceAccountManager() { return new SignalServiceAccountManager(networkAccess.getConfiguration(context), new DynamicCredentialsProvider(context), - BuildConfig.SIGNAL_AGENT); + BuildConfig.SIGNAL_AGENT, + provideGroupsV2Operations()); } @Override @@ -70,7 +81,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr TextSecurePreferences.isMultiDevice(context), Optional.fromNullable(IncomingMessageObserver.getPipe()), Optional.fromNullable(IncomingMessageObserver.getUnidentifiedPipe()), - Optional.of(new SecurityEventListener(context))); + Optional.of(new SecurityEventListener(context)), + provideClientZkOperations().getProfileOperations()); } @Override @@ -81,7 +93,8 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr new DynamicCredentialsProvider(context), BuildConfig.SIGNAL_AGENT, new PipeConnectivityListener(), - sleepTimer); + sleepTimer, + provideClientZkOperations().getProfileOperations()); } @Override diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 4de6c6cb92..15951e04a3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -9,7 +9,10 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; +import org.signal.zkgroup.VerificationFailedException; +import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; +import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -22,20 +25,26 @@ import org.whispersystems.signalservice.FeatureFlags; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream; -import org.whispersystems.signalservice.api.push.exceptions.NoContentException; -import org.whispersystems.signalservice.api.storage.StorageId; -import org.whispersystems.signalservice.api.storage.StorageKey; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Authorization; +import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; +import org.whispersystems.signalservice.api.push.exceptions.NoContentException; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.storage.SignalStorageCipher; import org.whispersystems.signalservice.api.storage.SignalStorageManifest; import org.whispersystems.signalservice.api.storage.SignalStorageModels; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.api.storage.StorageKey; import org.whispersystems.signalservice.api.storage.StorageManifestKey; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.StreamDetails; @@ -97,6 +106,7 @@ public class SignalServiceAccountManager { private final PushServiceSocket pushServiceSocket; private final CredentialsProvider credentials; private final String userAgent; + private final GroupsV2Operations groupsV2Operations; /** * Construct a SignalServiceAccountManager. @@ -111,16 +121,21 @@ public class SignalServiceAccountManager { UUID uuid, String e164, String password, String signalAgent) { - this(configuration, new StaticCredentialsProvider(uuid, e164, password, null), signalAgent); + this(configuration, + new StaticCredentialsProvider(uuid, e164, password, null), + signalAgent, + new GroupsV2Operations(ClientZkOperations.create(configuration))); } public SignalServiceAccountManager(SignalServiceConfiguration configuration, CredentialsProvider credentialsProvider, - String signalAgent) + String signalAgent, + GroupsV2Operations groupsV2Operations) { - this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent); - this.credentials = credentialsProvider; - this.userAgent = signalAgent; + this.groupsV2Operations = groupsV2Operations; + this.pushServiceSocket = new PushServiceSocket(configuration, credentialsProvider, signalAgent, groupsV2Operations.getProfileOperations()); + this.credentials = credentialsProvider; + this.userAgent = signalAgent; } public byte[] getSenderCertificate() throws IOException { @@ -664,7 +679,7 @@ public class SignalServiceAccountManager { * @return The avatar URL path, if one was written. */ public Optional setVersionedProfile(UUID uuid, ProfileKey profileKey, String name, StreamDetails avatar) - throws IOException + throws IOException { if (!FeatureFlags.VERSIONED_PROFILES) { throw new AssertionError(); @@ -690,6 +705,12 @@ public class SignalServiceAccountManager { profileAvatarData); } + public Optional resolveProfileKeyCredential(UUID uuid, ProfileKey profileKey) + throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException + { + return this.pushServiceSocket.retrieveProfile(uuid, profileKey, Optional.absent()).getProfileKeyCredential(); + } + public void setUsername(String username) throws IOException { this.pushServiceSocket.setUsername(username); } @@ -729,5 +750,11 @@ public class SignalServiceAccountManager { return tokenMap; } + public GroupsV2Api getGroupsV2Api() { + return new GroupsV2Api(pushServiceSocket, groupsV2Operations); + } + public GroupsV2Authorization createGroupsV2Authorization(UUID self) { + return new GroupsV2Authorization(self, pushServiceSocket, groupsV2Operations.getAuthOperations()); + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java index edb348701a..7a133fe40f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessagePipe.java @@ -168,7 +168,7 @@ public class SignalServiceMessagePipe { Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType) - throws IOException + throws IOException { try { List headers = new LinkedList<>(); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java index 6b32a17da5..df445f1b9a 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageReceiver.java @@ -6,7 +6,6 @@ package org.whispersystems.signalservice.api; -import org.signal.zkgroup.ServerPublicParams; import org.signal.zkgroup.VerificationFailedException; import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; @@ -17,6 +16,7 @@ import org.whispersystems.signalservice.FeatureFlags; import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream; import org.whispersystems.signalservice.api.crypto.ProfileCipherInputStream; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -25,6 +25,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifes import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.util.CredentialsProvider; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -61,7 +63,7 @@ public class SignalServiceMessageReceiver { private final String signalAgent; private final ConnectivityListener connectivityListener; private final SleepTimer sleepTimer; - private final ClientZkProfileOperations clientZkProfile; + private final ClientZkProfileOperations clientZkProfileOperations; /** * Construct a SignalServiceMessageReceiver. @@ -76,9 +78,10 @@ public class SignalServiceMessageReceiver { UUID uuid, String e164, String password, String signalingKey, String signalAgent, ConnectivityListener listener, - SleepTimer timer) + SleepTimer timer, + ClientZkProfileOperations clientZkProfileOperations) { - this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), signalAgent, listener, timer); + this(urls, new StaticCredentialsProvider(uuid, e164, password, signalingKey), signalAgent, listener, timer, clientZkProfileOperations); } /** @@ -91,15 +94,16 @@ public class SignalServiceMessageReceiver { CredentialsProvider credentials, String signalAgent, ConnectivityListener listener, - SleepTimer timer) + SleepTimer timer, + ClientZkProfileOperations clientZkProfileOperations) { - this.urls = urls; - this.credentialsProvider = credentials; - this.socket = new PushServiceSocket(urls, credentials, signalAgent); - this.signalAgent = signalAgent; - this.connectivityListener = listener; - this.sleepTimer = timer; - this.clientZkProfile = FeatureFlags.ZK_GROUPS ? new ClientZkProfileOperations(new ServerPublicParams(urls.getZkGroupServerPublicParams())) : null; + this.urls = urls; + this.credentialsProvider = credentials; + this.socket = new PushServiceSocket(urls, credentials, signalAgent, clientZkProfileOperations); + this.signalAgent = signalAgent; + this.connectivityListener = listener; + this.sleepTimer = timer; + this.clientZkProfileOperations = clientZkProfileOperations; } /** @@ -123,7 +127,7 @@ public class SignalServiceMessageReceiver { Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType) - throws IOException, VerificationFailedException + throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException { Optional uuid = address.getUuid(); @@ -143,12 +147,19 @@ public class SignalServiceMessageReceiver { } public InputStream retrieveProfileAvatar(String path, File destination, ProfileKey profileKey, long maxSizeBytes) - throws IOException + throws IOException { socket.retrieveProfileAvatar(path, destination, maxSizeBytes); return new ProfileCipherInputStream(new FileInputStream(destination), profileKey); } + public FileInputStream retrieveGroupsV2ProfileAvatar(String path, File destination, long maxSizeBytes) + throws IOException + { + socket.retrieveProfileAvatar(path, destination, maxSizeBytes); + return new FileInputStream(destination); + } + /** * Retrieves a SignalServiceAttachment. * @@ -224,7 +235,7 @@ public class SignalServiceMessageReceiver { urls.getNetworkInterceptors(), urls.getDns()); - return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile); + return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations); } public SignalServiceMessagePipe createUnidentifiedMessagePipe() { @@ -235,7 +246,7 @@ public class SignalServiceMessageReceiver { urls.getNetworkInterceptors(), urls.getDns()); - return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfile); + return new SignalServiceMessagePipe(webSocket, Optional.of(credentialsProvider), clientZkProfileOperations); } public List retrieveMessages() throws IOException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 951926e815..b403c11636 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; +import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.SessionBuilder; import org.whispersystems.libsignal.SignalProtocolAddress; @@ -21,6 +22,7 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -37,8 +39,8 @@ import org.whispersystems.signalservice.api.messages.calls.OfferMessage; import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage; import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; +import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -127,9 +129,10 @@ public class SignalServiceMessageSender { boolean isMultiDevice, Optional pipe, Optional unidentifiedPipe, - Optional eventListener) + Optional eventListener, + ClientZkProfileOperations clientZkProfileOperations) { - this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener); + this(urls, new StaticCredentialsProvider(uuid, e164, password, null), store, signalAgent, isMultiDevice, pipe, unidentifiedPipe, eventListener, clientZkProfileOperations); } public SignalServiceMessageSender(SignalServiceConfiguration urls, @@ -139,9 +142,10 @@ public class SignalServiceMessageSender { boolean isMultiDevice, Optional pipe, Optional unidentifiedPipe, - Optional eventListener) + Optional eventListener, + ClientZkProfileOperations clientZkProfileOperations) { - this.socket = new PushServiceSocket(urls, credentialsProvider, signalAgent); + this.socket = new PushServiceSocket(urls, credentialsProvider, signalAgent, clientZkProfileOperations); this.store = store; this.localAddress = new SignalServiceAddress(credentialsProvider.getUuid(), credentialsProvider.getE164()); this.pipe = new AtomicReference<>(pipe); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/NoCipherOutputStream.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/NoCipherOutputStream.java new file mode 100644 index 0000000000..8346eb7e56 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/crypto/NoCipherOutputStream.java @@ -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); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ClientZkOperations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ClientZkOperations.java new file mode 100644 index 0000000000..6a618dd2c5 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/ClientZkOperations.java @@ -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. + *

+ * 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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/CredentialResponse.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/CredentialResponse.java new file mode 100644 index 0000000000..5dda8e34dc --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/CredentialResponse.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupHistoryEntry.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupHistoryEntry.java new file mode 100644 index 0000000000..a58d632574 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupHistoryEntry.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java similarity index 98% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java index 88cb3df05a..e67e030d79 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java @@ -1,4 +1,4 @@ -package org.whispersystems.signalservice.internal.groupsv2; +package org.whispersystems.signalservice.api.groupsv2; import com.google.protobuf.ByteString; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java new file mode 100644 index 0000000000..d8927a380b --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupCandidate.java @@ -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. + *

+ * The entry may or may not have a {@link ProfileKeyCredential}. + *

+ * If it does not, then this user can only be invited. + *

+ * 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; + + public GroupCandidate(UUID uuid, Optional profileKeyCredential) { + this.uuid = uuid; + this.profileKeyCredential = profileKeyCredential; + } + + public UUID getUuid() { + return uuid; + } + + public Optional getProfileKeyCredential() { + return profileKeyCredential; + } + + public boolean hasProfileKeyCredential() { + return profileKeyCredential.isPresent(); + } + + public GroupCandidate withProfileKeyCredential(Optional profileKeyCredential) { + return new GroupCandidate(uuid, profileKeyCredential); + } + + public static List toUuidList(Collection candidates) { + final List 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(); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java similarity index 99% rename from libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtil.java rename to libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java index 12d951a752..d781849ac8 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil.java @@ -1,4 +1,4 @@ -package org.whispersystems.signalservice.internal.groupsv2; +package org.whispersystems.signalservice.api.groupsv2; import com.google.protobuf.ByteString; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java new file mode 100644 index 0000000000..cda211ec91 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java @@ -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 getGroupHistory(GroupSecretParams groupSecretParams, + int fromRevision, + GroupsV2Authorization authorization) + throws IOException, InvalidGroupStateException, VerificationFailedException + { + GroupChanges group = socket.getGroupsV2GroupHistory(fromRevision, authorization.getAuthorizationForToday(groupSecretParams)); + + List changesList = group.getGroupChangesList(); + ArrayList 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); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Authorization.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Authorization.java new file mode 100644 index 0000000000..f7ed597330 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Authorization.java @@ -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 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 credentials; + + AuthorizationFactory(UUID self, + ClientZkAuthOperations clientZkAuthOperations, + Map 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 verifyCredentials(UUID self, + ClientZkAuthOperations clientZkAuthOperations, + Map credentialResponseMap) + throws VerificationFailedException + { + Map credentials = new HashMap<>(credentialResponseMap.size()); + + for (Map.Entry entry : credentialResponseMap.entrySet()) { + int redemptionTime = entry.getKey(); + AuthCredentialResponse authCredentialResponse = entry.getValue(); + + AuthCredential authCredential = clientZkAuthOperations.receiveAuthCredential(self, redemptionTime, authCredentialResponse); + + credentials.put(redemptionTime, authCredential); + } + + return credentials; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java new file mode 100644 index 0000000000..6f3734b2d1 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -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 avatar, + final GroupCandidate self, + final Set 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 title, + final Set membersToAdd, + final Set 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 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 membersList = group.getMembersList(); + List pendingMembersList = group.getPendingMembersList(); + List decryptedMembers = new ArrayList<>(membersList.size()); + List 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. + *

+ * 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 avatar; + + private NewGroup(GroupSecretParams groupSecretParams, Group newGroupMessage, Optional avatar) { + this.groupSecretParams = groupSecretParams; + this.newGroupMessage = newGroupMessage; + this.avatar = avatar; + } + + public GroupSecretParams getGroupSecretParams() { + return groupSecretParams; + } + + public Group getNewGroupMessage() { + return newGroupMessage; + } + + public Optional getAvatar() { + return avatar; + } + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/InvalidGroupStateException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/InvalidGroupStateException.java new file mode 100644 index 0000000000..c010350c40 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/InvalidGroupStateException.java @@ -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() { + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NoCredentialForRedemptionTimeException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NoCredentialForRedemptionTimeException.java new file mode 100644 index 0000000000..6786b0a217 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NoCredentialForRedemptionTimeException.java @@ -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 { +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java new file mode 100644 index 0000000000..cf84fb4863 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/TemporalCredential.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKey.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKey.java new file mode 100644 index 0000000000..04a403a1ab --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKey.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKeyCredential.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKeyCredential.java new file mode 100644 index 0000000000..61ee41aa8a --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/UuidProfileKeyCredential.java @@ -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; + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 4685ec2cea..ba4c13c6ae 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -194,7 +194,7 @@ public final class SignalServiceContent { * Takes internal protobuf serialization format and processes it into a {@link SignalServiceContent}. */ public static SignalServiceContent createFromProto(SignalServiceContentProto serviceContentProto) - throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException + throws ProtocolInvalidMessageException, ProtocolInvalidKeyException, UnsupportedDataMessageException { SignalServiceMetadata metadata = SignalServiceMetadataProtobufSerializer.fromProtobuf(serviceContentProto.getMetadata()); SignalServiceAddress localAddress = SignalServiceAddressProtobufSerializer.fromProtobuf(serviceContentProto.getLocalAddress()); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java index 6f1ca258e9..c588045b84 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupContext.java @@ -27,13 +27,13 @@ public final class SignalServiceGroupContext { } static Optional createOptional(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2) - throws InvalidMessageException + throws InvalidMessageException { return Optional.fromNullable(create(groupV1, groupV2)); } public static SignalServiceGroupContext create(SignalServiceGroup groupV1, SignalServiceGroupV2 groupV2) - throws InvalidMessageException + throws InvalidMessageException { if (groupV1 == null && groupV2 == null) { return null; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java index 1362ad4e18..6db20910dd 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceGroupV2.java @@ -49,12 +49,12 @@ public final class SignalServiceGroupV2 { this.masterKey = masterKey; } - Builder withRevision(int revision) { + public Builder withRevision(int revision) { this.revision = revision; return this; } - Builder withSignedGroupChange(byte[] signedGroupChange) { + public Builder withSignedGroupChange(byte[] signedGroupChange) { this.signedGroupChange = signedGroupChange; return this; } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ConflictException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ConflictException.java new file mode 100644 index 0000000000..8304337217 --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ConflictException.java @@ -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"); + } +} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ContactManifestMismatchException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ContactManifestMismatchException.java index a739152247..e95f806949 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ContactManifestMismatchException.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/push/exceptions/ContactManifestMismatchException.java @@ -1,6 +1,6 @@ package org.whispersystems.signalservice.api.push.exceptions; -public class ContactManifestMismatchException extends NonSuccessfulResponseCodeException { +public class ContactManifestMismatchException extends ConflictException { private final byte[] responseBody; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/UuidUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/UuidUtil.java index 430ee7d3b2..7eab553a82 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/UuidUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/util/UuidUtil.java @@ -56,4 +56,14 @@ public final class UuidUtil { public static UUID fromByteString(ByteString bytes) { return parseOrThrow(bytes.toByteArray()); } + + public static List fromByteStrings(Collection byteStringCollection) { + ArrayList result = new ArrayList<>(byteStringCollection.size()); + + for (ByteString byteString : byteStringCollection) { + result.add(fromByteString(byteString)); + } + + return result; + } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java deleted file mode 100644 index abe9cddd82..0000000000 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/groupsv2/ClientZkOperations.java +++ /dev/null @@ -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; - } -} diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 3ef5b477b1..96c1fd0b67 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -8,13 +8,20 @@ package org.whispersystems.signalservice.internal.push; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; -import org.signal.zkgroup.ServerPublicParams; +import org.signal.storageservice.protos.groups.AvatarUploadAttributes; +import org.signal.storageservice.protos.groups.Group; +import org.signal.storageservice.protos.groups.GroupChange; +import org.signal.storageservice.protos.groups.GroupChanges; import org.signal.zkgroup.VerificationFailedException; +import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest; import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext; +import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse; import org.signal.zkgroup.profiles.ProfileKeyVersion; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -26,6 +33,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.FeatureFlags; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.groupsv2.CredentialResponse; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener; import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; @@ -37,6 +45,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignedPreKeyEntity; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; +import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.ContactManifestMismatchException; import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException; import org.whispersystems.signalservice.api.push.exceptions.NoContentException; @@ -59,11 +68,11 @@ import org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResp import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest; import org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse; import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse; -import org.whispersystems.signalservice.internal.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException; import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException; import org.whispersystems.signalservice.internal.push.http.CancelationSignal; import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody; +import org.whispersystems.signalservice.internal.push.http.NoCipherOutputStreamFactory; import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory; import org.whispersystems.signalservice.internal.storage.protos.ReadOperation; import org.whispersystems.signalservice.internal.storage.protos.StorageItems; @@ -75,6 +84,7 @@ import org.whispersystems.signalservice.internal.util.JsonUtil; import org.whispersystems.signalservice.internal.util.Util; import org.whispersystems.util.Base64; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; @@ -84,8 +94,6 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Collections; @@ -100,7 +108,6 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import okhttp3.Call; @@ -164,10 +171,16 @@ public class PushServiceSocket { private static final String ATTACHMENT_DOWNLOAD_PATH = "attachments/%d"; private static final String ATTACHMENT_UPLOAD_PATH = "attachments/"; + private static final String AVATAR_UPLOAD_PATH = ""; private static final String STICKER_MANIFEST_PATH = "stickers/%s/manifest.proto"; private static final String STICKER_PATH = "stickers/%s/full/%d"; + private static final String GROUPSV2_CREDENTIAL = "/v1/certificate/group/%d/%d"; + private static final String GROUPSV2_GROUP = "/v1/groups/"; + private static final String GROUPSV2_GROUP_CHANGES = "/v1/groups/logs/%s"; + private static final String GROUPSV2_AVATAR_REQUEST = "/v1/groups/avatar/form"; + private static final Map NO_HEADERS = Collections.emptyMap(); private static final ResponseCodeHandler NO_HANDLER = new EmptyResponseCodeHandler(); @@ -180,21 +193,25 @@ public class PushServiceSocket { private final ConnectionHolder[] keyBackupServiceClients; private final ConnectionHolder[] storageClients; - private final CredentialsProvider credentialsProvider; - private final String signalAgent; - private final SecureRandom random; - private final ClientZkOperations clientZkOperations; + private final CredentialsProvider credentialsProvider; + private final String signalAgent; + private final SecureRandom random; + private final ClientZkProfileOperations clientZkProfileOperations; - public PushServiceSocket(SignalServiceConfiguration serviceConfig, CredentialsProvider credentialsProvider, String signalAgent) { - this.credentialsProvider = credentialsProvider; - this.signalAgent = signalAgent; - this.serviceClients = createServiceConnectionHolders(serviceConfig.getSignalServiceUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns()); - this.cdnClients = createConnectionHolders(serviceConfig.getSignalCdnUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns()); - this.contactDiscoveryClients = createConnectionHolders(serviceConfig.getSignalContactDiscoveryUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns()); - this.keyBackupServiceClients = createConnectionHolders(serviceConfig.getSignalKeyBackupServiceUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns()); - this.storageClients = createConnectionHolders(serviceConfig.getSignalStorageUrls(), serviceConfig.getNetworkInterceptors(), serviceConfig.getDns()); - this.random = new SecureRandom(); - this.clientZkOperations = FeatureFlags.ZK_GROUPS ? new ClientZkOperations(new ServerPublicParams(serviceConfig.getZkGroupServerPublicParams())) : null; + public PushServiceSocket(SignalServiceConfiguration configuration, + CredentialsProvider credentialsProvider, + String signalAgent, + ClientZkProfileOperations clientZkProfileOperations) + { + this.credentialsProvider = credentialsProvider; + this.signalAgent = signalAgent; + this.serviceClients = createServiceConnectionHolders(configuration.getSignalServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); + this.cdnClients = createConnectionHolders(configuration.getSignalCdnUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); + this.contactDiscoveryClients = createConnectionHolders(configuration.getSignalContactDiscoveryUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); + this.keyBackupServiceClients = createConnectionHolders(configuration.getSignalKeyBackupServiceUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); + this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns()); + this.random = new SecureRandom(); + this.clientZkProfileOperations = clientZkProfileOperations; } public void requestSmsVerificationCode(boolean androidSmsRetriever, Optional captchaToken, Optional challenge) throws IOException { @@ -561,27 +578,27 @@ public class PushServiceSocket { } public ProfileAndCredential retrieveProfile(UUID target, ProfileKey profileKey, Optional unidentifiedAccess) - throws NonSuccessfulResponseCodeException, VerificationFailedException + throws NonSuccessfulResponseCodeException, PushNetworkException, VerificationFailedException { if (!FeatureFlags.VERSIONED_PROFILES) { throw new AssertionError(); } + ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target); + ProfileKeyCredentialRequestContext requestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, target, profileKey); + ProfileKeyCredentialRequest request = requestContext.getRequest(); + + String version = profileKeyIdentifier.serialize(); + String credentialRequest = Hex.toStringCondensed(request.serialize()); + String subPath = String.format("%s/%s/%s", target, version, credentialRequest); + + String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess); + try { - ProfileKeyVersion profileKeyIdentifier = profileKey.getProfileKeyVersion(target); - ProfileKeyCredentialRequestContext requestContext = clientZkOperations.getProfileOperations().createProfileKeyCredentialRequestContext(random, target, profileKey); - ProfileKeyCredentialRequest request = requestContext.getRequest(); - - String version = profileKeyIdentifier.serialize(); - String credentialRequest = Hex.toStringCondensed(request.serialize()); - String subPath = String.format("%s/%s/%s", target, version, credentialRequest); - - String response = makeServiceRequest(String.format(PROFILE_PATH, subPath), "GET", null, NO_HEADERS, unidentifiedAccess); - SignalServiceProfile signalServiceProfile = JsonUtil.fromJson(response, SignalServiceProfile.class); ProfileKeyCredential profileKeyCredential = signalServiceProfile.getProfileKeyCredentialResponse() != null - ? clientZkOperations.getProfileOperations().receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse()) + ? clientZkProfileOperations.receiveProfileKeyCredential(requestContext, signalServiceProfile.getProfileKeyCredentialResponse()) : null; return new ProfileAndCredential(signalServiceProfile, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, Optional.fromNullable(profileKeyCredential)); @@ -623,7 +640,7 @@ public class PushServiceSocket { } if (profileAvatar != null) { - uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(), + uploadToCdn(AVATAR_UPLOAD_PATH, formAttributes.getAcl(), formAttributes.getKey(), formAttributes.getPolicy(), formAttributes.getAlgorithm(), formAttributes.getCredential(), formAttributes.getDate(), formAttributes.getSignature(), profileAvatar.getData(), @@ -640,7 +657,7 @@ public class PushServiceSocket { * @return The avatar URL path, if one was written. */ public Optional writeProfile(SignalServiceProfileWrite signalServiceProfileWrite, ProfileAvatarData profileAvatar) - throws NonSuccessfulResponseCodeException, PushNetworkException + throws NonSuccessfulResponseCodeException, PushNetworkException { if (!FeatureFlags.VERSIONED_PROFILES) { throw new AssertionError(); @@ -659,7 +676,7 @@ public class PushServiceSocket { throw new NonSuccessfulResponseCodeException("Unable to parse entity"); } - uploadToCdn("", formAttributes.getAcl(), formAttributes.getKey(), + uploadToCdn(AVATAR_UPLOAD_PATH, formAttributes.getAcl(), formAttributes.getKey(), formAttributes.getPolicy(), formAttributes.getAlgorithm(), formAttributes.getCredential(), formAttributes.getDate(), formAttributes.getSignature(), profileAvatar.getData(), @@ -727,7 +744,7 @@ public class PushServiceSocket { } public TokenResponse getKeyBackupServiceToken(String authorizationToken, String enclaveName) - throws IOException + throws IOException { ResponseBody body = makeRequest(ClientSet.KeyBackup, authorizationToken, null, "/v1/token/" + enclaveName, "GET", null).body(); @@ -793,38 +810,38 @@ public class PushServiceSocket { } public StorageManifest getStorageManifest(String authToken) throws IOException { - Response response = makeStorageRequest(authToken, "/v1/storage/manifest", "GET", null); + ResponseBody response = makeStorageRequest(authToken, "/v1/storage/manifest", "GET", null); - if (response.body() == null) { + if (response == null) { throw new IOException("Missing body!"); } - return StorageManifest.parseFrom(response.body().bytes()); + return StorageManifest.parseFrom(response.bytes()); } public StorageManifest getStorageManifestIfDifferentVersion(String authToken, long version) throws IOException { - Response response = makeStorageRequest(authToken, "/v1/storage/manifest/version/" + version, "GET", null); + ResponseBody response = makeStorageRequest(authToken, "/v1/storage/manifest/version/" + version, "GET", null); - if (response.body() == null) { + if (response == null) { throw new IOException("Missing body!"); } - return StorageManifest.parseFrom(response.body().bytes()); + return StorageManifest.parseFrom(response.bytes()); } public StorageItems readStorageItems(String authToken, ReadOperation operation) throws IOException { - Response response = makeStorageRequest(authToken, "/v1/storage/read", "PUT", operation.toByteArray()); + ResponseBody response = makeStorageRequest(authToken, "/v1/storage/read", "PUT", protobufRequestBody(operation)); - if (response.body() == null) { + if (response == null) { throw new IOException("Missing body!"); } - return StorageItems.parseFrom(response.body().bytes()); + return StorageItems.parseFrom(response.bytes()); } public Optional writeStorageContacts(String authToken, WriteOperation writeOperation) throws IOException { try { - makeStorageRequest(authToken, "/v1/storage", "PUT", writeOperation.toByteArray()); + makeStorageRequest(authToken, "/v1/storage", "PUT", protobufRequestBody(writeOperation)); return Optional.absent(); } catch (ContactManifestMismatchException e) { return Optional.of(StorageManifest.parseFrom(e.getResponseBody())); @@ -860,6 +877,19 @@ public class PushServiceSocket { } } + public byte[] uploadGroupV2Avatar(byte[] avatarCipherText, AvatarUploadAttributes uploadAttributes) + throws IOException + { + return uploadToCdn(AVATAR_UPLOAD_PATH, uploadAttributes.getAcl(), uploadAttributes.getKey(), + uploadAttributes.getPolicy(), uploadAttributes.getAlgorithm(), + uploadAttributes.getCredential(), uploadAttributes.getDate(), + uploadAttributes.getSignature(), + new ByteArrayInputStream(avatarCipherText), + "application/octet-stream", avatarCipherText.length, + new NoCipherOutputStreamFactory(), + null, null); + } + public Pair uploadAttachment(PushAttachmentData attachment, AttachmentUploadAttributes uploadAttributes) throws PushNetworkException, NonSuccessfulResponseCodeException { @@ -1016,46 +1046,66 @@ public class PushServiceSocket { } } - private String makeServiceRequest(String urlFragment, String method, String body) + private String makeServiceRequest(String urlFragment, String method, String jsonBody) throws NonSuccessfulResponseCodeException, PushNetworkException { - return makeServiceRequest(urlFragment, method, body, NO_HEADERS, NO_HANDLER, Optional.absent()); + return makeServiceRequest(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER, Optional.absent()); } - private String makeServiceRequest(String urlFragment, String method, String body, Map headers) + private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map headers) throws NonSuccessfulResponseCodeException, PushNetworkException { - return makeServiceRequest(urlFragment, method, body, headers, NO_HANDLER, Optional.absent()); + return makeServiceRequest(urlFragment, method, jsonBody, headers, NO_HANDLER, Optional.absent()); } - private String makeServiceRequest(String urlFragment, String method, String body, Map headers, ResponseCodeHandler responseCodeHandler) + private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map headers, ResponseCodeHandler responseCodeHandler) throws NonSuccessfulResponseCodeException, PushNetworkException { - return makeServiceRequest(urlFragment, method, body, headers, responseCodeHandler, Optional.absent()); + return makeServiceRequest(urlFragment, method, jsonBody, headers, responseCodeHandler, Optional.absent()); } - private String makeServiceRequest(String urlFragment, String method, String body, Map headers, Optional unidentifiedAccessKey) + private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map headers, Optional unidentifiedAccessKey) throws NonSuccessfulResponseCodeException, PushNetworkException { - return makeServiceRequest(urlFragment, method, body, headers, NO_HANDLER, unidentifiedAccessKey); + return makeServiceRequest(urlFragment, method, jsonBody, headers, NO_HANDLER, unidentifiedAccessKey); } - private String makeServiceRequest(String urlFragment, String method, String body, Map headers, ResponseCodeHandler responseCodeHandler, Optional unidentifiedAccessKey) + private String makeServiceRequest(String urlFragment, String method, String jsonBody, Map headers, ResponseCodeHandler responseCodeHandler, Optional 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 headers, + ResponseCodeHandler responseCodeHandler, + Optional unidentifiedAccessKey) throws NonSuccessfulResponseCodeException, PushNetworkException { Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey); - int responseCode; - String responseMessage; - String responseBody; - - try { - responseCode = response.code(); - responseMessage = response.message(); - responseBody = response.body().string(); - } catch (IOException ioe) { - throw new PushNetworkException(ioe); - } + int responseCode = response.code(); + String responseMessage = response.message(); + ResponseBody responseBody = response.body(); responseCodeHandler.handle(responseCode); @@ -1071,7 +1121,7 @@ public class PushServiceSocket { MismatchedDevices mismatchedDevices; try { - mismatchedDevices = JsonUtil.fromJson(responseBody, MismatchedDevices.class); + mismatchedDevices = JsonUtil.fromJson(responseBody.string(), MismatchedDevices.class); } catch (JsonProcessingException e) { Log.w(TAG, e); throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); @@ -1084,7 +1134,7 @@ public class PushServiceSocket { StaleDevices staleDevices; try { - staleDevices = JsonUtil.fromJson(responseBody, StaleDevices.class); + staleDevices = JsonUtil.fromJson(responseBody.string(), StaleDevices.class); } catch (JsonProcessingException e) { throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { @@ -1096,7 +1146,7 @@ public class PushServiceSocket { DeviceLimit deviceLimit; try { - deviceLimit = JsonUtil.fromJson(responseBody, DeviceLimit.class); + deviceLimit = JsonUtil.fromJson(responseBody.string(), DeviceLimit.class); } catch (JsonProcessingException e) { throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); } catch (IOException e) { @@ -1110,7 +1160,7 @@ public class PushServiceSocket { RegistrationLockFailure accountLockFailure; try { - accountLockFailure = JsonUtil.fromJson(responseBody, RegistrationLockFailure.class); + accountLockFailure = JsonUtil.fromJson(responseBody.string(), RegistrationLockFailure.class); } catch (JsonProcessingException e) { Log.w(TAG, e); throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage); @@ -1133,7 +1183,7 @@ public class PushServiceSocket { return responseBody; } - private Response getServiceConnection(String urlFragment, String method, String body, Map headers, Optional unidentifiedAccess) + private Response getServiceConnection(String urlFragment, String method, RequestBody body, Map headers, Optional unidentifiedAccess) throws PushNetworkException { try { @@ -1149,12 +1199,7 @@ public class PushServiceSocket { Request.Builder request = new Request.Builder(); request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment)); - - if (body != null) { - request.method(method, RequestBody.create(MediaType.parse("application/json"), body)); - } else { - request.method(method, null); - } + request.method(method, body); for (Map.Entry header : headers.entrySet()) { request.addHeader(header.getKey(), header.getValue()); @@ -1277,7 +1322,7 @@ public class PushServiceSocket { throw new NonSuccessfulResponseCodeException("Response: " + response); } - private Response makeStorageRequest(String authorization, String path, String method, byte[] body) + private ResponseBody makeStorageRequest(String authorization, String path, String method, RequestBody body) throws PushNetworkException, NonSuccessfulResponseCodeException { ConnectionHolder connectionHolder = getRandom(storageClients, random); @@ -1290,12 +1335,7 @@ public class PushServiceSocket { Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path)); Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path); - - if (body != null) { - request.method(method, RequestBody.create(MediaType.parse("application/x-protobuf"), body)); - } else { - request.method(method, null); - } + request.method(method, body); if (connectionHolder.getHostHeader().isPresent()) { request.addHeader("Host", connectionHolder.getHostHeader().get()); @@ -1317,7 +1357,7 @@ public class PushServiceSocket { response = call.execute(); if (response.isSuccessful() && response.code() != 204) { - return response; + return response.body(); } } catch (IOException e) { throw new PushNetworkException(e); @@ -1337,11 +1377,9 @@ public class PushServiceSocket { throw new NotFoundException("Not found"); case 409: if (response.body() != null) { - try { - throw new ContactManifestMismatchException(response.body().bytes()); - } catch (IOException e) { - throw new PushNetworkException(e); - } + throw new ContactManifestMismatchException(readBodyBytes(response.body())); + } else { + throw new ConflictException(); } case 429: throw new RateLimitException("Rate limit exceeded: " + response.code()); @@ -1387,6 +1425,10 @@ public class PushServiceSocket { .connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.RESTRICTED_TLS))) .dns(dns.or(Dns.SYSTEM)); + builder.sslSocketFactory(new Tls12SocketFactory(context.getSocketFactory()), (X509TrustManager)trustManagers[0]) + .connectionSpecs(url.getConnectionSpecs().or(Util.immutableList(ConnectionSpec.RESTRICTED_TLS))) + .build(); + for (Interceptor interceptor : interceptors) { builder.addInterceptor(interceptor); } @@ -1410,6 +1452,23 @@ public class PushServiceSocket { return connections[random.nextInt(connections.length)]; } + public ProfileKeyCredential parseResponse(UUID uuid, ProfileKey profileKey, ProfileKeyCredentialResponse profileKeyCredentialResponse) throws VerificationFailedException { + ProfileKeyCredentialRequestContext profileKeyCredentialRequestContext = clientZkProfileOperations.createProfileKeyCredentialRequestContext(random, uuid, profileKey); + + return clientZkProfileOperations.receiveProfileKeyCredential(profileKeyCredentialRequestContext, profileKeyCredentialResponse); + } + + /** + * Converts {@link IOException} on body byte reading to {@link PushNetworkException}. + */ + private static byte[] readBodyBytes(ResponseBody response) throws PushNetworkException { + try { + return response.bytes(); + } catch (IOException e) { + throw new PushNetworkException(e); + } + } + private static class GcmRegistrationId { @JsonProperty @@ -1508,4 +1567,87 @@ public class PushServiceSocket { } public enum ClientSet { ContactDiscovery, KeyBackup } + + public CredentialResponse retrieveGroupsV2Credentials(int today) + throws IOException + { + int todayPlus7 = today + 7; + String response = makeServiceRequest(String.format(Locale.US, GROUPSV2_CREDENTIAL, today, todayPlus7), + "GET", + null, + NO_HEADERS, + Optional.absent()); + + return JsonUtil.fromJson(response, CredentialResponse.class); + } + + public void putNewGroupsV2Group(Group group, String authorization) + throws NonSuccessfulResponseCodeException, PushNetworkException + { + makeStorageRequest(authorization, + GROUPSV2_GROUP, + "PUT", + protobufRequestBody(group)); + } + + public Group getGroupsV2Group(String authorization) + throws IOException + { + ResponseBody response = makeStorageRequest(authorization, + GROUPSV2_GROUP, + "GET", + null); + + try { + return Group.parseFrom(response.bytes()); + } catch (InvalidProtocolBufferException e) { + throw new IOException("Cannot read protobuf", e); + } + } + + public AvatarUploadAttributes getGroupsV2AvatarUploadForm(String authorization) + throws IOException + { + ResponseBody response = makeStorageRequest(authorization, + GROUPSV2_AVATAR_REQUEST, + "GET", + null); + + try { + return AvatarUploadAttributes.parseFrom(response.bytes()); + } catch (InvalidProtocolBufferException e) { + throw new IOException("Cannot read protobuf", e); + } + } + + public GroupChange patchGroupsV2Group(GroupChange.Actions groupChange, String authorization) + throws IOException + { + ResponseBody response = makeStorageRequest(authorization, + GROUPSV2_GROUP, + "PATCH", + protobufRequestBody(groupChange)); + + try { + return GroupChange.parseFrom(response.bytes()); + } catch (InvalidProtocolBufferException e) { + throw new IOException("Cannot read protobuf", e); + } + } + + public GroupChanges getGroupsV2GroupHistory(int fromVersion, String authorization) + throws IOException + { + ResponseBody response = makeStorageRequest(authorization, + String.format(Locale.US, GROUPSV2_GROUP_CHANGES, fromVersion), + "GET", + null); + + try { + return GroupChanges.parseFrom(response.bytes()); + } catch (InvalidProtocolBufferException e) { + throw new IOException("Cannot read protobuf", e); + } + } + } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/NoCipherOutputStreamFactory.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/NoCipherOutputStreamFactory.java new file mode 100644 index 0000000000..c9048900af --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/internal/push/http/NoCipherOutputStreamFactory.java @@ -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); + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.java similarity index 95% rename from libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java rename to libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.java index b17fc4ec63..a8983de977 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/DecryptedGroupUtilTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtilTest.java @@ -1,4 +1,4 @@ -package org.whispersystems.signalservice.internal.groupsv2; +package org.whispersystems.signalservice.api.groupsv2; import com.google.protobuf.ByteString; diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtilTest.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtilTest.java similarity index 99% rename from libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtilTest.java rename to libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtilTest.java index 5075fb10f9..f4300d13eb 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtilTest.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtilTest.java @@ -1,4 +1,4 @@ -package org.whispersystems.signalservice.internal.groupsv2; +package org.whispersystems.signalservice.api.groupsv2; import org.junit.Test; import org.signal.storageservice.protos.groups.GroupChange; diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtil_resolveConflict_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java similarity index 99% rename from libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtil_resolveConflict_Test.java rename to libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java index 0c2e41b525..3a4b1fb398 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/internal/groupsv2/GroupChangeUtil_resolveConflict_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupChangeUtil_resolveConflict_Test.java @@ -1,4 +1,4 @@ -package org.whispersystems.signalservice.internal.groupsv2; +package org.whispersystems.signalservice.api.groupsv2; import com.google.protobuf.ByteString;