Update the storage service.

This commit is contained in:
Greyson Parrelli
2020-02-10 13:42:43 -05:00
parent 133bd44b85
commit 6184e5f828
44 changed files with 1592 additions and 431 deletions

View File

@@ -17,11 +17,14 @@ import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.FeatureFlags;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.ProfileCipherOutputStream;
import org.whispersystems.signalservice.api.push.exceptions.NoContentException;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite;
@@ -32,6 +35,7 @@ import org.whispersystems.signalservice.api.storage.SignalStorageCipher;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageModels;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageManifestKey;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.StreamDetails;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
@@ -66,6 +70,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
@@ -404,41 +409,48 @@ public class SignalServiceAccountManager {
}
}
public Optional<SignalStorageManifest> getStorageManifest(byte[] storageServiceKey) throws IOException, InvalidKeyException {
public Optional<SignalStorageManifest> getStorageManifestIfDifferentVersion(StorageKey storageKey, long manifestVersion) 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());
String authToken = this.pushServiceSocket.getStorageAuth();
StorageManifest storageManifest = this.pushServiceSocket.getStorageManifestIfDifferentVersion(authToken, manifestVersion);
if (storageManifest.getValue().isEmpty()) {
Log.w(TAG, "Got an empty storage manifest!");
return Optional.absent();
}
byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveManifestKey(storageManifest.getVersion()), 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) {
} catch (NoContentException e) {
return Optional.absent();
}
}
public List<SignalStorageRecord> readStorageRecords(byte[] storageServiceKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
public List<SignalStorageRecord> readStorageRecords(StorageKey storageKey, 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());
String authToken = this.pushServiceSocket.getStorageAuth();
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
SignalStorageCipher storageCipher = new SignalStorageCipher(storageServiceKey);
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
if (items.getItemsCount() != storageKeys.size()) {
Log.w(TAG, "Failed to find all remote keys! Requested: " + storageKeys.size() + ", Found: " + items.getItemsCount());
}
for (StorageItem item : items.getItemsList()) {
if (item.hasKey()) {
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageCipher));
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, storageKey));
} else {
Log.w(TAG, "Encountered a StorageItem with no key! Skipping.");
}
@@ -446,15 +458,38 @@ public class SignalServiceAccountManager {
return result;
}
/**
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
*/
public Optional<SignalStorageManifest> resetStorageRecords(StorageKey storageKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> allRecords)
throws IOException, InvalidKeyException
{
return writeStorageRecords(storageKey, manifest, allRecords, Collections.<byte[]>emptyList(), true);
}
/**
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
*/
public Optional<SignalStorageManifest> writeStorageRecords(byte[] storageServiceKey,
public Optional<SignalStorageManifest> writeStorageRecords(StorageKey storageKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> inserts,
List<byte[]> deletes)
throws IOException, InvalidKeyException
{
return writeStorageRecords(storageKey, manifest, inserts, deletes, false);
}
/**
* @return If there was a conflict, the latest {@link SignalStorageManifest}. Otherwise absent.
*/
private Optional<SignalStorageManifest> writeStorageRecords(StorageKey storageKey,
SignalStorageManifest manifest,
List<SignalStorageRecord> inserts,
List<byte[]> deletes,
boolean clearAll)
throws IOException, InvalidKeyException
{
ManifestRecord.Builder manifestRecordBuilder = ManifestRecord.newBuilder().setVersion(manifest.getVersion());
@@ -462,29 +497,34 @@ public class SignalServiceAccountManager {
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()
String authToken = this.pushServiceSocket.getStorageAuth();
StorageManifestKey manifestKey = storageKey.deriveManifestKey(manifest.getVersion());
byte[] encryptedRecord = SignalStorageCipher.encrypt(manifestKey, manifestRecordBuilder.build().toByteArray());
StorageManifest storageManifest = StorageManifest.newBuilder()
.setVersion(manifest.getVersion())
.setValue(ByteString.copyFrom(encryptedRecord))
.build();
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
WriteOperation.Builder writeBuilder = WriteOperation.newBuilder().setManifest(storageManifest);
for (SignalStorageRecord insert : inserts) {
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, cipher));
writeBuilder.addInsertItem(SignalStorageModels.localToRemoteStorageRecord(insert, storageKey));
}
for (byte[] delete : deletes) {
writeBuilder.addDeleteKey(ByteString.copyFrom(delete));
if (clearAll) {
writeBuilder.setClearAll(true);
} else {
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());
StorageManifestKey conflictKey = storageKey.deriveManifestKey(conflict.get().getVersion());
byte[] rawManifestRecord = SignalStorageCipher.decrypt(conflictKey, 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());

View File

@@ -36,6 +36,7 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess
import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage;
import org.whispersystems.signalservice.api.messages.multidevice.MessageRequestResponseMessage;
import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage;
import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
@@ -305,6 +306,8 @@ public class SignalServiceMessageSender {
content = createMultiDeviceFetchTypeContent(message.getFetchType().get());
} else if (message.getMessageRequestResponse().isPresent()) {
content = createMultiDeviceMessageRequestResponseContent(message.getMessageRequestResponse().get());
} else if (message.getKeys().isPresent()) {
content = createMultiDeviceSyncKeysContent(message.getKeys().get());
} else if (message.getVerified().isPresent()) {
sendMessage(message.getVerified().get(), unidentifiedAccess);
return;
@@ -822,8 +825,8 @@ public class SignalServiceMessageSender {
}
private byte[] createMultiDeviceMessageRequestResponseContent(MessageRequestResponseMessage message) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.MessageRequestResponse.Builder responseMessage = SyncMessage.MessageRequestResponse.newBuilder();
if (message.getGroupId().isPresent()) {
@@ -863,6 +866,20 @@ public class SignalServiceMessageSender {
return container.setSyncMessage(syncMessage).build().toByteArray();
}
private byte[] createMultiDeviceSyncKeysContent(KeysMessage keysMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();
SyncMessage.Keys.Builder builder = SyncMessage.Keys.newBuilder();
if (keysMessage.getStorageService().isPresent()) {
builder.setStorageService(ByteString.copyFrom(keysMessage.getStorageService().get().serialize()));
} else {
Log.w(TAG, "Invalid keys message!");
}
return container.setSyncMessage(syncMessage.setKeys(builder)).build().toByteArray();
}
private byte[] createMultiDeviceVerifiedContent(VerifiedMessage verifiedMessage, byte[] nullMessage) {
Content.Builder container = Content.newBuilder();
SyncMessage.Builder syncMessage = createSyncMessageBuilder();

View File

@@ -1,5 +1,6 @@
package org.whispersystems.signalservice.api.kbs;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.StringUtil;
@@ -30,8 +31,8 @@ public final class MasterKey {
return Hex.toStringCondensed(derive("Registration Lock"));
}
public byte[] deriveStorageServiceKey() {
return derive("Storage Service Encryption");
public StorageKey deriveStorageServiceKey() {
return new StorageKey(derive("Storage Service Encryption"));
}
private byte[] derive(String keyName) {

View File

@@ -2,16 +2,17 @@ package org.whispersystems.signalservice.api.messages.multidevice;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.storage.StorageKey;
public class KeysMessage {
private final Optional<byte[]> storageService;
private final Optional<StorageKey> storageService;
public KeysMessage(Optional<byte[]> storageService) {
public KeysMessage(Optional<StorageKey> storageService) {
this.storageService = storageService;
}
public Optional<byte[]> getStorageService() {
public Optional<StorageKey> getStorageService() {
return storageService;
}
}

View File

@@ -0,0 +1,13 @@
/**
* Copyright (C) 2014-2016 Open Whisper Systems
*
* Licensed according to the LICENSE file in this repository.
*/
package org.whispersystems.signalservice.api.push.exceptions;
public class NoContentException extends NonSuccessfulResponseCodeException {
public NoContentException(String s) {
super(s);
}
}

View File

@@ -7,11 +7,12 @@ import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Arrays;
import java.util.Objects;
public final class SignalContactRecord {
public final class SignalContactRecord implements SignalRecord {
private final byte[] key;
private final SignalServiceAddress address;
private final Optional<String> profileName;
private final Optional<String> givenName;
private final Optional<String> familyName;
private final Optional<byte[]> profileKey;
private final Optional<String> username;
private final Optional<byte[]> identityKey;
@@ -23,7 +24,8 @@ public final class SignalContactRecord {
private SignalContactRecord(byte[] key,
SignalServiceAddress address,
String profileName,
String givenName,
String familyName,
byte[] profileKey,
String username,
byte[] identityKey,
@@ -35,7 +37,8 @@ public final class SignalContactRecord {
{
this.key = key;
this.address = address;
this.profileName = Optional.fromNullable(profileName);
this.givenName = Optional.fromNullable(givenName);
this.familyName = Optional.fromNullable(familyName);
this.profileKey = Optional.fromNullable(profileKey);
this.username = Optional.fromNullable(username);
this.identityKey = Optional.fromNullable(identityKey);
@@ -46,6 +49,7 @@ public final class SignalContactRecord {
this.protoVersion = protoVersion;
}
@Override
public byte[] getKey() {
return key;
}
@@ -54,8 +58,12 @@ public final class SignalContactRecord {
return address;
}
public Optional<String> getProfileName() {
return profileName;
public Optional<String> getGivenName() {
return givenName;
}
public Optional<String> getFamilyName() {
return familyName;
}
public Optional<byte[]> getProfileKey() {
@@ -99,7 +107,8 @@ public final class SignalContactRecord {
profileSharingEnabled == contact.profileSharingEnabled &&
Arrays.equals(key, contact.key) &&
Objects.equals(address, contact.address) &&
profileName.equals(contact.profileName) &&
givenName.equals(contact.givenName) &&
familyName.equals(contact.familyName) &&
OptionalUtil.byteArrayEquals(profileKey, contact.profileKey) &&
username.equals(contact.username) &&
OptionalUtil.byteArrayEquals(identityKey, contact.identityKey) &&
@@ -109,7 +118,7 @@ public final class SignalContactRecord {
@Override
public int hashCode() {
int result = Objects.hash(address, profileName, username, identityState, blocked, profileSharingEnabled, nickname);
int result = Objects.hash(address, givenName, familyName, username, identityState, blocked, profileSharingEnabled, nickname);
result = 31 * result + Arrays.hashCode(key);
result = 31 * result + OptionalUtil.byteArrayHashCode(profileKey);
result = 31 * result + OptionalUtil.byteArrayHashCode(identityKey);
@@ -120,7 +129,8 @@ public final class SignalContactRecord {
private final byte[] key;
private final SignalServiceAddress address;
private String profileName;
private String givenName;
private String familyName;
private byte[] profileKey;
private String username;
private byte[] identityKey;
@@ -135,8 +145,13 @@ public final class SignalContactRecord {
this.address = address;
}
public Builder setProfileName(String profileName) {
this.profileName = profileName;
public Builder setGivenName(String givenName) {
this.givenName = givenName;
return this;
}
public Builder setFamilyName(String familyName) {
this.familyName = familyName;
return this;
}
@@ -183,7 +198,8 @@ public final class SignalContactRecord {
public SignalContactRecord build() {
return new SignalContactRecord(key,
address,
profileName,
givenName,
familyName,
profileKey,
username,
identityKey,

View File

@@ -0,0 +1,85 @@
package org.whispersystems.signalservice.api.storage;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Arrays;
import java.util.Objects;
public final class SignalGroupV1Record implements SignalRecord {
private final byte[] key;
private final byte[] groupId;
private final boolean blocked;
private final boolean profileSharingEnabled;
private SignalGroupV1Record(byte[] key, byte[] groupId, boolean blocked, boolean profileSharingEnabled) {
this.key = key;
this.groupId = groupId;
this.blocked = blocked;
this.profileSharingEnabled = profileSharingEnabled;
}
@Override
public byte[] getKey() {
return key;
}
public byte[] getGroupId() {
return groupId;
}
public boolean isBlocked() {
return blocked;
}
public boolean isProfileSharingEnabled() {
return profileSharingEnabled;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalGroupV1Record that = (SignalGroupV1Record) o;
return blocked == that.blocked &&
profileSharingEnabled == that.profileSharingEnabled &&
Arrays.equals(key, that.key) &&
Arrays.equals(groupId, that.groupId);
}
@Override
public int hashCode() {
int result = Objects.hash(blocked, profileSharingEnabled);
result = 31 * result + Arrays.hashCode(key);
result = 31 * result + Arrays.hashCode(groupId);
return result;
}
public static final class Builder {
private final byte[] key;
private final byte[] groupId;
private boolean blocked;
private boolean profileSharingEnabled;
public Builder(byte[] key, byte[] groupId) {
this.key = key;
this.groupId = groupId;
}
public Builder setBlocked(boolean blocked) {
this.blocked = blocked;
return this;
}
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
this.profileSharingEnabled = profileSharingEnabled;
return this;
}
public SignalGroupV1Record build() {
return new SignalGroupV1Record(key, groupId, blocked, profileSharingEnabled);
}
}
}

View File

@@ -0,0 +1,5 @@
package org.whispersystems.signalservice.api.storage;
public interface SignalRecord {
byte[] getKey();
}

View File

@@ -1,22 +1,13 @@
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;
@@ -27,34 +18,28 @@ import javax.crypto.spec.SecretKeySpec;
*/
public class SignalStorageCipher {
private final byte[] key;
public SignalStorageCipher(byte[] storageServiceKey) {
this.key = storageServiceKey;
}
public byte[] encrypt(byte[] data) {
public static byte[] encrypt(StorageCipherKey key, 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));
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, iv));
byte[] ciphertext = cipher.doFinal(data);
return ByteUtil.combine(iv, ciphertext);
return Util.join(iv, ciphertext);
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException e) {
throw new AssertionError(e);
}
}
public byte[] decrypt(byte[] data) throws InvalidKeyException {
public static byte[] decrypt(StorageCipherKey key, 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));
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, iv));
return cipher.doFinal(cipherText);
} catch (java.security.InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
throw new InvalidKeyException(e);

View File

@@ -6,6 +6,7 @@ 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.GroupV1Record;
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
@@ -13,31 +14,37 @@ 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();
public static SignalStorageRecord remoteToLocalStorageRecord(StorageItem item, StorageKey storageKey) throws IOException, InvalidKeyException {
byte[] key = item.getKey().toByteArray();
byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveItemKey(key), item.getValue().toByteArray());
StorageRecord record = StorageRecord.parseFrom(rawRecord);
if (record.getType() == StorageRecord.Type.CONTACT_VALUE && record.hasContact()) {
return SignalStorageRecord.forContact(storageKey, remoteToLocalContactRecord(storageKey, record.getContact()));
} else {
return SignalStorageRecord.forUnknown(storageKey, record.getType());
switch (record.getType()) {
case StorageRecord.Type.CONTACT_VALUE:
return SignalStorageRecord.forContact(key, remoteToLocalContactRecord(key, record.getContact()));
case StorageRecord.Type.GROUPV1_VALUE:
return SignalStorageRecord.forGroupV1(key, remoteToLocalGroupV1Record(key, record.getGroupV1()));
default:
return SignalStorageRecord.forUnknown(key, record.getType());
}
}
public static StorageItem localToRemoteStorageRecord(SignalStorageRecord record, SignalStorageCipher cipher) throws IOException {
public static StorageItem localToRemoteStorageRecord(SignalStorageRecord record, StorageKey storageKey) {
StorageRecord.Builder builder = StorageRecord.newBuilder();
if (record.getContact().isPresent()) {
builder.setContact(localToRemoteContactRecord(record.getContact().get()));
} else if (record.getGroupV1().isPresent()) {
builder.setGroupV1(localToRemoteGroupV1Record(record.getGroupV1().get()));
} else {
throw new InvalidStorageWriteError();
}
builder.setType(record.getType());
StorageRecord remoteRecord = builder.build();
byte[] encryptedRecord = cipher.encrypt(remoteRecord.toByteArray());
StorageRecord remoteRecord = builder.build();
StorageItemKey itemKey = storageKey.deriveItemKey(record.getKey());
byte[] encryptedRecord = SignalStorageCipher.encrypt(itemKey, remoteRecord.toByteArray());
return StorageItem.newBuilder()
.setKey(ByteString.copyFrom(record.getKey()))
@@ -45,9 +52,9 @@ public final class SignalStorageModels {
.build();
}
public static SignalContactRecord remoteToLocalContactRecord(byte[] storageKey, ContactRecord contact) throws IOException {
private static SignalContactRecord remoteToLocalContactRecord(byte[] key, ContactRecord contact) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(storageKey, address);
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(key, address);
if (contact.hasBlocked()) {
builder.setBlocked(contact.getBlocked());
@@ -66,8 +73,12 @@ public final class SignalStorageModels {
builder.setProfileKey(contact.getProfile().getKey().toByteArray());
}
if (contact.getProfile().hasName()) {
builder.setProfileName(contact.getProfile().getName());
if (contact.getProfile().hasGivenName()) {
builder.setGivenName(contact.getProfile().getGivenName());
}
if (contact.getProfile().hasFamilyName()) {
builder.setFamilyName(contact.getProfile().getFamilyName());
}
if (contact.getProfile().hasUsername()) {
@@ -84,7 +95,7 @@ public final class SignalStorageModels {
switch (contact.getIdentity().getState()) {
case VERIFIED: builder.setIdentityState(SignalContactRecord.IdentityState.VERIFIED);
case UNVERIFIED: builder.setIdentityState(SignalContactRecord.IdentityState.UNVERIFIED);
default: builder.setIdentityState(SignalContactRecord.IdentityState.VERIFIED);
default: builder.setIdentityState(SignalContactRecord.IdentityState.DEFAULT);
}
}
}
@@ -92,7 +103,21 @@ public final class SignalStorageModels {
return builder.build();
}
public static ContactRecord localToRemoteContactRecord(SignalContactRecord contact) {
private static SignalGroupV1Record remoteToLocalGroupV1Record(byte[] key, GroupV1Record groupV1) {
SignalGroupV1Record.Builder builder = new SignalGroupV1Record.Builder(key, groupV1.getId().toByteArray());
if (groupV1.hasBlocked()) {
builder.setBlocked(groupV1.getBlocked());
}
if (groupV1.hasWhitelisted()) {
builder.setProfileSharingEnabled(groupV1.getWhitelisted());
}
return builder.build();
}
private static ContactRecord localToRemoteContactRecord(SignalContactRecord contact) {
ContactRecord.Builder contactRecordBuilder = ContactRecord.newBuilder()
.setBlocked(contact.isBlocked())
.setWhitelisted(contact.isProfileSharingEnabled());
@@ -128,8 +153,12 @@ public final class SignalStorageModels {
profileBuilder.setKey(ByteString.copyFrom(contact.getProfileKey().get()));
}
if (contact.getProfileName().isPresent()) {
profileBuilder.setName(contact.getProfileName().get());
if (contact.getGivenName().isPresent()) {
profileBuilder.setGivenName(contact.getGivenName().get());
}
if (contact.getFamilyName().isPresent()) {
profileBuilder.setFamilyName(contact.getFamilyName().get());
}
if (contact.getUsername().isPresent()) {
@@ -141,6 +170,14 @@ public final class SignalStorageModels {
return contactRecordBuilder.build();
}
private static GroupV1Record localToRemoteGroupV1Record(SignalGroupV1Record groupV1) {
return GroupV1Record.newBuilder()
.setId(ByteString.copyFrom(groupV1.getGroupId()))
.setBlocked(groupV1.isBlocked())
.setWhitelisted(groupV1.isProfileSharingEnabled())
.build();
}
private static class InvalidStorageWriteError extends Error {
}
}

View File

@@ -6,26 +6,45 @@ import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
import java.util.Arrays;
import java.util.Objects;
public class SignalStorageRecord {
public class SignalStorageRecord implements SignalRecord {
private final byte[] key;
private final int type;
private final Optional<SignalContactRecord> contact;
private final Optional<SignalGroupV1Record> groupV1;
public static SignalStorageRecord forContact(SignalContactRecord contact) {
return forContact(contact.getKey(), contact);
}
public static SignalStorageRecord forContact(byte[] key, SignalContactRecord contact) {
return new SignalStorageRecord(key, StorageRecord.Type.CONTACT_VALUE, Optional.of(contact));
return new SignalStorageRecord(key, StorageRecord.Type.CONTACT_VALUE, Optional.of(contact), Optional.<SignalGroupV1Record>absent());
}
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
return forGroupV1(groupV1.getKey(), groupV1);
}
public static SignalStorageRecord forGroupV1(byte[] key, SignalGroupV1Record groupV1) {
return new SignalStorageRecord(key, StorageRecord.Type.GROUPV1_VALUE, Optional.<SignalContactRecord>absent(), Optional.of(groupV1));
}
public static SignalStorageRecord forUnknown(byte[] key, int type) {
return new SignalStorageRecord(key, type, Optional.<SignalContactRecord>absent());
return new SignalStorageRecord(key, type, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent());
}
private SignalStorageRecord(byte key[], int type, Optional<SignalContactRecord> contact) {
private SignalStorageRecord(byte[] key,
int type,
Optional<SignalContactRecord> contact,
Optional<SignalGroupV1Record> groupV1)
{
this.key = key;
this.type = type;
this.contact = contact;
this.groupV1 = groupV1;
}
@Override
public byte[] getKey() {
return key;
}
@@ -38,8 +57,12 @@ public class SignalStorageRecord {
return contact;
}
public Optional<SignalGroupV1Record> getGroupV1() {
return groupV1;
}
public boolean isUnknown() {
return !contact.isPresent();
return !contact.isPresent() && !groupV1.isPresent();
}
@Override
@@ -48,13 +71,14 @@ public class SignalStorageRecord {
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);
Arrays.equals(key, record.key) &&
contact.equals(record.contact) &&
groupV1.equals(record.groupV1);
}
@Override
public int hashCode() {
int result = Objects.hash(type, contact);
int result = Objects.hash(type, contact, groupV1);
result = 31 * result + Arrays.hashCode(key);
return result;
}

View File

@@ -0,0 +1,5 @@
package org.whispersystems.signalservice.api.storage;
public interface StorageCipherKey {
byte[] serialize();
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.signalservice.api.storage;
import java.util.Arrays;
/**
* Key used to encrypt individual storage items in the storage service.
*
* Created via {@link StorageKey#deriveItemKey(byte[]) }.
*/
public final class StorageItemKey implements StorageCipherKey {
private static final int LENGTH = 32;
private final byte[] key;
StorageItemKey(byte[] key) {
if (key.length != LENGTH) throw new AssertionError();
this.key = key;
}
@Override
public byte[] serialize() {
return key.clone();
}
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) return false;
return Arrays.equals(((StorageItemKey) o).key, key);
}
@Override
public int hashCode() {
return Arrays.hashCode(key);
}
}

View File

@@ -0,0 +1,57 @@
package org.whispersystems.signalservice.api.storage;
import org.whispersystems.signalservice.api.kbs.MasterKey;
import org.whispersystems.signalservice.internal.util.Hex;
import org.whispersystems.util.Base64;
import org.whispersystems.util.StringUtil;
import java.util.Arrays;
import static org.whispersystems.signalservice.api.crypto.CryptoUtil.hmacSha256;
/**
* Key used to encrypt data on the storage service. Not used directly -- instead we used keys that
* are derived for each item we're storing.
*
* Created via {@link MasterKey#deriveStorageServiceKey()}.
*/
public final class StorageKey {
private static final int LENGTH = 32;
private final byte[] key;
public StorageKey(byte[] key) {
if (key.length != LENGTH) throw new AssertionError();
this.key = key;
}
public StorageManifestKey deriveManifestKey(long version) {
return new StorageManifestKey(derive("Manifest_" + version));
}
public StorageItemKey deriveItemKey(byte[] key) {
return new StorageItemKey(derive("Item_" + Base64.encodeBytes(key)));
}
private byte[] derive(String keyName) {
return hmacSha256(key, StringUtil.utf8(keyName));
}
public byte[] serialize() {
return key.clone();
}
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) return false;
return Arrays.equals(((StorageKey) o).key, key);
}
@Override
public int hashCode() {
return Arrays.hashCode(key);
}
}

View File

@@ -0,0 +1,38 @@
package org.whispersystems.signalservice.api.storage;
import java.util.Arrays;
/**
* Key used to encrypt a manifest in the storage service.
*
* Created via {@link StorageKey#deriveManifestKey(long)}.
*/
public final class StorageManifestKey implements StorageCipherKey {
private static final int LENGTH = 32;
private final byte[] key;
StorageManifestKey(byte[] key) {
if (key.length != LENGTH) throw new AssertionError();
this.key = key;
}
@Override
public byte[] serialize() {
return key.clone();
}
@Override
public boolean equals(Object o) {
if (o == null || o.getClass() != getClass()) return false;
return Arrays.equals(((StorageManifestKey) o).key, key);
}
@Override
public int hashCode() {
return Arrays.hashCode(key);
}
}

View File

@@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
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.NoContentException;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
@@ -787,6 +788,16 @@ public class PushServiceSocket {
return StorageManifest.parseFrom(response.body().bytes());
}
public StorageManifest getStorageManifestIfDifferentVersion(String authToken, long version) throws IOException {
Response response = makeStorageRequest(authToken, "/v1/storage/manifest/version/" + version, "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());
@@ -1119,8 +1130,8 @@ public class PushServiceSocket {
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Log.w(TAG, "Push service URL: " + connectionHolder.getUrl());
Log.w(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
Log.d(TAG, "Push service URL: " + connectionHolder.getUrl());
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), urlFragment));
Request.Builder request = new Request.Builder();
request.url(String.format("%s%s", connectionHolder.getUrl(), urlFragment));
@@ -1262,6 +1273,8 @@ public class PushServiceSocket {
.readTimeout(soTimeoutMillis, TimeUnit.MILLISECONDS)
.build();
Log.d(TAG, "Opening URL: " + String.format("%s%s", connectionHolder.getUrl(), path));
Request.Builder request = new Request.Builder().url(connectionHolder.getUrl() + path);
if (body != null) {
@@ -1289,7 +1302,7 @@ public class PushServiceSocket {
try {
response = call.execute();
if (response.isSuccessful()) {
if (response.isSuccessful() && response.code() != 204) {
return response;
}
} catch (IOException e) {
@@ -1301,6 +1314,8 @@ public class PushServiceSocket {
}
switch (response.code()) {
case 204:
throw new NoContentException("No content!");
case 401:
case 403:
throw new AuthorizationFailedException("Authorization failed!");

View File

@@ -33,16 +33,19 @@ message WriteOperation {
optional StorageManifest manifest = 1;
repeated StorageItem insertItem = 2;
repeated bytes deleteKey = 3;
optional bool clearAll = 4;
}
message StorageRecord {
enum Type {
UNKNOWN = 0;
CONTACT = 1;
GROUPV1 = 2;
}
optional uint32 type = 1;
optional ContactRecord contact = 2;
optional GroupV1Record groupV1 = 3;
}
message ContactRecord {
@@ -58,9 +61,10 @@ message ContactRecord {
}
message Profile {
optional string name = 1;
optional bytes key = 2;
optional string username = 3;
optional string givenName = 1;
optional string familyName = 4;
optional bytes key = 2;
optional string username = 3;
}
optional string serviceUuid = 1;
@@ -72,6 +76,12 @@ message ContactRecord {
optional string nickname = 7;
}
message GroupV1Record {
optional bytes id = 1;
optional bool blocked = 2;
optional bool whitelisted = 3;
}
message ManifestRecord {
optional uint64 version = 1;
repeated bytes keys = 2;

View File

@@ -0,0 +1,59 @@
package org.whispersystems.signalservice.api.storage;
import org.junit.Test;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
public class SignalContactRecordTest {
private static final UUID UUID_A = UuidUtil.parseOrThrow("ebef429e-695e-4f51-bcc4-526a60ac68c7");
private static final String E164_A = "+16108675309";
@Test
public void contacts_with_same_identity_key_contents_are_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
@Test
public void contacts_with_different_identity_key_contents_are_not_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
profileKeyCopy[0] = 1;
SignalContactRecord a = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, UUID_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
assertNotEquals(a, b);
assertNotEquals(a.hashCode(), b.hashCode());
}
private static byte[] byteArray(int a) {
byte[] bytes = new byte[4];
bytes[3] = (byte) a;
bytes[2] = (byte)(a >> 8);
bytes[1] = (byte)(a >> 16);
bytes[0] = (byte)(a >> 24);
return bytes;
}
private static SignalContactRecord.Builder contactBuilder(int key,
UUID uuid,
String e164,
String givenName)
{
return new SignalContactRecord.Builder(byteArray(key), new SignalServiceAddress(uuid, e164))
.setGivenName(givenName);
}
}

View File

@@ -0,0 +1,33 @@
package org.whispersystems.signalservice.api.storage;
import org.junit.Test;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.signalservice.internal.util.Util;
import static org.junit.Assert.assertArrayEquals;
public class SignalStorageCipherTest {
@Test
public void symmetry() throws InvalidKeyException {
StorageItemKey key = new StorageItemKey(Util.getSecretBytes(32));
byte[] data = Util.getSecretBytes(1337);
byte[] ciphertext = SignalStorageCipher.encrypt(key, data);
byte[] plaintext = SignalStorageCipher.decrypt(key, ciphertext);
assertArrayEquals(data, plaintext);
}
@Test(expected = InvalidKeyException.class)
public void badKeyOnDecrypt() throws InvalidKeyException {
StorageItemKey key = new StorageItemKey(Util.getSecretBytes(32));
byte[] data = Util.getSecretBytes(1337);
byte[] badKey = key.serialize().clone();
badKey[0] += 1;
byte[] ciphertext = SignalStorageCipher.encrypt(key, data);
byte[] plaintext = SignalStorageCipher.decrypt(new StorageItemKey(badKey), ciphertext);
}
}