Implement creating V2 Groups

This commit is contained in:
AsamK 2020-12-07 21:06:07 +01:00
parent d267974223
commit 4f2261e86f
19 changed files with 1157 additions and 356 deletions

View file

@ -50,10 +50,23 @@
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
</XML>
<codeStyleSettings language="JAVA">
<option name="RIGHT_MARGIN" value="120" />
<option name="KEEP_LINE_BREAKS" value="false" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
<option name="CALL_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_WRAP" value="5" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
<option name="PARENTHESES_EXPRESSION_LPAREN_WRAP" value="true" />
<option name="PARENTHESES_EXPRESSION_RPAREN_WRAP" value="true" />
<option name="BINARY_OPERATION_WRAP" value="5" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_WRAP" value="5" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
</codeStyleSettings>
<codeStyleSettings language="XML">
<arrangement>

View file

@ -44,7 +44,10 @@ public class DbusSignalImpl implements Signal {
return sendMessage(message, attachments, recipients);
}
private static void checkSendMessageResults(long timestamp, List<SendMessageResult> results) throws DBusExecutionException {
private static void checkSendMessageResults(
long timestamp,
List<SendMessageResult> results
) throws DBusExecutionException {
List<String> errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
if (errors.size() == 0) {
return;
@ -164,13 +167,29 @@ public class DbusSignalImpl implements Signal {
if (group == null) {
return Collections.emptyList();
} else {
return group.getMembers().stream().map(m::resolveSignalServiceAddress).map(SignalServiceAddress::getLegacyIdentifier).collect(Collectors.toList());
return group.getMembers()
.stream()
.map(m::resolveSignalServiceAddress)
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toList());
}
}
@Override
public byte[] updateGroup(final byte[] groupId, final String name, final List<String> members, final String avatar) {
public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) {
try {
if (groupId.length == 0) {
groupId = null;
}
if (name.isEmpty()) {
name = null;
}
if (members.isEmpty()) {
members = null;
}
if (avatar.isEmpty()) {
avatar = null;
}
final Pair<byte[], List<SendMessageResult>> results = m.updateGroup(groupId, name, members, avatar);
checkSendMessageResults(0, results.second());
return results.first();

View file

@ -30,10 +30,6 @@ class KeyUtils {
return getSecretBytes(16);
}
static byte[] createUnrestrictedUnidentifiedAccess() {
return getSecretBytes(16);
}
static byte[] createStickerUploadKey() {
return getSecretBytes(32);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.groups.GroupInfo;
import org.asamk.signal.storage.groups.GroupInfoV1;
import org.asamk.signal.storage.groups.GroupInfoV2;
import org.signal.storageservice.protos.groups.Member;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
public class GroupHelper {
private final ProfileKeyCredentialProvider profileKeyCredentialProvider;
private final ProfileProvider profileProvider;
private final SelfAddressProvider selfAddressProvider;
private final GroupsV2Operations groupsV2Operations;
public GroupHelper(
final ProfileKeyCredentialProvider profileKeyCredentialProvider,
final ProfileProvider profileProvider,
final SelfAddressProvider selfAddressProvider,
final GroupsV2Operations groupsV2Operations
) {
this.profileKeyCredentialProvider = profileKeyCredentialProvider;
this.profileProvider = profileProvider;
this.selfAddressProvider = selfAddressProvider;
this.groupsV2Operations = groupsV2Operations;
}
public static void setGroupContext(
final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo groupInfo
) {
if (groupInfo instanceof GroupInfoV1) {
SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
.withId(groupInfo.groupId)
.build();
messageBuilder.asGroupMessage(group);
} else {
final GroupInfoV2 groupInfoV2 = (GroupInfoV2) groupInfo;
SignalServiceGroupV2 group = SignalServiceGroupV2.newBuilder(groupInfoV2.getMasterKey())
.withRevision(groupInfoV2.getGroup() == null ? 0 : groupInfoV2.getGroup().getRevision())
.build();
messageBuilder.asGroupMessage(group);
}
}
public GroupsV2Operations.NewGroup createGroupV2(
String name, Collection<SignalServiceAddress> members, byte[] avatar
) {
final ProfileKeyCredential profileKeyCredential = profileKeyCredentialProvider.getProfileKeyCredential(
selfAddressProvider.getSelfAddress());
if (profileKeyCredential == null) {
System.err.println("Cannot create a V2 group as self does not have a versioned profile");
return null;
}
final int noUuidCapability = members.stream()
.filter(address -> !address.getUuid().isPresent())
.collect(Collectors.toUnmodifiableSet())
.size();
if (noUuidCapability > 0) {
System.err.println("Cannot create a V2 group as " + noUuidCapability + " members don't have a UUID.");
return null;
}
final int noGv2Capability = members.stream()
.map(profileProvider::getProfile)
.filter(profile -> !profile.getCapabilities().gv2)
.collect(Collectors.toUnmodifiableSet())
.size();
if (noGv2Capability > 0) {
System.err.println("Cannot create a V2 group as " + noGv2Capability + " members don't support Groups V2.");
return null;
}
GroupCandidate self = new GroupCandidate(selfAddressProvider.getSelfAddress().getUuid().orNull(),
Optional.fromNullable(profileKeyCredential));
Set<GroupCandidate> candidates = members.stream()
.map(member -> new GroupCandidate(member.getUuid().get(),
Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member))))
.collect(Collectors.toSet());
final GroupSecretParams groupSecretParams = GroupSecretParams.generate();
return groupsV2Operations.createNewGroup(groupSecretParams,
name,
Optional.fromNullable(avatar),
self,
candidates,
Member.Role.DEFAULT,
0);
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
public interface MessagePipeProvider {
SignalServiceMessagePipe getMessagePipe(boolean unidentified);
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
public interface MessageReceiverProvider {
SignalServiceMessageReceiver getMessageReceiver();
}

View file

@ -0,0 +1,135 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.profiles.ProfileAndCredential;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.internal.util.concurrent.CascadingFuture;
import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture;
import org.whispersystems.util.Base64;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
public final class ProfileHelper {
private final ProfileKeyProvider profileKeyProvider;
private final UnidentifiedAccessProvider unidentifiedAccessProvider;
private final MessagePipeProvider messagePipeProvider;
private final MessageReceiverProvider messageReceiverProvider;
public ProfileHelper(
final ProfileKeyProvider profileKeyProvider,
final UnidentifiedAccessProvider unidentifiedAccessProvider,
final MessagePipeProvider messagePipeProvider,
final MessageReceiverProvider messageReceiverProvider
) {
this.profileKeyProvider = profileKeyProvider;
this.unidentifiedAccessProvider = unidentifiedAccessProvider;
this.messagePipeProvider = messagePipeProvider;
this.messageReceiverProvider = messageReceiverProvider;
}
public ProfileAndCredential retrieveProfileSync(
SignalServiceAddress recipient,
SignalServiceProfile.RequestType requestType
) throws IOException {
try {
return retrieveProfile(recipient, requestType).get(10, TimeUnit.SECONDS);
} catch (ExecutionException e) {
if (e.getCause() instanceof PushNetworkException) {
throw (PushNetworkException) e.getCause();
} else if (e.getCause() instanceof NotFoundException) {
throw (NotFoundException) e.getCause();
} else {
throw new IOException(e);
}
} catch (InterruptedException | TimeoutException e) {
throw new PushNetworkException(e);
}
}
public ListenableFuture<ProfileAndCredential> retrieveProfile(
SignalServiceAddress address,
SignalServiceProfile.RequestType requestType
) {
Optional<UnidentifiedAccess> unidentifiedAccess = getUnidentifiedAccess(address);
Optional<ProfileKey> profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(address));
if (unidentifiedAccess.isPresent()) {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
() -> getSocketRetrievalFuture(address, profileKey, unidentifiedAccess, requestType),
() -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
() -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
} else {
return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType),
() -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)),
e -> !(e instanceof NotFoundException));
}
}
public String decryptName(
ProfileKey profileKey,
String encryptedName
) throws InvalidCiphertextException, IOException {
if (encryptedName == null) {
return null;
}
ProfileCipher profileCipher = new ProfileCipher(profileKey);
return new String(profileCipher.decryptName(Base64.decode(encryptedName)));
}
private ListenableFuture<ProfileAndCredential> getPipeRetrievalFuture(
SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) throws IOException {
SignalServiceMessagePipe unidentifiedPipe = messagePipeProvider.getMessagePipe(true);
SignalServiceMessagePipe pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent()
? unidentifiedPipe
: messagePipeProvider.getMessagePipe(false);
if (pipe != null) {
return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType);
}
throw new IOException("No pipe available!");
}
private ListenableFuture<ProfileAndCredential> getSocketRetrievalFuture(
SignalServiceAddress address,
Optional<ProfileKey> profileKey,
Optional<UnidentifiedAccess> unidentifiedAccess,
SignalServiceProfile.RequestType requestType
) {
SignalServiceMessageReceiver receiver = messageReceiverProvider.getMessageReceiver();
return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType);
}
private Optional<UnidentifiedAccess> getUnidentifiedAccess(SignalServiceAddress recipient) {
Optional<UnidentifiedAccessPair> unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipient);
if (unidentifiedAccess.isPresent()) {
return unidentifiedAccess.get().getTargetUnidentifiedAccess();
}
return Optional.absent();
}
}

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileKeyCredentialProvider {
ProfileKeyCredential getProfileKeyCredential(SignalServiceAddress address);
}

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileKeyProvider {
ProfileKey getProfileKey(SignalServiceAddress address);
}

View file

@ -0,0 +1,9 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface ProfileProvider {
SignalProfile getProfile(SignalServiceAddress address);
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface SelfAddressProvider {
SignalServiceAddress getSelfAddress();
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.signal.zkgroup.profiles.ProfileKey;
public interface SelfProfileKeyProvider {
ProfileKey getProfileKey();
}

View file

@ -0,0 +1,102 @@
package org.asamk.signal.manager.helper;
import org.asamk.signal.storage.profiles.SignalProfile;
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes;
public class UnidentifiedAccessHelper {
private final SelfProfileKeyProvider selfProfileKeyProvider;
private final ProfileKeyProvider profileKeyProvider;
private final ProfileProvider profileProvider;
private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider;
public UnidentifiedAccessHelper(final SelfProfileKeyProvider selfProfileKeyProvider, final ProfileKeyProvider profileKeyProvider, final ProfileProvider profileProvider, final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider) {
this.selfProfileKeyProvider = selfProfileKeyProvider;
this.profileKeyProvider = profileKeyProvider;
this.profileProvider = profileProvider;
this.senderCertificateProvider = senderCertificateProvider;
}
public byte[] getSelfUnidentifiedAccessKey() {
return UnidentifiedAccess.deriveAccessKeyFrom(selfProfileKeyProvider.getProfileKey());
}
public byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
ProfileKey theirProfileKey = profileKeyProvider.getProfileKey(recipient);
if (theirProfileKey == null) {
return null;
}
SignalProfile targetProfile = profileProvider.getProfile(recipient);
if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
return null;
}
if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
return createUnrestrictedUnidentifiedAccess();
}
return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
}
public Optional<UnidentifiedAccessPair> getAccessForSync() {
byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
byte[] selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
return Optional.absent();
}
try {
return Optional.of(new UnidentifiedAccessPair(
new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
));
} catch (InvalidCertificateException e) {
return Optional.absent();
}
}
public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
return recipients.stream()
.map(this::getAccessFor)
.collect(Collectors.toList());
}
public Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
byte[] selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate();
if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
return Optional.absent();
}
try {
return Optional.of(new UnidentifiedAccessPair(
new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
));
} catch (InvalidCertificateException e) {
return Optional.absent();
}
}
private static byte[] createUnrestrictedUnidentifiedAccess() {
return getSecretBytes(16);
}
}

View file

@ -0,0 +1,10 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public interface UnidentifiedAccessProvider {
Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress address);
}

View file

@ -0,0 +1,6 @@
package org.asamk.signal.manager.helper;
public interface UnidentifiedAccessSenderCertificateProvider {
byte[] getSenderCertificate();
}

View file

@ -48,7 +48,7 @@ public class JsonGroupStore {
public void updateGroup(GroupInfo group) {
groups.put(Base64.encodeBytes(group.groupId), group);
if (group instanceof GroupInfoV2) {
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
try {
IOUtils.createPrivateDirectories(groupCachePath);
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) {
@ -103,7 +103,11 @@ public class JsonGroupStore {
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
@Override
public void serialize(final Map<String, GroupInfo> value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException {
public void serialize(
final Map<String, GroupInfo> value,
final JsonGenerator jgen,
final SerializerProvider provider
) throws IOException {
final Collection<GroupInfo> groups = value.values();
jgen.writeStartArray(groups.size());
for (GroupInfo group : groups) {
@ -127,7 +131,10 @@ public class JsonGroupStore {
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
@Override
public Map<String, GroupInfo> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public Map<String, GroupInfo> deserialize(
JsonParser jsonParser,
DeserializationContext deserializationContext
) throws IOException {
Map<String, GroupInfo> groups = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode n : node) {

View file

@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.util.Base64;
@ -32,7 +33,7 @@ public class ProfileStore {
@JsonSerialize(using = ProfileStoreSerializer.class)
private final List<SignalProfileEntry> profiles = new ArrayList<>();
public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) {
public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) {
for (SignalProfileEntry entry : profiles) {
if (entry.getServiceAddress().matches(serviceAddress)) {
return entry;
@ -50,8 +51,18 @@ public class ProfileStore {
return null;
}
public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile);
public void updateProfile(
SignalServiceAddress serviceAddress,
ProfileKey profileKey,
long now,
SignalProfile profile,
ProfileKeyCredential profileKeyCredential
) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress,
profileKey,
now,
profile,
profileKeyCredential);
for (int i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
profiles.set(i, newEntry);
@ -63,7 +74,7 @@ public class ProfileStore {
}
public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) {
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null);
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, 0, null, null);
for (int i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
if (!profiles.get(i).getProfileKey().equals(profileKey)) {
@ -79,28 +90,38 @@ public class ProfileStore {
public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
@Override
public List<SignalProfileEntry> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
public List<SignalProfileEntry> deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
List<SignalProfileEntry> addresses = new ArrayList<>();
if (node.isArray()) {
for (JsonNode entry : node) {
String name = entry.hasNonNull("name")
? entry.get("name").asText()
: null;
UUID uuid = entry.hasNonNull("uuid")
? UuidUtil.parseOrNull(entry.get("uuid").asText())
: null;
String name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
UUID uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name);
ProfileKey profileKey = null;
try {
profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText()));
} catch (InvalidInputException ignored) {
}
ProfileKeyCredential profileKeyCredential = null;
if (entry.hasNonNull("profileKeyCredential")) {
try {
profileKeyCredential = new ProfileKeyCredential(Base64.decode(entry.get(
"profileKeyCredential").asText()));
} catch (InvalidInputException ignored) {
}
}
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
addresses.add(new SignalProfileEntry(serviceAddress, profileKey, lastUpdateTimestamp, profile));
addresses.add(new SignalProfileEntry(serviceAddress,
profileKey,
lastUpdateTimestamp,
profile,
profileKeyCredential));
}
}
@ -111,7 +132,9 @@ public class ProfileStore {
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
@Override
public void serialize(List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider) throws IOException {
public void serialize(
List<SignalProfileEntry> profiles, JsonGenerator json, SerializerProvider serializerProvider
) throws IOException {
json.writeStartArray();
for (SignalProfileEntry profileEntry : profiles) {
final SignalServiceAddress address = profileEntry.getServiceAddress();
@ -125,6 +148,10 @@ public class ProfileStore {
json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize()));
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
json.writeObjectField("profile", profileEntry.getProfile());
if (profileEntry.getProfileKeyCredential() != null) {
json.writeStringField("profileKeyCredential",
Base64.encodeBytes(profileEntry.getProfileKeyCredential().serialize()));
}
json.writeEndObject();
}
json.writeEndArray();

View file

@ -1,6 +1,7 @@
package org.asamk.signal.storage.profiles;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
public class SignalProfileEntry {
@ -13,11 +14,22 @@ public class SignalProfileEntry {
private final SignalProfile profile;
public SignalProfileEntry(final SignalServiceAddress serviceAddress, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile) {
private final ProfileKeyCredential profileKeyCredential;
private boolean requestPending;
public SignalProfileEntry(
final SignalServiceAddress serviceAddress,
final ProfileKey profileKey,
final long lastUpdateTimestamp,
final SignalProfile profile,
final ProfileKeyCredential profileKeyCredential
) {
this.serviceAddress = serviceAddress;
this.profileKey = profileKey;
this.lastUpdateTimestamp = lastUpdateTimestamp;
this.profile = profile;
this.profileKeyCredential = profileKeyCredential;
}
public SignalServiceAddress getServiceAddress() {
@ -35,4 +47,16 @@ public class SignalProfileEntry {
public SignalProfile getProfile() {
return profile;
}
public ProfileKeyCredential getProfileKeyCredential() {
return profileKeyCredential;
}
public boolean isRequestPending() {
return requestPending;
}
public void setRequestPending(final boolean requestPending) {
this.requestPending = requestPending;
}
}