Update and refactor storage service syncing.

Switched to proto3, updated protos, and generally refactored things to
make it easier to add new storage record types.
This commit is contained in:
Greyson Parrelli
2020-02-28 13:03:06 -05:00
parent 40d9d663ec
commit 5f7075d39a
25 changed files with 1484 additions and 1387 deletions

View File

@@ -17,13 +17,13 @@ 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.StorageId;
import org.whispersystems.signalservice.api.storage.StorageKey;
import org.whispersystems.signalservice.api.messages.calls.TurnServerInfo;
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo;
@@ -424,40 +424,40 @@ public class SignalServiceAccountManager {
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));
return Optional.of(SignalStorageModels.remoteToLocalStorageManifest(storageManifest, storageKey));
} catch (NoContentException e) {
return Optional.absent();
}
}
public List<SignalStorageRecord> readStorageRecords(StorageKey storageKey, List<byte[]> storageKeys) throws IOException, InvalidKeyException {
ReadOperation.Builder operation = ReadOperation.newBuilder();
public List<SignalStorageRecord> readStorageRecords(StorageKey storageKey, List<StorageId> storageKeys) throws IOException, InvalidKeyException {
List<SignalStorageRecord> result = new ArrayList<>();
ReadOperation.Builder operation = ReadOperation.newBuilder();
Map<ByteString, Integer> typeMap = new HashMap<>();
for (byte[] key : storageKeys) {
operation.addReadKey(ByteString.copyFrom(key));
for (StorageId key : storageKeys) {
typeMap.put(ByteString.copyFrom(key.getRaw()), key.getType());
if (StorageId.isKnownType(key.getType())) {
operation.addReadKey(ByteString.copyFrom(key.getRaw()));
} else {
result.add(SignalStorageRecord.forUnknown(key));
}
}
String authToken = this.pushServiceSocket.getStorageAuth();
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
List<SignalStorageRecord> result = new ArrayList<>(items.getItemsCount());
String authToken = this.pushServiceSocket.getStorageAuth();
StorageItems items = this.pushServiceSocket.readStorageItems(authToken, operation.build());
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, storageKey));
Integer type = typeMap.get(item.getKey());
if (type != null) {
result.add(SignalStorageModels.remoteToLocalStorageRecord(item, type, storageKey));
} else {
Log.w(TAG, "Encountered a StorageItem with no key! Skipping.");
Log.w(TAG, "No type found! Skipping.");
}
}
@@ -498,8 +498,11 @@ public class SignalServiceAccountManager {
{
ManifestRecord.Builder manifestRecordBuilder = ManifestRecord.newBuilder().setVersion(manifest.getVersion());
for (byte[] key : manifest.getStorageKeys()) {
manifestRecordBuilder.addKeys(ByteString.copyFrom(key));
for (StorageId id : manifest.getStorageIds()) {
ManifestRecord.Identifier idProto = ManifestRecord.Identifier.newBuilder()
.setRaw(ByteString.copyFrom(id.getRaw()))
.setType(ManifestRecord.Identifier.Type.forNumber(id.getType())).build();
manifestRecordBuilder.addIdentifiers(idProto);
}
String authToken = this.pushServiceSocket.getStorageAuth();
@@ -529,13 +532,13 @@ public class SignalServiceAccountManager {
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());
List<StorageId> ids = new ArrayList<>(record.getIdentifiersCount());
for (ByteString key : record.getKeysList()) {
keys.add(key.toByteArray());
for (ManifestRecord.Identifier id : record.getIdentifiersList()) {
ids.add(StorageId.forType(id.getRaw().toByteArray(), id.getType().getNumber()));
}
SignalStorageManifest conflictManifest = new SignalStorageManifest(record.getVersion(), keys);
SignalStorageManifest conflictManifest = new SignalStorageManifest(record.getVersion(), ids);
return Optional.of(conflictManifest);
} else {

View File

@@ -1,57 +1,43 @@
package org.whispersystems.signalservice.api.storage;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
import java.util.Arrays;
import java.util.Objects;
public final class SignalContactRecord implements SignalRecord {
private final byte[] key;
private final StorageId id;
private final ContactRecord proto;
private final SignalServiceAddress address;
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;
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 givenName,
String familyName,
byte[] profileKey,
String username,
byte[] identityKey,
IdentityState identityState,
boolean blocked,
boolean profileSharingEnabled,
String nickname,
int protoVersion)
{
this.key = key;
this.address = address;
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);
this.identityState = identityState != null ? identityState : IdentityState.DEFAULT;
this.blocked = blocked;
this.profileSharingEnabled = profileSharingEnabled;
this.nickname = Optional.fromNullable(nickname);
this.protoVersion = protoVersion;
private SignalContactRecord(StorageId id, ContactRecord proto) {
this.id = id;
this.proto = proto;
this.address = new SignalServiceAddress(UuidUtil.parseOrNull(proto.getServiceUuid()), proto.getServiceE164());
this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName());
this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName());
this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey());
this.username = OptionalUtil.absentIfEmpty(proto.getUsername());
this.identityKey = OptionalUtil.absentIfEmpty(proto.getIdentityKey());
}
@Override
public byte[] getKey() {
return key;
public StorageId getId() {
return id;
}
public SignalServiceAddress getAddress() {
@@ -79,139 +65,98 @@ public final class SignalContactRecord implements SignalRecord {
}
public IdentityState getIdentityState() {
return identityState;
return proto.getIdentityState();
}
public boolean isBlocked() {
return blocked;
return proto.getBlocked();
}
public boolean isProfileSharingEnabled() {
return profileSharingEnabled;
return proto.getWhitelisted();
}
public Optional<String> getNickname() {
return nickname;
public boolean isArchived() {
return proto.getArchived();
}
public int getProtoVersion() {
return protoVersion;
ContactRecord toProto() {
return proto;
}
@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) &&
givenName.equals(contact.givenName) &&
familyName.equals(contact.familyName) &&
OptionalUtil.byteArrayEquals(profileKey, contact.profileKey) &&
username.equals(contact.username) &&
OptionalUtil.byteArrayEquals(identityKey, contact.identityKey) &&
identityState == contact.identityState &&
Objects.equals(nickname, contact.nickname);
SignalContactRecord that = (SignalContactRecord) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
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);
return result;
return Objects.hash(id, proto);
}
public static final class Builder {
private final byte[] key;
private final SignalServiceAddress address;
private final StorageId id;
private final ContactRecord.Builder builder;
private String givenName;
private String familyName;
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[] rawId, SignalServiceAddress address) {
this.id = StorageId.forContact(rawId);
this.builder = ContactRecord.newBuilder();
public Builder(byte[] key, SignalServiceAddress address) {
this.key = key;
this.address = address;
builder.setServiceUuid(address.getUuid().isPresent() ? address.getUuid().get().toString() : "");
builder.setServiceE164(address.getNumber().or(""));
}
public Builder setGivenName(String givenName) {
this.givenName = givenName;
builder.setGivenName(givenName == null ? "" : givenName);
return this;
}
public Builder setFamilyName(String familyName) {
this.familyName = familyName;
builder.setFamilyName(familyName == null ? "" : familyName);
return this;
}
public Builder setProfileKey(byte[] profileKey) {
this.profileKey = profileKey;
builder.setProfileKey(profileKey == null ? ByteString.EMPTY : ByteString.copyFrom(profileKey));
return this;
}
public Builder setUsername(String username) {
this.username = username;
builder.setUsername(username == null ? "" : username);
return this;
}
public Builder setIdentityKey(byte[] identityKey) {
this.identityKey = identityKey;
builder.setIdentityKey(identityKey == null ? ByteString.EMPTY : ByteString.copyFrom(identityKey));
return this;
}
public Builder setIdentityState(IdentityState identityState) {
this.identityState = identityState;
builder.setIdentityState(identityState == null ? IdentityState.DEFAULT : identityState);
return this;
}
public Builder setBlocked(boolean blocked) {
this.blocked = blocked;
builder.setBlocked(blocked);
return this;
}
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
this.profileSharingEnabled = profileSharingEnabled;
builder.setWhitelisted(profileSharingEnabled);
return this;
}
public Builder setNickname(String nickname) {
this.nickname = nickname;
return this;
}
Builder setProtoVersion(int version) {
this.version = version;
public Builder setArchived(boolean archived) {
builder.setArchived(archived);
return this;
}
public SignalContactRecord build() {
return new SignalContactRecord(key,
address,
givenName,
familyName,
profileKey,
username,
identityKey,
identityState,
blocked,
profileSharingEnabled,
nickname,
version);
return new SignalContactRecord(id, builder.build());
}
}
public enum IdentityState {
DEFAULT, VERIFIED, UNVERIFIED
}
}

View File

@@ -1,29 +1,26 @@
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 com.google.protobuf.ByteString;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
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 final StorageId id;
private final GroupV1Record proto;
private final byte[] groupId;
private SignalGroupV1Record(byte[] key, byte[] groupId, boolean blocked, boolean profileSharingEnabled) {
this.key = key;
this.groupId = groupId;
this.blocked = blocked;
this.profileSharingEnabled = profileSharingEnabled;
private SignalGroupV1Record(StorageId id, GroupV1Record proto) {
this.id = id;
this.proto = proto;
this.groupId = proto.getId().toByteArray();
}
@Override
public byte[] getKey() {
return key;
public StorageId getId() {
return id;
}
public byte[] getGroupId() {
@@ -31,11 +28,19 @@ public final class SignalGroupV1Record implements SignalRecord {
}
public boolean isBlocked() {
return blocked;
return proto.getBlocked();
}
public boolean isProfileSharingEnabled() {
return profileSharingEnabled;
return proto.getWhitelisted();
}
public boolean isArchived() {
return proto.getArchived();
}
GroupV1Record toProto() {
return proto;
}
@Override
@@ -43,43 +48,43 @@ public final class SignalGroupV1Record implements SignalRecord {
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);
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
int result = Objects.hash(blocked, profileSharingEnabled);
result = 31 * result + Arrays.hashCode(key);
result = 31 * result + Arrays.hashCode(groupId);
return result;
return Objects.hash(id, proto);
}
public static final class Builder {
private final byte[] key;
private final byte[] groupId;
private boolean blocked;
private boolean profileSharingEnabled;
private final StorageId id;
private final GroupV1Record.Builder builder;
public Builder(byte[] key, byte[] groupId) {
this.key = key;
this.groupId = groupId;
public Builder(byte[] rawId, byte[] groupId) {
this.id = StorageId.forGroupV1(rawId);
this.builder = GroupV1Record.newBuilder();
builder.setId(ByteString.copyFrom(groupId));
}
public Builder setBlocked(boolean blocked) {
this.blocked = blocked;
builder.setBlocked(blocked);
return this;
}
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
this.profileSharingEnabled = profileSharingEnabled;
builder.setWhitelisted(profileSharingEnabled);
return this;
}
public Builder setArchived(boolean archived) {
builder.setArchived(archived);
return this;
}
public SignalGroupV1Record build() {
return new SignalGroupV1Record(key, groupId, blocked, profileSharingEnabled);
return new SignalGroupV1Record(id, builder.build());
}
}
}

View File

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

View File

@@ -3,19 +3,19 @@ package org.whispersystems.signalservice.api.storage;
import java.util.List;
public class SignalStorageManifest {
private final long version;
private final List<byte[]> storageKeys;
private final long version;
private final List<StorageId> storageIds;
public SignalStorageManifest(long version, List<byte[]> storageKeys) {
public SignalStorageManifest(long version, List<StorageId> storageIds) {
this.version = version;
this.storageKeys = storageKeys;
this.storageIds = storageIds;
}
public long getVersion() {
return version;
}
public List<byte[]> getStorageKeys() {
return storageKeys;
public List<StorageId> getStorageIds() {
return storageIds;
}
}

View File

@@ -7,25 +7,40 @@ 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.ManifestRecord;
import org.whispersystems.signalservice.internal.storage.protos.StorageItem;
import org.whispersystems.signalservice.internal.storage.protos.StorageManifest;
import org.whispersystems.signalservice.internal.storage.protos.StorageRecord;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public final class SignalStorageModels {
public static SignalStorageRecord remoteToLocalStorageRecord(StorageItem item, StorageKey storageKey) throws IOException, InvalidKeyException {
public static SignalStorageManifest remoteToLocalStorageManifest(StorageManifest manifest, StorageKey storageKey) throws IOException, InvalidKeyException {
byte[] rawRecord = SignalStorageCipher.decrypt(storageKey.deriveManifestKey(manifest.getVersion()), manifest.getValue().toByteArray());
ManifestRecord manifestRecord = ManifestRecord.parseFrom(rawRecord);
List<StorageId> ids = new ArrayList<>(manifestRecord.getIdentifiersCount());
for (ManifestRecord.Identifier id : manifestRecord.getIdentifiersList()) {
ids.add(StorageId.forType(id.getRaw().toByteArray(), id.getType().getNumber()));
}
return new SignalStorageManifest(manifestRecord.getVersion(), ids);
}
public static SignalStorageRecord remoteToLocalStorageRecord(StorageItem item, int type, 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);
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());
if (record.hasContact() && type == ManifestRecord.Identifier.Type.CONTACT_VALUE) {
return SignalStorageRecord.forContact(StorageId.forContact(key), remoteToLocalContactRecord(key, record.getContact()));
} else if (record.hasGroupV1() && type == ManifestRecord.Identifier.Type.GROUPV1_VALUE) {
return SignalStorageRecord.forGroupV1(StorageId.forGroupV1(key), remoteToLocalGroupV1Record(key, record.getGroupV1()));
} else {
return SignalStorageRecord.forUnknown(StorageId.forType(key, type));
}
}
@@ -33,149 +48,43 @@ public final class SignalStorageModels {
StorageRecord.Builder builder = StorageRecord.newBuilder();
if (record.getContact().isPresent()) {
builder.setContact(localToRemoteContactRecord(record.getContact().get()));
builder.setContact(record.getContact().get().toProto());
} else if (record.getGroupV1().isPresent()) {
builder.setGroupV1(localToRemoteGroupV1Record(record.getGroupV1().get()));
builder.setGroupV1(record.getGroupV1().get().toProto());
} else {
throw new InvalidStorageWriteError();
}
builder.setType(record.getType());
StorageRecord remoteRecord = builder.build();
StorageItemKey itemKey = storageKey.deriveItemKey(record.getKey());
StorageItemKey itemKey = storageKey.deriveItemKey(record.getId().getRaw());
byte[] encryptedRecord = SignalStorageCipher.encrypt(itemKey, remoteRecord.toByteArray());
return StorageItem.newBuilder()
.setKey(ByteString.copyFrom(record.getKey()))
.setKey(ByteString.copyFrom(record.getId().getRaw()))
.setValue(ByteString.copyFrom(encryptedRecord))
.build();
}
private static SignalContactRecord remoteToLocalContactRecord(byte[] key, ContactRecord contact) {
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
SignalContactRecord.Builder builder = new SignalContactRecord.Builder(key, address);
SignalServiceAddress address = new SignalServiceAddress(UuidUtil.parseOrNull(contact.getServiceUuid()), contact.getServiceE164());
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().hasGivenName()) {
builder.setGivenName(contact.getProfile().getGivenName());
}
if (contact.getProfile().hasFamilyName()) {
builder.setFamilyName(contact.getProfile().getFamilyName());
}
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.DEFAULT);
}
}
}
return builder.build();
return new SignalContactRecord.Builder(key, address)
.setBlocked(contact.getBlocked())
.setProfileSharingEnabled(contact.getWhitelisted())
.setProfileKey(contact.getProfileKey().toByteArray())
.setGivenName(contact.getGivenName())
.setFamilyName(contact.getFamilyName())
.setUsername(contact.getUsername())
.setIdentityKey(contact.getIdentityKey().toByteArray())
.setIdentityState(contact.getIdentityState())
.build();
}
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());
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.getGivenName().isPresent()) {
profileBuilder.setGivenName(contact.getGivenName().get());
}
if (contact.getFamilyName().isPresent()) {
profileBuilder.setFamilyName(contact.getFamilyName().get());
}
if (contact.getUsername().isPresent()) {
profileBuilder.setUsername(contact.getUsername().get());
}
contactRecordBuilder.setProfile(profileBuilder.build());
return contactRecordBuilder.build();
}
private static GroupV1Record localToRemoteGroupV1Record(SignalGroupV1Record groupV1) {
return GroupV1Record.newBuilder()
.setId(ByteString.copyFrom(groupV1.getGroupId()))
.setBlocked(groupV1.isBlocked())
.setWhitelisted(groupV1.isProfileSharingEnabled())
.build();
return new SignalGroupV1Record.Builder(key, groupV1.getId().toByteArray())
.setBlocked(groupV1.getBlocked())
.setProfileSharingEnabled(groupV1.getWhitelisted())
.build();
}
private static class InvalidStorageWriteError extends Error {

View File

@@ -1,56 +1,51 @@
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 implements SignalRecord {
private final byte[] key;
private final int type;
private final StorageId id;
private final Optional<SignalContactRecord> contact;
private final Optional<SignalGroupV1Record> groupV1;
public static SignalStorageRecord forContact(SignalContactRecord contact) {
return forContact(contact.getKey(), contact);
return forContact(contact.getId(), contact);
}
public static SignalStorageRecord forContact(byte[] key, SignalContactRecord contact) {
return new SignalStorageRecord(key, StorageRecord.Type.CONTACT_VALUE, Optional.of(contact), Optional.<SignalGroupV1Record>absent());
public static SignalStorageRecord forContact(StorageId key, SignalContactRecord contact) {
return new SignalStorageRecord(key, Optional.of(contact), Optional.<SignalGroupV1Record>absent());
}
public static SignalStorageRecord forGroupV1(SignalGroupV1Record groupV1) {
return forGroupV1(groupV1.getKey(), groupV1);
return forGroupV1(groupV1.getId(), 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 forGroupV1(StorageId key, SignalGroupV1Record groupV1) {
return new SignalStorageRecord(key, Optional.<SignalContactRecord>absent(), Optional.of(groupV1));
}
public static SignalStorageRecord forUnknown(byte[] key, int type) {
return new SignalStorageRecord(key, type, Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent());
public static SignalStorageRecord forUnknown(StorageId key) {
return new SignalStorageRecord(key,Optional.<SignalContactRecord>absent(), Optional.<SignalGroupV1Record>absent());
}
private SignalStorageRecord(byte[] key,
int type,
private SignalStorageRecord(StorageId id,
Optional<SignalContactRecord> contact,
Optional<SignalGroupV1Record> groupV1)
{
this.key = key;
this.type = type;
this.id = id;
this.contact = contact;
this.groupV1 = groupV1;
}
@Override
public byte[] getKey() {
return key;
public StorageId getId() {
return id;
}
public int getType() {
return type;
return id.getType();
}
public Optional<SignalContactRecord> getContact() {
@@ -69,17 +64,14 @@ public class SignalStorageRecord implements SignalRecord {
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) &&
groupV1.equals(record.groupV1);
SignalStorageRecord that = (SignalStorageRecord) o;
return Objects.equals(id, that.id) &&
Objects.equals(contact, that.contact) &&
Objects.equals(groupV1, that.groupV1);
}
@Override
public int hashCode() {
int result = Objects.hash(type, contact, groupV1);
result = 31 * result + Arrays.hashCode(key);
return result;
return Objects.hash(id, contact, groupV1);
}
}

View File

@@ -0,0 +1,65 @@
package org.whispersystems.signalservice.api.storage;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.util.Arrays;
import java.util.Objects;
public class StorageId {
private final int type;
private final byte[] raw;
public static StorageId forContact(byte[] raw) {
return new StorageId(ManifestRecord.Identifier.Type.CONTACT_VALUE, raw);
}
public static StorageId forGroupV1(byte[] raw) {
return new StorageId(ManifestRecord.Identifier.Type.GROUPV1_VALUE, raw);
}
public static StorageId forType(byte[] raw, int type) {
return new StorageId(type, raw);
}
private StorageId(int type, byte[] raw) {
this.type = type;
this.raw = raw;
}
public int getType() {
return type;
}
public byte[] getRaw() {
return raw;
}
public StorageId withNewBytes(byte[] key) {
return new StorageId(type, key);
}
public static boolean isKnownType(int val) {
for (ManifestRecord.Identifier.Type type : ManifestRecord.Identifier.Type.values()) {
if (type.getNumber() == val) {
return true;
}
}
return false;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StorageId storageId = (StorageId) o;
return type == storageId.type &&
Arrays.equals(raw, storageId.raw);
}
@Override
public int hashCode() {
int result = Objects.hash(type);
result = 31 * result + Arrays.hashCode(raw);
return result;
}
}

View File

@@ -1,5 +1,7 @@
package org.whispersystems.signalservice.api.util;
import com.google.protobuf.ByteString;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Arrays;
@@ -24,4 +26,20 @@ public final class OptionalUtil {
public static int byteArrayHashCode(Optional<byte[]> bytes) {
return bytes.isPresent() ? Arrays.hashCode(bytes.get()) : 0;
}
public static Optional<String> absentIfEmpty(String value) {
if (value == null || value.length() == 0) {
return Optional.absent();
} else {
return Optional.of(value);
}
}
public static Optional<byte[]> absentIfEmpty(ByteString value) {
if (value == null || value.isEmpty()) {
return Optional.absent();
} else {
return Optional.of(value.toByteArray());
}
}
}

View File

@@ -3,86 +3,107 @@
*
* Licensed according to the LICENSE file in this repository.
*/
syntax = "proto2";
syntax = "proto3";
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.storage.protos";
option java_multiple_files = true;
message StorageManifest {
uint64 version = 1;
bytes value = 2;
}
message StorageItem {
optional bytes key = 1;
optional bytes value = 2;
bytes key = 1;
bytes value = 2;
}
message StorageItems {
repeated StorageItem items = 1;
}
message StorageManifest {
optional uint64 version = 1;
optional bytes value = 2;
}
message ReadOperation {
repeated bytes readKey = 1;
}
message WriteOperation {
optional StorageManifest manifest = 1;
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 {
message Identity {
enum State {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional bytes key = 1;
optional State state = 2;
}
message Profile {
optional string givenName = 1;
optional string familyName = 4;
optional bytes key = 2;
optional string username = 3;
}
optional string serviceUuid = 1;
optional string serviceE164 = 2;
optional Profile profile = 3;
optional Identity identity = 4;
optional bool blocked = 5;
optional bool whitelisted = 6;
optional string nickname = 7;
}
message GroupV1Record {
optional bytes id = 1;
optional bool blocked = 2;
optional bool whitelisted = 3;
bool clearAll = 4;
}
message ManifestRecord {
optional uint64 version = 1;
repeated bytes keys = 2;
message Identifier {
enum Type {
UNKNOWN = 0;
CONTACT = 1;
GROUPV1 = 2;
GROUPV2 = 3;
ACCOUNT = 4;
}
bytes raw = 1;
Type type = 2;
}
uint64 version = 1;
repeated Identifier identifiers = 2;
}
message StorageRecord {
oneof record {
ContactRecord contact = 1;
GroupV1Record groupV1 = 2;
GroupV2Record groupV2 = 3;
AccountRecord account = 4;
}
}
message ContactRecord {
enum IdentityState {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
string serviceUuid = 1;
string serviceE164 = 2;
bytes profileKey = 3;
bytes identityKey = 4;
IdentityState identityState = 5;
string givenName = 6;
string familyName = 7;
string username = 8;
bool blocked = 9;
bool whitelisted = 10;
bool archived = 11;
}
message GroupV1Record {
bytes id = 1;
bool blocked = 2;
bool whitelisted = 3;
bool archived = 4;
}
message GroupV2Record {
bytes masterKey = 1;
bool blocked = 2;
bool whitelisted = 3;
bool archived = 4;
}
message AccountRecord {
message Config {
bool readReceipts = 1;
bool sealedSenderIndicators = 2;
bool typingIndicators = 3;
bool linkPreviews = 4;
}
ContactRecord contact = 1;
Config config = 2;
}