mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-23 18:30:20 +01:00
Add internal pre-alpha support for storage service.
This commit is contained in:
@@ -24,6 +24,12 @@ import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
|
||||
import org.whispersystems.signalservice.api.push.ContactTokenDetails;
|
||||
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
|
||||
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.StreamDetails;
|
||||
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
|
||||
@@ -39,6 +45,12 @@ import org.whispersystems.signalservice.internal.push.ProfileAvatarData;
|
||||
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
|
||||
import org.whispersystems.signalservice.internal.push.RemoteAttestationUtil;
|
||||
import org.whispersystems.signalservice.internal.push.http.ProfileCipherOutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItems;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
|
||||
import org.whispersystems.signalservice.internal.util.StaticCredentialsProvider;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
import org.whispersystems.util.Base64;
|
||||
@@ -48,6 +60,7 @@ import java.security.KeyStore;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SignatureException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
@@ -386,6 +399,113 @@ public class SignalServiceAccountManager {
|
||||
}
|
||||
}
|
||||
|
||||
public long getStorageManifestVersion() throws IOException {
|
||||
try {
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
|
||||
|
||||
return storageManifest.getVersion();
|
||||
} catch (NotFoundException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<SignalStorageManifest> getStorageManifest(byte[] storageServiceKey) throws IOException, InvalidKeyException {
|
||||
try {
|
||||
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifest(authToken);
|
||||
byte[] rawRecord = cipher.decrypt(storageManifest.getValue().toByteArray());
|
||||
ManifestRecord manifestRecord = ManifestRecord.parseFrom(rawRecord);
|
||||
List<byte[]> keys = new ArrayList<>(manifestRecord.getKeysCount());
|
||||
|
||||
for (ByteString key : manifestRecord.getKeysList()) {
|
||||
keys.add(key.toByteArray());
|
||||
}
|
||||
|
||||
return Optional.of(new SignalStorageManifest(manifestRecord.getVersion(), keys));
|
||||
} catch (NotFoundException e) {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public List<SignalStorageRecord> readStorageRecords(byte[] storageServiceKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
|
||||
ReadOperation.Builder operation = ReadOperation.newBuilder();
|
||||
|
||||
for (byte[] key : storageKeys) {
|
||||
operation.addReadKey(ByteString.copyFrom(key));
|
||||
}
|
||||
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
|
||||
|
||||
SignalStorageCipher storageCipher = new SignalStorageCipher(storageServiceKey);
|
||||
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
|
||||
|
||||
for (StorageItem item : items.getItemsList()) {
|
||||
if (item.hasKey()) {
|
||||
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageCipher));
|
||||
} else {
|
||||
Log.w(TAG, "Encountered a StorageItem with no key! Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
|
||||
*/
|
||||
public Optional<SignalStorageManifest> writeStorageRecords(byte[] storageServiceKey,
|
||||
SignalStorageManifest manifest,
|
||||
List<SignalStorageRecord> inserts,
|
||||
List<byte[]> deletes)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
ManifestRecord.Builder manifestRecordBuilder = ManifestRecord.newBuilder().setVersion(manifest.getVersion());
|
||||
|
||||
for (byte[] key : manifest.getStorageKeys()) {
|
||||
manifestRecordBuilder.addKeys(ByteString.copyFrom(key));
|
||||
}
|
||||
|
||||
String authToken = this.pushServiceSocket.getStorageAuth();
|
||||
SignalStorageCipher cipher = new SignalStorageCipher(storageServiceKey);
|
||||
byte[] encryptedRecord = cipher.encrypt(manifestRecordBuilder.build().toByteArray());
|
||||
StorageManifest storageManifest = StorageManifest.newBuilder()
|
||||
.setVersion(manifest.getVersion())
|
||||
.setValue(ByteString.copyFrom(encryptedRecord))
|
||||
.build();
|
||||
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
|
||||
|
||||
for (SignalStorageRecord insert : inserts) {
|
||||
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, cipher));
|
||||
}
|
||||
|
||||
for (byte[] delete : deletes) {
|
||||
writeBuilder.addDeleteKey(ByteString.copyFrom(delete));
|
||||
}
|
||||
|
||||
Optional<StorageManifest> conflict = this.pushServiceSocket.writeStorageContacts(authToken, writeBuilder.build());
|
||||
|
||||
if (conflict.isPresent()) {
|
||||
byte[] rawManifestRecord = cipher.decrypt(conflict.get().getValue().toByteArray());
|
||||
ManifestRecord record = ManifestRecord.parseFrom(rawManifestRecord);
|
||||
List<byte[]> keys = new ArrayList<>(record.getKeysCount());
|
||||
|
||||
for (ByteString key : record.getKeysList()) {
|
||||
keys.add(key.toByteArray());
|
||||
}
|
||||
|
||||
SignalStorageManifest conflictManifest = new SignalStorageManifest(record.getVersion(), keys);
|
||||
|
||||
return Optional.of(conflictManifest);
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getNewDeviceVerificationCode() throws IOException {
|
||||
return this.pushServiceSocket.getNewDeviceVerificationCode();
|
||||
}
|
||||
@@ -495,4 +615,5 @@ public class SignalServiceAccountManager {
|
||||
return tokenMap;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public final class CryptoUtil {
|
||||
|
||||
private CryptoUtil () { }
|
||||
|
||||
public static byte[] computeHmacSha256(byte[] key, byte[] data) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
return mac.doFinal(data);
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.whispersystems.signalservice.api.messages.multidevice;
|
||||
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class KeysMessage {
|
||||
|
||||
private final Optional<byte[]> storageService;
|
||||
|
||||
public KeysMessage(Optional<byte[]> storageService) {
|
||||
this.storageService = storageService;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getStorageService() {
|
||||
return storageService;
|
||||
}
|
||||
}
|
||||
@@ -31,4 +31,8 @@ public class RequestMessage {
|
||||
public boolean isConfigurationRequest() {
|
||||
return request.getType() == Request.Type.CONFIGURATION;
|
||||
}
|
||||
|
||||
public boolean isKeysRequest() {
|
||||
return request.getType() == Request.Type.KEYS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ public class SignalServiceSyncMessage {
|
||||
private final Optional<ConfigurationMessage> configuration;
|
||||
private final Optional<List<StickerPackOperationMessage>> stickerPackOperations;
|
||||
private final Optional<FetchType> fetchType;
|
||||
private final Optional<KeysMessage> keys;
|
||||
|
||||
private SignalServiceSyncMessage(Optional<SentTranscriptMessage> sent,
|
||||
Optional<ContactsMessage> contacts,
|
||||
@@ -36,7 +37,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional<VerifiedMessage> verified,
|
||||
Optional<ConfigurationMessage> configuration,
|
||||
Optional<List<StickerPackOperationMessage>> stickerPackOperations,
|
||||
Optional<FetchType> fetchType)
|
||||
Optional<FetchType> fetchType,
|
||||
Optional<KeysMessage> keys)
|
||||
{
|
||||
this.sent = sent;
|
||||
this.contacts = contacts;
|
||||
@@ -49,6 +51,7 @@ public class SignalServiceSyncMessage {
|
||||
this.configuration = configuration;
|
||||
this.stickerPackOperations = stickerPackOperations;
|
||||
this.fetchType = fetchType;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forSentTranscript(SentTranscriptMessage sent) {
|
||||
@@ -62,7 +65,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forContacts(ContactsMessage contacts) {
|
||||
@@ -76,7 +80,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forGroups(SignalServiceAttachment groups) {
|
||||
@@ -90,7 +95,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRequest(RequestMessage request) {
|
||||
@@ -104,7 +110,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRead(List<ReadMessage> reads) {
|
||||
@@ -118,7 +125,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forViewOnceOpen(ViewOnceOpenMessage timerRead) {
|
||||
@@ -132,7 +140,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forRead(ReadMessage read) {
|
||||
@@ -149,7 +158,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forVerified(VerifiedMessage verifiedMessage) {
|
||||
@@ -163,7 +173,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.of(verifiedMessage),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forBlocked(BlockedListMessage blocked) {
|
||||
@@ -177,7 +188,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forConfiguration(ConfigurationMessage configuration) {
|
||||
@@ -191,7 +203,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.of(configuration),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forStickerPackOperations(List<StickerPackOperationMessage> stickerPackOperations) {
|
||||
@@ -205,7 +218,8 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.of(stickerPackOperations),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage forFetchLatest(FetchType fetchType) {
|
||||
@@ -219,13 +233,14 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.of(fetchType));
|
||||
Optional.of(fetchType),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage empty() {
|
||||
public static SignalServiceSyncMessage forKeys(KeysMessage keys) {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
@@ -233,7 +248,23 @@ public class SignalServiceSyncMessage {
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent());
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.of(keys));
|
||||
}
|
||||
|
||||
public static SignalServiceSyncMessage empty() {
|
||||
return new SignalServiceSyncMessage(Optional.<SentTranscriptMessage>absent(),
|
||||
Optional.<ContactsMessage>absent(),
|
||||
Optional.<SignalServiceAttachment>absent(),
|
||||
Optional.<BlockedListMessage>absent(),
|
||||
Optional.<RequestMessage>absent(),
|
||||
Optional.<List<ReadMessage>>absent(),
|
||||
Optional.<ViewOnceOpenMessage>absent(),
|
||||
Optional.<VerifiedMessage>absent(),
|
||||
Optional.<ConfigurationMessage>absent(),
|
||||
Optional.<List<StickerPackOperationMessage>>absent(),
|
||||
Optional.<FetchType>absent(),
|
||||
Optional.<KeysMessage>absent());
|
||||
}
|
||||
|
||||
public Optional<SentTranscriptMessage> getSent() {
|
||||
@@ -280,6 +311,10 @@ public class SignalServiceSyncMessage {
|
||||
return fetchType;
|
||||
}
|
||||
|
||||
public Optional<KeysMessage> getKeys() {
|
||||
return keys;
|
||||
}
|
||||
|
||||
public enum FetchType {
|
||||
LOCAL_PROFILE,
|
||||
STORAGE_MANIFEST
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.whispersystems.signalservice.api.push.exceptions;
|
||||
|
||||
public class ContactManifestMismatchException extends NonSuccessfulResponseCodeException {
|
||||
|
||||
private final byte[] responseBody;
|
||||
|
||||
public ContactManifestMismatchException(byte[] responseBody) {
|
||||
this.responseBody = responseBody;
|
||||
}
|
||||
|
||||
public byte[] getResponseBody() {
|
||||
return responseBody;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SignalContactRecord {
|
||||
|
||||
private final byte[] key;
|
||||
private final SignalServiceAddress address;
|
||||
private final Optional<String> profileName;
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final Optional<String> username;
|
||||
private final Optional<byte[]> identityKey;
|
||||
private final IdentityState identityState;
|
||||
private final boolean blocked;
|
||||
private final boolean profileSharingEnabled;
|
||||
private final Optional<String> nickname;
|
||||
private final int protoVersion;
|
||||
|
||||
private SignalContactRecord(byte[] key,
|
||||
SignalServiceAddress address,
|
||||
String profileName,
|
||||
byte[] profileKey,
|
||||
String username,
|
||||
byte[] identityKey,
|
||||
IdentityState identityState,
|
||||
boolean blocked,
|
||||
boolean profileSharingEnabled,
|
||||
String nickname,
|
||||
int protoVersion)
|
||||
{
|
||||
this.key = key;
|
||||
this.address = address;
|
||||
this.profileName = Optional.fromNullable(profileName);
|
||||
this.profileKey = Optional.fromNullable(profileKey);
|
||||
this.username = Optional.fromNullable(username);
|
||||
this.identityKey = Optional.fromNullable(identityKey);
|
||||
this.identityState = identityState != null ? identityState : IdentityState.DEFAULT;
|
||||
this.blocked = blocked;
|
||||
this.profileSharingEnabled = profileSharingEnabled;
|
||||
this.nickname = Optional.fromNullable(nickname);
|
||||
this.protoVersion = protoVersion;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public SignalServiceAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public Optional<String> getProfileName() {
|
||||
return profileName;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getProfileKey() {
|
||||
return profileKey;
|
||||
}
|
||||
|
||||
public Optional<String> getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public Optional<byte[]> getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public IdentityState getIdentityState() {
|
||||
return identityState;
|
||||
}
|
||||
|
||||
public boolean isBlocked() {
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public boolean isProfileSharingEnabled() {
|
||||
return profileSharingEnabled;
|
||||
}
|
||||
|
||||
public Optional<String> getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public int getProtoVersion() {
|
||||
return protoVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalContactRecord contact = (SignalContactRecord) o;
|
||||
return blocked == contact.blocked &&
|
||||
profileSharingEnabled == contact.profileSharingEnabled &&
|
||||
Arrays.equals(key, contact.key) &&
|
||||
Objects.equals(address, contact.address) &&
|
||||
Objects.equals(profileName, contact.profileName) &&
|
||||
Objects.equals(profileKey, contact.profileKey) &&
|
||||
Objects.equals(username, contact.username) &&
|
||||
Objects.equals(identityKey, contact.identityKey) &&
|
||||
identityState == contact.identityState &&
|
||||
Objects.equals(nickname, contact.nickname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(address, profileName, profileKey, username, identityKey, identityState, blocked, profileSharingEnabled, nickname);
|
||||
result = 31 * result + Arrays.hashCode(key);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final byte[] key;
|
||||
private final SignalServiceAddress address;
|
||||
|
||||
private String profileName;
|
||||
private byte[] profileKey;
|
||||
private String username;
|
||||
private byte[] identityKey;
|
||||
private IdentityState identityState;
|
||||
private boolean blocked;
|
||||
private boolean profileSharingEnabled;
|
||||
private String nickname;
|
||||
private int version;
|
||||
|
||||
public Builder(byte[] key, SignalServiceAddress address) {
|
||||
this.key = key;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public Builder setProfileName(String profileName) {
|
||||
this.profileName = profileName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileKey(byte[] profileKey) {
|
||||
this.profileKey= profileKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUsername(String username) {
|
||||
this.username = username;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIdentityKey(byte[] identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setIdentityState(IdentityState identityState) {
|
||||
this.identityState = identityState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBlocked(boolean blocked) {
|
||||
this.blocked = blocked;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
|
||||
this.profileSharingEnabled = profileSharingEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder setProtoVersion(int version) {
|
||||
this.version = version;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalContactRecord build() {
|
||||
return new SignalContactRecord(key,
|
||||
address,
|
||||
profileName,
|
||||
profileKey,
|
||||
username,
|
||||
identityKey,
|
||||
identityState,
|
||||
blocked,
|
||||
profileSharingEnabled,
|
||||
nickname,
|
||||
version);
|
||||
}
|
||||
}
|
||||
|
||||
public enum IdentityState {
|
||||
DEFAULT, VERIFIED, UNVERIFIED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.util.ByteUtil;
|
||||
import org.whispersystems.signalservice.api.crypto.CryptoUtil;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Encrypts and decrypts data from the storage service.
|
||||
*/
|
||||
public class SignalStorageCipher {
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
public SignalStorageCipher(byte[] storageServiceKey) {
|
||||
this.key = storageServiceKey;
|
||||
}
|
||||
|
||||
public byte[] encrypt(byte[] data) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
byte[] iv = Util.getSecretBytes(16);
|
||||
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
|
||||
byte[] ciphertext = cipher.doFinal(data);
|
||||
|
||||
return ByteUtil.combine(iv, ciphertext);
|
||||
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] data) throws InvalidKeyException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
byte[][] split = Util.split(data, 16, data.length - 16);
|
||||
byte[] iv = split[0];
|
||||
byte[] cipherText = split[1];
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (java.security.InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new InvalidKeyException(e);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SignalStorageManifest {
|
||||
private final long version;
|
||||
private final List<byte[]> storageKeys;
|
||||
|
||||
public SignalStorageManifest(long version, List<byte[]> storageKeys) {
|
||||
this.version = version;
|
||||
this.storageKeys = storageKeys;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<byte[]> getStorageKeys() {
|
||||
return storageKeys;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class SignalStorageModels {
|
||||
|
||||
public static SignalStorageRecord remoteToLocalStorageRecord(StorageItem item, SignalStorageCipher cipher) throws IOException, InvalidKeyException {
|
||||
byte[] rawRecord = cipher.decrypt(item.getValue().toByteArray());
|
||||
StorageRecord record = StorageRecord.parseFrom(rawRecord);
|
||||
byte[] storageKey = item.getKey().toByteArray();
|
||||
|
||||
if (record.getType() == StorageRecord.Type.CONTACT_VALUE && record.hasContact()) {
|
||||
return SignalStorageRecord.forContact(storageKey, remoteToLocalContactRecord(storageKey, record.getContact()));
|
||||
} else {
|
||||
return SignalStorageRecord.forUnknown(storageKey, record.getType());
|
||||
}
|
||||
}
|
||||
|
||||
public static StorageItem localToRemoteStorageRecord(SignalStorageRecord record, SignalStorageCipher cipher) throws IOException {
|
||||
StorageRecord.Builder builder = StorageRecord.newBuilder();
|
||||
|
||||
if (record.getContact().isPresent()) {
|
||||
builder.setContact(localToRemoteContactRecord(record.getContact().get()));
|
||||
} else {
|
||||
throw new InvalidStorageWriteError();
|
||||
}
|
||||
|
||||
builder.setType(record.getType());
|
||||
|
||||
StorageRecord remoteRecord = builder.build();
|
||||
byte[] encryptedRecord = cipher.encrypt(remoteRecord.toByteArray());
|
||||
|
||||
return StorageItem.newBuilder()
|
||||
.setKey(ByteString.copyFrom(record.getKey()))
|
||||
.setValue(ByteString.copyFrom(encryptedRecord))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static SignalContactRecord remoteToLocalContactRecord(byte[] storageKey, ContactRecord contact) throws IOException {
|
||||
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
|
||||
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
|
||||
|
||||
if (contact.hasBlocked()) {
|
||||
builder.setBlocked(contact.getBlocked());
|
||||
}
|
||||
|
||||
if (contact.hasWhitelisted()) {
|
||||
builder.setProfileSharingEnabled(contact.getWhitelisted());
|
||||
}
|
||||
|
||||
if (contact.hasNickname()) {
|
||||
builder.setNickname(contact.getNickname());
|
||||
}
|
||||
|
||||
if (contact.hasProfile()) {
|
||||
if (contact.getProfile().hasKey()) {
|
||||
builder.setProfileKey(contact.getProfile().getKey().toByteArray());
|
||||
}
|
||||
|
||||
if (contact.getProfile().hasName()) {
|
||||
builder.setProfileName(contact.getProfile().getName());
|
||||
}
|
||||
|
||||
if (contact.getProfile().hasUsername()) {
|
||||
builder.setUsername(contact.getProfile().getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.hasIdentity()) {
|
||||
if (contact.getIdentity().hasKey()) {
|
||||
builder.setIdentityKey(contact.getIdentity().getKey().toByteArray());
|
||||
}
|
||||
|
||||
if (contact.getIdentity().hasState()) {
|
||||
switch (contact.getIdentity().getState()) {
|
||||
case VERIFIED: builder.setIdentityState(SignalContactRecord.IdentityState.VERIFIED);
|
||||
case UNVERIFIED: builder.setIdentityState(SignalContactRecord.IdentityState.UNVERIFIED);
|
||||
default: builder.setIdentityState(SignalContactRecord.IdentityState.VERIFIED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static ContactRecord localToRemoteContactRecord(SignalContactRecord contact) {
|
||||
ContactRecord.Builder contactRecordBuilder = ContactRecord.newBuilder()
|
||||
.setBlocked(contact.isBlocked())
|
||||
.setWhitelisted(contact.isProfileSharingEnabled());
|
||||
if (contact.getAddress().getNumber().isPresent()) {
|
||||
contactRecordBuilder.setServiceE164(contact.getAddress().getNumber().get());
|
||||
}
|
||||
|
||||
if (contact.getAddress().getUuid().isPresent()) {
|
||||
contactRecordBuilder.setServiceUuid(contact.getAddress().getUuid().get().toString());
|
||||
}
|
||||
|
||||
if (contact.getNickname().isPresent()) {
|
||||
contactRecordBuilder.setNickname(contact.getNickname().get());
|
||||
}
|
||||
|
||||
ContactRecord.Identity.Builder identityBuilder = ContactRecord.Identity.newBuilder();
|
||||
|
||||
switch (contact.getIdentityState()) {
|
||||
case VERIFIED: identityBuilder.setState(ContactRecord.Identity.State.VERIFIED);
|
||||
case UNVERIFIED: identityBuilder.setState(ContactRecord.Identity.State.UNVERIFIED);
|
||||
case DEFAULT: identityBuilder.setState(ContactRecord.Identity.State.DEFAULT);
|
||||
}
|
||||
|
||||
if (contact.getIdentityKey().isPresent()) {
|
||||
identityBuilder.setKey(ByteString.copyFrom(contact.getIdentityKey().get()));
|
||||
}
|
||||
|
||||
contactRecordBuilder.setIdentity(identityBuilder.build());
|
||||
|
||||
ContactRecord.Profile.Builder profileBuilder = ContactRecord.Profile.newBuilder();
|
||||
|
||||
if (contact.getProfileKey().isPresent()) {
|
||||
profileBuilder.setKey(ByteString.copyFrom(contact.getProfileKey().get()));
|
||||
}
|
||||
|
||||
if (contact.getProfileName().isPresent()) {
|
||||
profileBuilder.setName(contact.getProfileName().get());
|
||||
}
|
||||
|
||||
if (contact.getUsername().isPresent()) {
|
||||
profileBuilder.setUsername(contact.getUsername().get());
|
||||
}
|
||||
|
||||
contactRecordBuilder.setProfile(profileBuilder.build());
|
||||
|
||||
return contactRecordBuilder.build();
|
||||
}
|
||||
|
||||
private static class InvalidStorageWriteError extends Error {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SignalStorageRecord {
|
||||
|
||||
private final byte[] key;
|
||||
private final int type;
|
||||
private final Optional<SignalContactRecord> contact;
|
||||
|
||||
public static SignalStorageRecord forContact(byte[] key, SignalContactRecord contact) {
|
||||
return new SignalStorageRecord(key, StorageRecord.Type.CONTACT_VALUE, Optional.of(contact));
|
||||
}
|
||||
|
||||
public static SignalStorageRecord forUnknown(byte[] key, int type) {
|
||||
return new SignalStorageRecord(key, type, Optional.<SignalContactRecord>absent());
|
||||
}
|
||||
|
||||
private SignalStorageRecord(byte key[], int type, Optional<SignalContactRecord> contact) {
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Optional<SignalContactRecord> getContact() {
|
||||
return contact;
|
||||
}
|
||||
|
||||
public boolean isUnknown() {
|
||||
return !contact.isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
SignalStorageRecord record = (SignalStorageRecord) o;
|
||||
return type == record.type &&
|
||||
Arrays.equals(key, record.key) &&
|
||||
contact.equals(record.contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = Objects.hash(type, contact);
|
||||
result = 31 * result + Arrays.hashCode(key);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import org.whispersystems.signalservice.api.crypto.CryptoUtil;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class SignalStorageUtil {
|
||||
|
||||
public static byte[] computeStorageServiceKey(byte[] kbsMasterKey) {
|
||||
return CryptoUtil.computeHmacSha256(kbsMasterKey, "Storage Service Encryption".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class StorageAuthResponse {
|
||||
|
||||
@JsonProperty
|
||||
private String username;
|
||||
|
||||
@JsonProperty
|
||||
private String password;
|
||||
|
||||
public StorageAuthResponse() { }
|
||||
|
||||
public StorageAuthResponse(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,19 @@ public class SignalServiceConfiguration {
|
||||
private final SignalCdnUrl[] signalCdnUrls;
|
||||
private final SignalContactDiscoveryUrl[] signalContactDiscoveryUrls;
|
||||
private final SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls;
|
||||
private final SignalStorageUrl[] signalStorageUrls;
|
||||
|
||||
public SignalServiceConfiguration(SignalServiceUrl[] signalServiceUrls,
|
||||
SignalCdnUrl[] signalCdnUrls,
|
||||
SignalContactDiscoveryUrl[] signalContactDiscoveryUrls,
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls) {
|
||||
SignalKeyBackupServiceUrl[] signalKeyBackupServiceUrls,
|
||||
SignalStorageUrl[] signalStorageUrls)
|
||||
{
|
||||
this.signalServiceUrls = signalServiceUrls;
|
||||
this.signalCdnUrls = signalCdnUrls;
|
||||
this.signalContactDiscoveryUrls = signalContactDiscoveryUrls;
|
||||
this.signalKeyBackupServiceUrls = signalKeyBackupServiceUrls;
|
||||
this.signalStorageUrls = signalStorageUrls;
|
||||
}
|
||||
|
||||
public SignalServiceUrl[] getSignalServiceUrls() {
|
||||
@@ -33,4 +37,8 @@ public class SignalServiceConfiguration {
|
||||
public SignalKeyBackupServiceUrl[] getSignalKeyBackupServiceUrls() {
|
||||
return signalKeyBackupServiceUrls;
|
||||
}
|
||||
|
||||
public SignalStorageUrl[] getSignalStorageUrls() {
|
||||
return signalStorageUrls;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.whispersystems.signalservice.internal.configuration;
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.api.push.TrustStore;
|
||||
|
||||
import okhttp3.ConnectionSpec;
|
||||
|
||||
public class SignalStorageUrl extends SignalUrl {
|
||||
|
||||
public SignalStorageUrl(String url, TrustStore trustStore) {
|
||||
super(url, trustStore);
|
||||
}
|
||||
|
||||
public SignalStorageUrl(String url, String hostHeader, TrustStore trustStore, ConnectionSpec connectionSpec) {
|
||||
super(url, hostHeader, trustStore, connectionSpec);
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,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.ContactManifestMismatchException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ExpectationFailedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
@@ -36,6 +37,7 @@ import org.whispersystems.signalservice.api.push.exceptions.RemoteAttestationRes
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
|
||||
import org.whispersystems.signalservice.api.storage.StorageAuthResponse;
|
||||
import org.whispersystems.signalservice.api.util.CredentialsProvider;
|
||||
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
@@ -50,6 +52,10 @@ import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevic
|
||||
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
|
||||
import org.whispersystems.signalservice.internal.push.http.DigestingRequestBody;
|
||||
import org.whispersystems.signalservice.internal.push.http.OutputStreamFactory;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.ReadOperation;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageItems;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
|
||||
import org.whispersystems.signalservice.internal.storage.protos.WriteOperation;
|
||||
import org.whispersystems.signalservice.internal.util.BlacklistingTrustManager;
|
||||
import org.whispersystems.signalservice.internal.util.Hex;
|
||||
import org.whispersystems.signalservice.internal.util.JsonUtil;
|
||||
@@ -79,15 +85,14 @@ import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.Call;
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.Credentials;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
@@ -158,6 +163,7 @@ public class PushServiceSocket {
|
||||
private final ConnectionHolder[] cdnClients;
|
||||
private final ConnectionHolder[] contactDiscoveryClients;
|
||||
private final ConnectionHolder[] keyBackupServiceClients;
|
||||
private final ConnectionHolder[] storageClients;
|
||||
private final OkHttpClient attachmentClient;
|
||||
|
||||
private final CredentialsProvider credentialsProvider;
|
||||
@@ -171,6 +177,7 @@ public class PushServiceSocket {
|
||||
this.cdnClients = createConnectionHolders(signalServiceConfiguration.getSignalCdnUrls());
|
||||
this.contactDiscoveryClients = createConnectionHolders(signalServiceConfiguration.getSignalContactDiscoveryUrls());
|
||||
this.keyBackupServiceClients = createConnectionHolders(signalServiceConfiguration.getSignalKeyBackupServiceUrls());
|
||||
this.storageClients = createConnectionHolders(signalServiceConfiguration.getSignalStorageUrls());
|
||||
this.attachmentClient = createAttachmentClient();
|
||||
this.random = new SecureRandom();
|
||||
}
|
||||
@@ -203,7 +210,7 @@ public class PushServiceSocket {
|
||||
} else if (challenge.isPresent()) {
|
||||
path += "?challenge=" + challenge.get();
|
||||
}
|
||||
|
||||
|
||||
makeServiceRequest(path, "GET", null, headers, new ResponseCodeHandler() {
|
||||
@Override
|
||||
public void handle(int responseCode) throws NonSuccessfulResponseCodeException {
|
||||
@@ -426,8 +433,7 @@ public class PushServiceSocket {
|
||||
|
||||
public PreKeyBundle getPreKey(SignalServiceAddress destination, int deviceId) throws IOException {
|
||||
try {
|
||||
String path = String.format(PREKEY_DEVICE_PATH, destination.getIdentifier(),
|
||||
String.valueOf(deviceId));
|
||||
String path = String.format(PREKEY_DEVICE_PATH, destination.getIdentifier(), String.valueOf(deviceId));
|
||||
|
||||
if (destination.getRelay().isPresent()) {
|
||||
path = path + "?relay=" + destination.getRelay().get();
|
||||
@@ -543,7 +549,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
public void retrieveProfileAvatar(String path, File destination, int maxSizeBytes)
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
throws NonSuccessfulResponseCodeException, PushNetworkException
|
||||
{
|
||||
downloadFromCdn(destination, path, maxSizeBytes, null);
|
||||
}
|
||||
@@ -688,6 +694,42 @@ public class PushServiceSocket {
|
||||
return JsonUtil.fromJson(response, TurnServerInfo.class);
|
||||
}
|
||||
|
||||
public String getStorageAuth() throws IOException {
|
||||
String response = makeServiceRequest("/v1/storage/auth", "GET", null);
|
||||
StorageAuthResponse authResponse = JsonUtil.fromJson(response, StorageAuthResponse.class);
|
||||
|
||||
return Credentials.basic(authResponse.getUsername(), authResponse.getPassword());
|
||||
}
|
||||
|
||||
public StorageManifest getStorageManifest(String authToken) throws IOException {
|
||||
Response response = makeStorageRequest(authToken, "/v1/storage/manifest", "GET", null);
|
||||
|
||||
if (response.body() == null) {
|
||||
throw new IOException("Missing body!");
|
||||
}
|
||||
|
||||
return StorageManifest.parseFrom(response.body().bytes());
|
||||
}
|
||||
|
||||
public StorageItems readStorageItems(String authToken, ReadOperation operation) throws IOException {
|
||||
Response response = makeStorageRequest(authToken, "/v1/storage/read", "PUT", operation.toByteArray());
|
||||
|
||||
if (response.body() == null) {
|
||||
throw new IOException("Missing body!");
|
||||
}
|
||||
|
||||
return StorageItems.parseFrom(response.body().bytes());
|
||||
}
|
||||
|
||||
public Optional<StorageManifest> writeStorageContacts(String authToken, WriteOperation writeOperation) throws IOException {
|
||||
try {
|
||||
makeStorageRequest(authToken, "/v1/storage", "PUT", writeOperation.toByteArray());
|
||||
return Optional.absent();
|
||||
} catch (ContactManifestMismatchException e) {
|
||||
return Optional.of(StorageManifest.parseFrom(e.getResponseBody()));
|
||||
}
|
||||
}
|
||||
|
||||
public void setSoTimeoutMillis(long soTimeoutMillis) {
|
||||
this.soTimeoutMillis = soTimeoutMillis;
|
||||
}
|
||||
@@ -812,17 +854,17 @@ public class PushServiceSocket {
|
||||
DigestingRequestBody file = new DigestingRequestBody(data, outputStreamFactory, contentType, length, progressListener);
|
||||
|
||||
RequestBody requestBody = new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("acl", acl)
|
||||
.addFormDataPart("key", key)
|
||||
.addFormDataPart("policy", policy)
|
||||
.addFormDataPart("Content-Type", contentType)
|
||||
.addFormDataPart("x-amz-algorithm", algorithm)
|
||||
.addFormDataPart("x-amz-credential", credential)
|
||||
.addFormDataPart("x-amz-date", date)
|
||||
.addFormDataPart("x-amz-signature", signature)
|
||||
.addFormDataPart("file", "file", file)
|
||||
.build();
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("acl", acl)
|
||||
.addFormDataPart("key", key)
|
||||
.addFormDataPart("policy", policy)
|
||||
.addFormDataPart("Content-Type", contentType)
|
||||
.addFormDataPart("x-amz-algorithm", algorithm)
|
||||
.addFormDataPart("x-amz-credential", credential)
|
||||
.addFormDataPart("x-amz-date", date)
|
||||
.addFormDataPart("x-amz-signature", signature)
|
||||
.addFormDataPart("file", "file", file)
|
||||
.build();
|
||||
|
||||
Request.Builder request = new Request.Builder()
|
||||
.url(connectionHolder.getUrl() + "/" + path)
|
||||
@@ -967,8 +1009,7 @@ public class PushServiceSocket {
|
||||
}
|
||||
|
||||
if (responseCode != 200 && responseCode != 204) {
|
||||
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " +
|
||||
responseMessage);
|
||||
throw new NonSuccessfulResponseCodeException("Bad response: " + responseCode + " " + responseMessage);
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
@@ -1118,6 +1159,75 @@ public class PushServiceSocket {
|
||||
throw new NonSuccessfulResponseCodeException("Response: " + response);
|
||||
}
|
||||
|
||||
private Response makeStorageRequest(String authorization, String path, String method, byte[] body)
|
||||
throws PushNetworkException, NonSuccessfulResponseCodeException
|
||||
{
|
||||
ConnectionHolder connectionHolder = getRandom(storageClients, random);
|
||||
OkHttpClient okHttpClient = connectionHolder.getClient()
|
||||
.newBuilder()
|
||||
.connectTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (connectionHolder.getHostHeader().isPresent()) {
|
||||
request.addHeader("Host", connectionHolder.getHostHeader().get());
|
||||
}
|
||||
|
||||
if (authorization != null) {
|
||||
request.addHeader("Authorization", authorization);
|
||||
}
|
||||
|
||||
Call call = okHttpClient.newCall(request.build());
|
||||
|
||||
synchronized (connections) {
|
||||
connections.add(call);
|
||||
}
|
||||
|
||||
Response response;
|
||||
|
||||
try {
|
||||
response = call.execute();
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
return response;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new PushNetworkException(e);
|
||||
} finally {
|
||||
synchronized (connections) {
|
||||
connections.remove(call);
|
||||
}
|
||||
}
|
||||
|
||||
switch (response.code()) {
|
||||
case 401:
|
||||
case 403:
|
||||
throw new AuthorizationFailedException("Authorization failed!");
|
||||
case 404:
|
||||
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);
|
||||
}
|
||||
}
|
||||
case 429:
|
||||
throw new RateLimitException("Rate limit exceeded: " + response.code());
|
||||
}
|
||||
|
||||
throw new NonSuccessfulResponseCodeException("Response: " + response);
|
||||
}
|
||||
|
||||
private ServiceConnectionHolder[] createServiceConnectionHolders(SignalUrl[] urls) {
|
||||
List<ServiceConnectionHolder> serviceConnectionHolders = new LinkedList<>();
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ public class Util {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String readFully(InputStream in) throws IOException {
|
||||
public static byte[] readFullyAsBytes(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
@@ -93,7 +93,11 @@ public class Util {
|
||||
|
||||
in.close();
|
||||
|
||||
return new String(bout.toByteArray());
|
||||
return bout.toByteArray();
|
||||
}
|
||||
|
||||
public static String readFully(InputStream in) throws IOException {
|
||||
return new String(readFullyAsBytes(in));
|
||||
}
|
||||
|
||||
public static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user