mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-26 00:17:27 +00:00
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:
@@ -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 {
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package org.whispersystems.signalservice.api.storage;
|
||||
|
||||
public interface SignalRecord {
|
||||
byte[] getKey();
|
||||
StorageId getId();
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
Reference in New Issue
Block a user