The rest of the storage service unwrapping.

This commit is contained in:
Greyson Parrelli
2024-11-13 10:35:02 -05:00
parent 8746f483c0
commit 7dd1fc09c0
33 changed files with 754 additions and 1692 deletions

View File

@@ -37,7 +37,7 @@ sealed class ServiceId(val libSignalServiceId: LibSignalServiceId) {
@JvmOverloads
@JvmStatic
fun parseOrNull(raw: String?, logFailures: Boolean = true): ServiceId? {
if (raw == null) {
if (raw.isNullOrBlank()) {
return null
}

View File

@@ -0,0 +1,15 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.storage
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
val ContactRecord.signalAci: ServiceId.ACI?
get() = ServiceId.ACI.parseOrNull(this.aci)
val ContactRecord.signalPni: ServiceId.PNI?
get() = ServiceId.PNI.parseOrNull(this.pni)

View File

@@ -1,55 +1,27 @@
package org.whispersystems.signalservice.api.storage
import org.signal.core.util.hasUnknownFields
import org.signal.libsignal.protocol.logging.Log
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
import java.io.IOException
class SignalAccountRecord(
/**
* Wrapper around a [AccountRecord] to pair it with a [StorageId].
*/
data class SignalAccountRecord(
override val id: StorageId,
override val proto: AccountRecord
) : SignalRecord<AccountRecord> {
companion object {
private val TAG: String = SignalAccountRecord::class.java.simpleName
fun newBuilder(serializedUnknowns: ByteArray?): AccountRecord.Builder {
return if (serializedUnknowns != null) {
parseUnknowns(serializedUnknowns)
} else {
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: AccountRecord.Builder()
}
private fun builderFromUnknowns(serializedUnknowns: ByteArray): AccountRecord.Builder {
return try {
AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
AccountRecord.Builder()
}
}
private fun parseUnknowns(serializedUnknowns: ByteArray): AccountRecord.Builder {
try {
return AccountRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
Log.w(TAG, "Failed to combine unknown fields!", e)
return AccountRecord.Builder()
}
}
}
fun serializeUnknownFields(): ByteArray? {
return if (proto.hasUnknownFields()) proto.encode() else null
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SignalAccountRecord
if (id != other.id) return false
if (proto != other.proto) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + proto.hashCode()
return result
}
}

View File

@@ -5,64 +5,27 @@
package org.whispersystems.signalservice.api.storage
import okio.ByteString.Companion.toByteString
import org.whispersystems.signalservice.internal.storage.protos.CallLinkRecord
import java.io.IOException
/**
* A record in storage service that represents a call link that was already created.
* Wrapper around a [CallLinkRecord] to pair it with a [StorageId].
*/
class SignalCallLinkRecord(
data class SignalCallLinkRecord(
override val id: StorageId,
override val proto: CallLinkRecord
) : SignalRecord<CallLinkRecord> {
val rootKey: ByteArray = proto.rootKey.toByteArray()
val adminPassKey: ByteArray = proto.adminPasskey.toByteArray()
val deletionTimestamp: Long = proto.deletedAtTimestampMs
fun isDeleted(): Boolean {
return deletionTimestamp > 0
}
class Builder(rawId: ByteArray, serializedUnknowns: ByteArray?) {
private var id: StorageId = StorageId.forCallLink(rawId)
private var builder: CallLinkRecord.Builder
init {
if (serializedUnknowns != null) {
this.builder = parseUnknowns(serializedUnknowns)
} else {
this.builder = CallLinkRecord.Builder()
}
companion object {
fun newBuilder(serializedUnknowns: ByteArray?): CallLinkRecord.Builder {
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: CallLinkRecord.Builder()
}
fun setRootKey(rootKey: ByteArray): Builder {
builder.rootKey = rootKey.toByteString()
return this
}
fun setAdminPassKey(adminPasskey: ByteArray): Builder {
builder.adminPasskey = adminPasskey.toByteString()
return this
}
fun setDeletedTimestamp(deletedTimestamp: Long): Builder {
builder.deletedAtTimestampMs = deletedTimestamp
return this
}
fun build(): SignalCallLinkRecord {
return SignalCallLinkRecord(id, builder.build())
}
companion object {
fun parseUnknowns(serializedUnknowns: ByteArray): CallLinkRecord.Builder {
return try {
CallLinkRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
CallLinkRecord.Builder()
}
private fun builderFromUnknowns(serializedUnknowns: ByteArray): CallLinkRecord.Builder {
return try {
CallLinkRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
CallLinkRecord.Builder()
}
}
}

View File

@@ -1,360 +0,0 @@
package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.api.push.ServiceId.PNI;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;
import okio.ByteString;
public final class SignalContactRecord implements SignalRecord<ContactRecord> {
private static final String TAG = SignalContactRecord.class.getSimpleName();
private final StorageId id;
private final ContactRecord proto;
private final boolean hasUnknownFields;
private final Optional<ACI> aci;
private final Optional<PNI> pni;
private final Optional<String> e164;
private final Optional<String> profileGivenName;
private final Optional<String> profileFamilyName;
private final Optional<String> systemGivenName;
private final Optional<String> systemFamilyName;
private final Optional<String> systemNickname;
private final Optional<byte[]> profileKey;
private final Optional<String> username;
private final Optional<byte[]> identityKey;
private final Optional<String> nicknameGivenName;
private final Optional<String> nicknameFamilyName;
private final Optional<String> note;
public SignalContactRecord(StorageId id, ContactRecord proto) {
this.id = id;
this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.aci = OptionalUtil.absentIfEmpty(proto.aci).map(ACI::parseOrNull).map(it -> it.isUnknown() ? null : it);
this.pni = OptionalUtil.absentIfEmpty(proto.pni).map(PNI::parseOrNull).map(it -> it.isUnknown() ? null : it);
this.e164 = OptionalUtil.absentIfEmpty(proto.e164);
this.profileGivenName = OptionalUtil.absentIfEmpty(proto.givenName);
this.profileFamilyName = OptionalUtil.absentIfEmpty(proto.familyName);
this.systemGivenName = OptionalUtil.absentIfEmpty(proto.systemGivenName);
this.systemFamilyName = OptionalUtil.absentIfEmpty(proto.systemFamilyName);
this.systemNickname = OptionalUtil.absentIfEmpty(proto.systemNickname);
this.profileKey = OptionalUtil.absentIfEmpty(proto.profileKey);
this.username = OptionalUtil.absentIfEmpty(proto.username);
this.identityKey = OptionalUtil.absentIfEmpty(proto.identityKey);
this.nicknameGivenName = Optional.ofNullable(proto.nickname).flatMap(n -> OptionalUtil.absentIfEmpty(n.given));
this.nicknameFamilyName = Optional.ofNullable(proto.nickname).flatMap(n -> OptionalUtil.absentIfEmpty(n.family));
this.note = OptionalUtil.absentIfEmpty(proto.note);
}
@Override
public StorageId getId() {
return id;
}
@Override
public ContactRecord getProto() {
return proto;
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.encode() : null;
}
public Optional<ACI> getAci() {
return aci;
}
public Optional<PNI> getPni() {
return pni;
}
public Optional<? extends ServiceId> getServiceId() {
if (aci.isPresent()) {
return aci;
} else if (pni.isPresent()) {
return pni;
} else {
return Optional.empty();
}
}
public Optional<String> getNumber() {
return e164;
}
public Optional<String> getProfileGivenName() {
return profileGivenName;
}
public Optional<String> getProfileFamilyName() {
return profileFamilyName;
}
public Optional<String> getSystemGivenName() {
return systemGivenName;
}
public Optional<String> getSystemFamilyName() {
return systemFamilyName;
}
public Optional<String> getSystemNickname() {
return systemNickname;
}
public Optional<String> getNicknameGivenName() {
return nicknameGivenName;
}
public Optional<String> getNicknameFamilyName() {
return nicknameFamilyName;
}
public Optional<String> getNote() {
return note;
}
public Optional<byte[]> getProfileKey() {
return profileKey;
}
public Optional<String> getUsername() {
return username;
}
public Optional<byte[]> getIdentityKey() {
return identityKey;
}
public IdentityState getIdentityState() {
return proto.identityState;
}
public boolean isBlocked() {
return proto.blocked;
}
public boolean isProfileSharingEnabled() {
return proto.whitelisted;
}
public boolean isArchived() {
return proto.archived;
}
public boolean isForcedUnread() {
return proto.markedUnread;
}
public long getMuteUntil() {
return proto.mutedUntilTimestamp;
}
public boolean shouldHideStory() {
return proto.hideStory;
}
public long getUnregisteredTimestamp() {
return proto.unregisteredAtTimestamp;
}
public boolean isHidden() {
return proto.hidden;
}
public boolean isPniSignatureVerified() {
return proto.pniSignatureVerified;
}
/**
* Returns the same record, but stripped of the PNI field. Only used while PNP is in development.
*/
public SignalContactRecord withoutPni() {
return new SignalContactRecord(id, proto.newBuilder().pni("").build());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalContactRecord that = (SignalContactRecord) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
return Objects.hash(id, proto);
}
public static final class Builder {
private final StorageId id;
private final ContactRecord.Builder builder;
public Builder(byte[] rawId, @Nullable ACI aci, byte[] serializedUnknowns) {
this.id = StorageId.forContact(rawId);
if (serializedUnknowns != null) {
this.builder = parseUnknowns(serializedUnknowns);
} else {
this.builder = new ContactRecord.Builder();
}
builder.aci(aci == null ? "" : aci.toString());
}
public Builder setE164(String e164) {
builder.e164(e164 == null ? "" : e164);
return this;
}
public Builder setPni(PNI pni) {
builder.pni(pni == null ? "" : pni.toStringWithoutPrefix());
return this;
}
public Builder setProfileGivenName(String givenName) {
builder.givenName(givenName == null ? "" : givenName);
return this;
}
public Builder setProfileFamilyName(String familyName) {
builder.familyName(familyName == null ? "" : familyName);
return this;
}
public Builder setSystemGivenName(String givenName) {
builder.systemGivenName(givenName == null ? "" : givenName);
return this;
}
public Builder setSystemFamilyName(String familyName) {
builder.systemFamilyName(familyName == null ? "" : familyName);
return this;
}
public Builder setSystemNickname(String nickname) {
builder.systemNickname(nickname == null ? "" : nickname);
return this;
}
public Builder setProfileKey(byte[] profileKey) {
builder.profileKey(profileKey == null ? ByteString.EMPTY : ByteString.of(profileKey));
return this;
}
public Builder setUsername(String username) {
builder.username(username == null ? "" : username);
return this;
}
public Builder setIdentityKey(byte[] identityKey) {
builder.identityKey(identityKey == null ? ByteString.EMPTY : ByteString.of(identityKey));
return this;
}
public Builder setIdentityState(IdentityState identityState) {
builder.identityState(identityState == null ? IdentityState.DEFAULT : identityState);
return this;
}
public Builder setBlocked(boolean blocked) {
builder.blocked(blocked);
return this;
}
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
builder.whitelisted(profileSharingEnabled);
return this;
}
public Builder setArchived(boolean archived) {
builder.archived(archived);
return this;
}
public Builder setForcedUnread(boolean forcedUnread) {
builder.markedUnread(forcedUnread);
return this;
}
public Builder setMuteUntil(long muteUntil) {
builder.mutedUntilTimestamp(muteUntil);
return this;
}
public Builder setHideStory(boolean hideStory) {
builder.hideStory(hideStory);
return this;
}
public Builder setUnregisteredTimestamp(long timestamp) {
builder.unregisteredAtTimestamp(timestamp);
return this;
}
public Builder setHidden(boolean hidden) {
builder.hidden(hidden);
return this;
}
public Builder setPniSignatureVerified(boolean verified) {
builder.pniSignatureVerified(verified);
return this;
}
public Builder setNicknameGivenName(String nicknameGivenName) {
ContactRecord.Name.Builder name = builder.nickname == null ? new ContactRecord.Name.Builder() : builder.nickname.newBuilder();
name.given(nicknameGivenName);
builder.nickname(name.build());
return this;
}
public Builder setNicknameFamilyName(String nicknameFamilyName) {
ContactRecord.Name.Builder name = builder.nickname == null ? new ContactRecord.Name.Builder() : builder.nickname.newBuilder();
name.family(nicknameFamilyName);
builder.nickname(name.build());
return this;
}
public Builder setNote(String note) {
builder.note(note == null ? "" : note);
return this;
}
private static ContactRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return ContactRecord.ADAPTER.decode(serializedUnknowns).newBuilder();
} catch (IOException e) {
Log.w(TAG, "Failed to combine unknown fields!", e);
return new ContactRecord.Builder();
}
}
public SignalContactRecord build() {
return new SignalContactRecord(id, builder.build());
}
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api.storage
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord
import java.io.IOException
/**
* Wrapper around a [ContactRecord] to pair it with a [StorageId].
*/
data class SignalContactRecord(
override val id: StorageId,
override val proto: ContactRecord
) : SignalRecord<ContactRecord> {
companion object {
fun newBuilder(serializedUnknowns: ByteArray?): ContactRecord.Builder {
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: ContactRecord.Builder()
}
private fun builderFromUnknowns(serializedUnknowns: ByteArray): ContactRecord.Builder {
return try {
ContactRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
ContactRecord.Builder()
}
}
}
}

View File

@@ -1,140 +0,0 @@
package org.whispersystems.signalservice.api.storage;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import okio.ByteString;
public final class SignalGroupV1Record implements SignalRecord<GroupV1Record> {
private static final String TAG = SignalGroupV1Record.class.getSimpleName();
private final StorageId id;
private final GroupV1Record proto;
private final byte[] groupId;
private final boolean hasUnknownFields;
public SignalGroupV1Record(StorageId id, GroupV1Record proto) {
this.id = id;
this.proto = proto;
this.groupId = proto.id.toByteArray();
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
}
@Override
public StorageId getId() {
return id;
}
@Override public GroupV1Record getProto() {
return proto;
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.encode() : null;
}
public byte[] getGroupId() {
return groupId;
}
public boolean isBlocked() {
return proto.blocked;
}
public boolean isProfileSharingEnabled() {
return proto.whitelisted;
}
public boolean isArchived() {
return proto.archived;
}
public boolean isForcedUnread() {
return proto.markedUnread;
}
public long getMuteUntil() {
return proto.mutedUntilTimestamp;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalGroupV1Record that = (SignalGroupV1Record) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
return Objects.hash(id, proto);
}
public static final class Builder {
private final StorageId id;
private final GroupV1Record.Builder builder;
public Builder(byte[] rawId, byte[] groupId, byte[] serializedUnknowns) {
this.id = StorageId.forGroupV1(rawId);
if (serializedUnknowns != null) {
this.builder = parseUnknowns(serializedUnknowns);
} else {
this.builder = new GroupV1Record.Builder();
}
builder.id(ByteString.of(groupId));
}
public Builder setBlocked(boolean blocked) {
builder.blocked(blocked);
return this;
}
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
builder.whitelisted(profileSharingEnabled);
return this;
}
public Builder setArchived(boolean archived) {
builder.archived(archived);
return this;
}
public Builder setForcedUnread(boolean forcedUnread) {
builder.markedUnread(forcedUnread);
return this;
}
public Builder setMuteUntil(long muteUntil) {
builder.mutedUntilTimestamp(muteUntil);
return this;
}
private static GroupV1Record.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return GroupV1Record.ADAPTER.decode(serializedUnknowns).newBuilder();
} catch (IOException e) {
Log.w(TAG, "Failed to combine unknown fields!", e);
return new GroupV1Record.Builder();
}
}
public SignalGroupV1Record build() {
return new SignalGroupV1Record(id, builder.build());
}
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api.storage
import org.whispersystems.signalservice.internal.storage.protos.GroupV1Record
import java.io.IOException
/**
* Wrapper around a [GroupV1Record] to pair it with a [StorageId].
*/
data class SignalGroupV1Record(
override val id: StorageId,
override val proto: GroupV1Record
) : SignalRecord<GroupV1Record> {
companion object {
fun newBuilder(serializedUnknowns: ByteArray?): GroupV1Record.Builder {
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: GroupV1Record.Builder()
}
private fun builderFromUnknowns(serializedUnknowns: ByteArray): GroupV1Record.Builder {
return try {
GroupV1Record.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
GroupV1Record.Builder()
}
}
}
}

View File

@@ -1,182 +0,0 @@
package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import okio.ByteString;
public final class SignalGroupV2Record implements SignalRecord<GroupV2Record> {
private static final String TAG = SignalGroupV2Record.class.getSimpleName();
private final StorageId id;
private final GroupV2Record proto;
private final byte[] masterKey;
private final boolean hasUnknownFields;
public SignalGroupV2Record(StorageId id, GroupV2Record proto) {
this.id = id;
this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.masterKey = proto.masterKey.toByteArray();
}
@Override
public StorageId getId() {
return id;
}
@Override public GroupV2Record getProto() {
return proto;
}
public boolean hasUnknownFields() {
return hasUnknownFields;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.encode() : null;
}
public byte[] getMasterKeyBytes() {
return masterKey;
}
public GroupMasterKey getMasterKeyOrThrow() {
try {
return new GroupMasterKey(masterKey);
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
public boolean isBlocked() {
return proto.blocked;
}
public boolean isProfileSharingEnabled() {
return proto.whitelisted;
}
public boolean isArchived() {
return proto.archived;
}
public boolean isForcedUnread() {
return proto.markedUnread;
}
public long getMuteUntil() {
return proto.mutedUntilTimestamp;
}
public boolean notifyForMentionsWhenMuted() {
return !proto.dontNotifyForMentionsIfMuted;
}
public boolean shouldHideStory() {
return proto.hideStory;
}
public GroupV2Record.StorySendMode getStorySendMode() {
return proto.storySendMode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalGroupV2Record that = (SignalGroupV2Record) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
return Objects.hash(id, proto);
}
public static final class Builder {
private final StorageId id;
private final GroupV2Record.Builder builder;
public Builder(byte[] rawId, GroupMasterKey masterKey, byte[] serializedUnknowns) {
this(rawId, masterKey.serialize(), serializedUnknowns);
}
public Builder(byte[] rawId, byte[] masterKey, byte[] serializedUnknowns) {
this.id = StorageId.forGroupV2(rawId);
if (serializedUnknowns != null) {
this.builder = parseUnknowns(serializedUnknowns);
} else {
this.builder = new GroupV2Record.Builder();
}
builder.masterKey(ByteString.of(masterKey));
}
public Builder setBlocked(boolean blocked) {
builder.blocked(blocked);
return this;
}
public Builder setProfileSharingEnabled(boolean profileSharingEnabled) {
builder.whitelisted(profileSharingEnabled);
return this;
}
public Builder setArchived(boolean archived) {
builder.archived(archived);
return this;
}
public Builder setForcedUnread(boolean forcedUnread) {
builder.markedUnread(forcedUnread);
return this;
}
public Builder setMuteUntil(long muteUntil) {
builder.mutedUntilTimestamp(muteUntil);
return this;
}
public Builder setNotifyForMentionsWhenMuted(boolean value) {
builder.dontNotifyForMentionsIfMuted(!value);
return this;
}
public Builder setHideStory(boolean hideStory) {
builder.hideStory(hideStory);
return this;
}
public Builder setStorySendMode(GroupV2Record.StorySendMode storySendMode) {
builder.storySendMode(storySendMode);
return this;
}
private static GroupV2Record.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return GroupV2Record.ADAPTER.decode(serializedUnknowns).newBuilder();
} catch (IOException e) {
Log.w(TAG, "Failed to combine unknown fields!", e);
return new GroupV2Record.Builder();
}
}
public SignalGroupV2Record build() {
return new SignalGroupV2Record(id, builder.build());
}
}
}

View File

@@ -0,0 +1,27 @@
package org.whispersystems.signalservice.api.storage
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
import java.io.IOException
/**
* Wrapper around a [GroupV2Record] to pair it with a [StorageId].
*/
data class SignalGroupV2Record(
override val id: StorageId,
override val proto: GroupV2Record
) : SignalRecord<GroupV2Record> {
companion object {
fun newBuilder(serializedUnknowns: ByteArray?): GroupV2Record.Builder {
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: GroupV2Record.Builder()
}
private fun builderFromUnknowns(serializedUnknowns: ByteArray): GroupV2Record.Builder {
return try {
GroupV2Record.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
GroupV2Record.Builder()
}
}
}
}

View File

@@ -1,12 +1,20 @@
package org.whispersystems.signalservice.api.storage
import com.squareup.wire.Message
import org.signal.core.util.hasUnknownFields
import kotlin.reflect.KVisibility
import kotlin.reflect.full.memberProperties
/**
* Pairs a storage record with its id. Also contains some useful common methods.
*/
interface SignalRecord<E> {
val id: StorageId
val proto: E
val serializedUnknowns: ByteArray?
get() = (proto as Message<*, *>).takeIf { it.hasUnknownFields() }?.encode()
fun describeDiff(other: SignalRecord<*>): String {
if (this::class != other::class) {
return "Classes are different!"

View File

@@ -64,33 +64,12 @@ object SignalStorageModels {
@JvmStatic
fun localToRemoteStorageRecord(record: SignalStorageRecord, storageKey: StorageKey): StorageItem {
val builder = StorageRecord.Builder()
if (record.proto.contact != null) {
builder.contact(record.proto.contact)
} else if (record.proto.groupV1 != null) {
builder.groupV1(record.proto.groupV1)
} else if (record.proto.groupV2 != null) {
builder.groupV2(record.proto.groupV2)
} else if (record.proto.account != null) {
builder.account(record.proto.account)
} else if (record.proto.storyDistributionList != null) {
builder.storyDistributionList(record.proto.storyDistributionList)
} else if (record.proto.callLink != null) {
builder.callLink(record.proto.callLink)
} else {
throw InvalidStorageWriteError()
}
val remoteRecord = builder.build()
val itemKey = storageKey.deriveItemKey(record.id.raw)
val encryptedRecord = SignalStorageCipher.encrypt(itemKey, remoteRecord.encode())
val encryptedRecord = SignalStorageCipher.encrypt(itemKey, record.proto.encode())
return StorageItem.Builder()
.key(record.id.raw.toByteString())
.value_(encryptedRecord.toByteString())
.build()
}
private class InvalidStorageWriteError : Error()
}

View File

@@ -1,151 +0,0 @@
package org.whispersystems.signalservice.api.storage;
import org.jetbrains.annotations.NotNull;
import org.signal.core.util.ProtoUtil;
import org.signal.libsignal.protocol.logging.Log;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord;
import java.io.IOException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import okio.ByteString;
public class SignalStoryDistributionListRecord implements SignalRecord<StoryDistributionListRecord> {
private static final String TAG = SignalStoryDistributionListRecord.class.getSimpleName();
private final StorageId id;
private final StoryDistributionListRecord proto;
private final boolean hasUnknownFields;
private final List<SignalServiceAddress> recipients;
public SignalStoryDistributionListRecord(StorageId id, StoryDistributionListRecord proto) {
this.id = id;
this.proto = proto;
this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto);
this.recipients = proto.recipientServiceIds
.stream()
.map(ServiceId::parseOrNull)
.filter(Objects::nonNull)
.map(SignalServiceAddress::new)
.collect(Collectors.toList());
}
@Override
public StorageId getId() {
return id;
}
@Override
public StoryDistributionListRecord getProto() {
return proto;
}
public byte[] serializeUnknownFields() {
return hasUnknownFields ? proto.encode() : null;
}
public byte[] getIdentifier() {
return proto.identifier.toByteArray();
}
public String getName() {
return proto.name;
}
public List<SignalServiceAddress> getRecipients() {
return recipients;
}
public long getDeletedAtTimestamp() {
return proto.deletedAtTimestamp;
}
public boolean allowsReplies() {
return proto.allowsReplies;
}
public boolean isBlockList() {
return proto.isBlockList;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
SignalStoryDistributionListRecord that = (SignalStoryDistributionListRecord) o;
return id.equals(that.id) &&
proto.equals(that.proto);
}
@Override
public int hashCode() {
return Objects.hash(id, proto);
}
public static final class Builder {
private final StorageId id;
private final StoryDistributionListRecord.Builder builder;
public Builder(byte[] rawId, byte[] serializedUnknowns) {
this.id = StorageId.forStoryDistributionList(rawId);
if (serializedUnknowns != null) {
this.builder = parseUnknowns(serializedUnknowns);
} else {
this.builder = new StoryDistributionListRecord.Builder();
}
}
public Builder setIdentifier(byte[] identifier) {
builder.identifier(ByteString.of(identifier));
return this;
}
public Builder setName(String name) {
builder.name(name);
return this;
}
public Builder setRecipients(List<SignalServiceAddress> recipients) {
builder.recipientServiceIds = recipients.stream()
.map(SignalServiceAddress::getIdentifier)
.collect(Collectors.toList());
return this;
}
public Builder setDeletedAtTimestamp(long deletedAtTimestamp) {
builder.deletedAtTimestamp(deletedAtTimestamp);
return this;
}
public Builder setAllowsReplies(boolean allowsReplies) {
builder.allowsReplies(allowsReplies);
return this;
}
public Builder setIsBlockList(boolean isBlockList) {
builder.isBlockList(isBlockList);
return this;
}
public SignalStoryDistributionListRecord build() {
return new SignalStoryDistributionListRecord(id, builder.build());
}
private static StoryDistributionListRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
try {
return StoryDistributionListRecord.ADAPTER.decode(serializedUnknowns).newBuilder();
} catch (IOException e) {
Log.w(TAG, "Failed to combine unknown fields!", e);
return new StoryDistributionListRecord.Builder();
}
}
}
}

View File

@@ -0,0 +1,24 @@
package org.whispersystems.signalservice.api.storage
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
import java.io.IOException
data class SignalStoryDistributionListRecord(
override val id: StorageId,
override val proto: StoryDistributionListRecord
) : SignalRecord<StoryDistributionListRecord> {
companion object {
fun newBuilder(serializedUnknowns: ByteArray?): StoryDistributionListRecord.Builder {
return serializedUnknowns?.let { builderFromUnknowns(it) } ?: StoryDistributionListRecord.Builder()
}
private fun builderFromUnknowns(serializedUnknowns: ByteArray): StoryDistributionListRecord.Builder {
return try {
StoryDistributionListRecord.ADAPTER.decode(serializedUnknowns).newBuilder()
} catch (e: IOException) {
StoryDistributionListRecord.Builder()
}
}
}
}

View File

@@ -7,6 +7,9 @@ import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.util.Arrays;
import java.util.Objects;
/**
* A copy of {@link ManifestRecord.Identifier} that allows us to more easily store unknown types with their integer constant.
*/
public class StorageId {
private final int type;
private final byte[] raw;

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.storage
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.storage.protos.StoryDistributionListRecord
val StoryDistributionListRecord.recipientServiceAddresses: List<SignalServiceAddress>
get() {
return this.recipientServiceIds
.mapNotNull { ServiceId.parseOrNull(it) }
.map { SignalServiceAddress(it) }
}

View File

@@ -46,6 +46,10 @@ public final class UuidUtil {
return new UUID(high, low);
}
public static UUID parseOrThrow(ByteString bytes) {
return parseOrNull(bytes.toByteArray());
}
public static boolean isUuid(String uuid) {
return uuid != null && UUID_PATTERN.matcher(uuid).matches();
}
@@ -83,6 +87,10 @@ public final class UuidUtil {
return byteArray != null && byteArray.length == 16 ? parseOrThrow(byteArray) : null;
}
public static UUID parseOrNull(ByteString byteString) {
return parseOrNull(byteString.toByteArray());
}
public static List<UUID> fromByteStrings(Collection<ByteString> byteStringCollection) {
ArrayList<UUID> result = new ArrayList<>(byteStringCollection.size());

View File

@@ -1,8 +1,10 @@
package org.whispersystems.signalservice.api.storage;
import org.junit.Test;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
import org.whispersystems.signalservice.internal.storage.protos.ContactRecord;
import okio.ByteString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
@@ -14,27 +16,33 @@ public class SignalContactRecordTest {
@Test
public void contacts_with_same_identity_key_contents_are_equal() {
byte[] profileKey = new byte[32];
byte[] profileKeyCopy = profileKey.clone();
byte[] identityKey = new byte[32];
byte[] identityKeyCopy = identityKey.clone();
SignalContactRecord a = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKey)).build();
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKeyCopy)).build();
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
assertEquals(signalContactA, signalContactB);
assertEquals(signalContactA.hashCode(), signalContactB.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;
byte[] identityKey = new byte[32];
byte[] identityKeyCopy = identityKey.clone();
identityKeyCopy[0] = 1;
SignalContactRecord a = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKey).build();
SignalContactRecord b = contactBuilder(1, ACI_A, E164_A, "a").setIdentityKey(profileKeyCopy).build();
ContactRecord contactA = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKey)).build();
ContactRecord contactB = contactBuilder(ACI_A, E164_A, "a").identityKey(ByteString.of(identityKeyCopy)).build();
assertNotEquals(a, b);
assertNotEquals(a.hashCode(), b.hashCode());
SignalContactRecord signalContactA = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactA);
SignalContactRecord signalContactB = new SignalContactRecord(StorageId.forContact(byteArray(1)), contactB);
assertNotEquals(signalContactA, signalContactB);
assertNotEquals(signalContactA.hashCode(), signalContactB.hashCode());
}
private static byte[] byteArray(int a) {
@@ -46,13 +54,9 @@ public class SignalContactRecordTest {
return bytes;
}
private static SignalContactRecord.Builder contactBuilder(int key,
ACI serviceId,
String e164,
String givenName)
{
return new SignalContactRecord.Builder(byteArray(key), serviceId, null)
.setE164(e164)
.setProfileGivenName(givenName);
private static ContactRecord.Builder contactBuilder(ACI serviceId, String e164, String givenName) {
return new ContactRecord.Builder()
.e164(e164)
.givenName(givenName);
}
}