mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-15 20:42:00 +00:00
Update the storage service.
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
public interface SignalRecord {
|
||||
byte[] getKey();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
public interface StorageCipherKey {
|
||||
byte[] serialize();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user