Add internal pre-alpha support for storage service.

This commit is contained in:
Greyson Parrelli
2019-09-26 10:12:51 -04:00
parent 52447f5e97
commit cc0ced9a81
43 changed files with 3238 additions and 163 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 {
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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<>();

View File

@@ -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 {