mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-28 18:10:38 +00:00
Implement creating V2 Groups
This commit is contained in:
parent
d267974223
commit
4f2261e86f
19 changed files with 1157 additions and 356 deletions
13
.idea/codeStyles/Project.xml
generated
13
.idea/codeStyles/Project.xml
generated
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
106
src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Normal file
106
src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
|
||||
|
||||
public interface MessagePipeProvider {
|
||||
|
||||
SignalServiceMessagePipe getMessagePipe(boolean unidentified);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
|
||||
|
||||
public interface MessageReceiverProvider {
|
||||
|
||||
SignalServiceMessageReceiver getMessageReceiver();
|
||||
}
|
135
src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
Normal file
135
src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public interface SelfAddressProvider {
|
||||
|
||||
SignalServiceAddress getSelfAddress();
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
|
||||
public interface SelfProfileKeyProvider {
|
||||
|
||||
ProfileKey getProfileKey();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package org.asamk.signal.manager.helper;
|
||||
|
||||
public interface UnidentifiedAccessSenderCertificateProvider {
|
||||
|
||||
byte[] getSenderCertificate();
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue