Implement creating V2 Groups
This commit is contained in:
parent
d267974223
commit
4f2261e86f
13
.idea/codeStyles/Project.xml
generated
13
.idea/codeStyles/Project.xml
generated
@ -50,10 +50,23 @@
|
|||||||
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
<option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" />
|
||||||
</XML>
|
</XML>
|
||||||
<codeStyleSettings language="JAVA">
|
<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_DECLARATIONS" value="1" />
|
||||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
||||||
<option name="BLANK_LINES_AFTER_CLASS_HEADER" 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>
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
<arrangement>
|
<arrangement>
|
||||||
|
@ -44,7 +44,10 @@ public class DbusSignalImpl implements Signal {
|
|||||||
return sendMessage(message, attachments, recipients);
|
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);
|
List<String> errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results);
|
||||||
if (errors.size() == 0) {
|
if (errors.size() == 0) {
|
||||||
return;
|
return;
|
||||||
@ -164,13 +167,29 @@ public class DbusSignalImpl implements Signal {
|
|||||||
if (group == null) {
|
if (group == null) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
} else {
|
} 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
|
@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 {
|
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);
|
final Pair<byte[], List<SendMessageResult>> results = m.updateGroup(groupId, name, members, avatar);
|
||||||
checkSendMessageResults(0, results.second());
|
checkSendMessageResults(0, results.second());
|
||||||
return results.first();
|
return results.first();
|
||||||
|
@ -30,10 +30,6 @@ class KeyUtils {
|
|||||||
return getSecretBytes(16);
|
return getSecretBytes(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
static byte[] createUnrestrictedUnidentifiedAccess() {
|
|
||||||
return getSecretBytes(16);
|
|
||||||
}
|
|
||||||
|
|
||||||
static byte[] createStickerUploadKey() {
|
static byte[] createStickerUploadKey() {
|
||||||
return getSecretBytes(32);
|
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) {
|
public void updateGroup(GroupInfo group) {
|
||||||
groups.put(Base64.encodeBytes(group.groupId), group);
|
groups.put(Base64.encodeBytes(group.groupId), group);
|
||||||
if (group instanceof GroupInfoV2) {
|
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() != null) {
|
||||||
try {
|
try {
|
||||||
IOUtils.createPrivateDirectories(groupCachePath);
|
IOUtils.createPrivateDirectories(groupCachePath);
|
||||||
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) {
|
try (FileOutputStream stream = new FileOutputStream(getGroupFile(group.groupId))) {
|
||||||
@ -103,7 +103,11 @@ public class JsonGroupStore {
|
|||||||
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
|
private static class GroupsSerializer extends JsonSerializer<Map<String, GroupInfo>> {
|
||||||
|
|
||||||
@Override
|
@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();
|
final Collection<GroupInfo> groups = value.values();
|
||||||
jgen.writeStartArray(groups.size());
|
jgen.writeStartArray(groups.size());
|
||||||
for (GroupInfo group : groups) {
|
for (GroupInfo group : groups) {
|
||||||
@ -127,7 +131,10 @@ public class JsonGroupStore {
|
|||||||
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
|
private static class GroupsDeserializer extends JsonDeserializer<Map<String, GroupInfo>> {
|
||||||
|
|
||||||
@Override
|
@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<>();
|
Map<String, GroupInfo> groups = new HashMap<>();
|
||||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||||
for (JsonNode n : node) {
|
for (JsonNode n : node) {
|
||||||
|
@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
|||||||
|
|
||||||
import org.signal.zkgroup.InvalidInputException;
|
import org.signal.zkgroup.InvalidInputException;
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
import org.whispersystems.util.Base64;
|
import org.whispersystems.util.Base64;
|
||||||
@ -32,7 +33,7 @@ public class ProfileStore {
|
|||||||
@JsonSerialize(using = ProfileStoreSerializer.class)
|
@JsonSerialize(using = ProfileStoreSerializer.class)
|
||||||
private final List<SignalProfileEntry> profiles = new ArrayList<>();
|
private final List<SignalProfileEntry> profiles = new ArrayList<>();
|
||||||
|
|
||||||
public SignalProfileEntry getProfile(SignalServiceAddress serviceAddress) {
|
public SignalProfileEntry getProfileEntry(SignalServiceAddress serviceAddress) {
|
||||||
for (SignalProfileEntry entry : profiles) {
|
for (SignalProfileEntry entry : profiles) {
|
||||||
if (entry.getServiceAddress().matches(serviceAddress)) {
|
if (entry.getServiceAddress().matches(serviceAddress)) {
|
||||||
return entry;
|
return entry;
|
||||||
@ -50,8 +51,18 @@ public class ProfileStore {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateProfile(SignalServiceAddress serviceAddress, ProfileKey profileKey, long now, SignalProfile profile) {
|
public void updateProfile(
|
||||||
SignalProfileEntry newEntry = new SignalProfileEntry(serviceAddress, profileKey, now, profile);
|
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++) {
|
for (int i = 0; i < profiles.size(); i++) {
|
||||||
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
||||||
profiles.set(i, newEntry);
|
profiles.set(i, newEntry);
|
||||||
@ -63,7 +74,7 @@ public class ProfileStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void storeProfileKey(SignalServiceAddress serviceAddress, ProfileKey profileKey) {
|
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++) {
|
for (int i = 0; i < profiles.size(); i++) {
|
||||||
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
if (profiles.get(i).getServiceAddress().matches(serviceAddress)) {
|
||||||
if (!profiles.get(i).getProfileKey().equals(profileKey)) {
|
if (!profiles.get(i).getProfileKey().equals(profileKey)) {
|
||||||
@ -79,28 +90,38 @@ public class ProfileStore {
|
|||||||
public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
|
public static class ProfileStoreDeserializer extends JsonDeserializer<List<SignalProfileEntry>> {
|
||||||
|
|
||||||
@Override
|
@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);
|
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||||
|
|
||||||
List<SignalProfileEntry> addresses = new ArrayList<>();
|
List<SignalProfileEntry> addresses = new ArrayList<>();
|
||||||
|
|
||||||
if (node.isArray()) {
|
if (node.isArray()) {
|
||||||
for (JsonNode entry : node) {
|
for (JsonNode entry : node) {
|
||||||
String name = entry.hasNonNull("name")
|
String name = entry.hasNonNull("name") ? entry.get("name").asText() : null;
|
||||||
? entry.get("name").asText()
|
UUID uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null;
|
||||||
: null;
|
|
||||||
UUID uuid = entry.hasNonNull("uuid")
|
|
||||||
? UuidUtil.parseOrNull(entry.get("uuid").asText())
|
|
||||||
: null;
|
|
||||||
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name);
|
final SignalServiceAddress serviceAddress = new SignalServiceAddress(uuid, name);
|
||||||
ProfileKey profileKey = null;
|
ProfileKey profileKey = null;
|
||||||
try {
|
try {
|
||||||
profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText()));
|
profileKey = new ProfileKey(Base64.decode(entry.get("profileKey").asText()));
|
||||||
} catch (InvalidInputException ignored) {
|
} 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();
|
long lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong();
|
||||||
SignalProfile profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class);
|
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>> {
|
public static class ProfileStoreSerializer extends JsonSerializer<List<SignalProfileEntry>> {
|
||||||
|
|
||||||
@Override
|
@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();
|
json.writeStartArray();
|
||||||
for (SignalProfileEntry profileEntry : profiles) {
|
for (SignalProfileEntry profileEntry : profiles) {
|
||||||
final SignalServiceAddress address = profileEntry.getServiceAddress();
|
final SignalServiceAddress address = profileEntry.getServiceAddress();
|
||||||
@ -125,6 +148,10 @@ public class ProfileStore {
|
|||||||
json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize()));
|
json.writeStringField("profileKey", Base64.encodeBytes(profileEntry.getProfileKey().serialize()));
|
||||||
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
|
json.writeNumberField("lastUpdateTimestamp", profileEntry.getLastUpdateTimestamp());
|
||||||
json.writeObjectField("profile", profileEntry.getProfile());
|
json.writeObjectField("profile", profileEntry.getProfile());
|
||||||
|
if (profileEntry.getProfileKeyCredential() != null) {
|
||||||
|
json.writeStringField("profileKeyCredential",
|
||||||
|
Base64.encodeBytes(profileEntry.getProfileKeyCredential().serialize()));
|
||||||
|
}
|
||||||
json.writeEndObject();
|
json.writeEndObject();
|
||||||
}
|
}
|
||||||
json.writeEndArray();
|
json.writeEndArray();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.asamk.signal.storage.profiles;
|
package org.asamk.signal.storage.profiles;
|
||||||
|
|
||||||
import org.signal.zkgroup.profiles.ProfileKey;
|
import org.signal.zkgroup.profiles.ProfileKey;
|
||||||
|
import org.signal.zkgroup.profiles.ProfileKeyCredential;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
|
|
||||||
public class SignalProfileEntry {
|
public class SignalProfileEntry {
|
||||||
@ -13,11 +14,22 @@ public class SignalProfileEntry {
|
|||||||
|
|
||||||
private final SignalProfile profile;
|
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.serviceAddress = serviceAddress;
|
||||||
this.profileKey = profileKey;
|
this.profileKey = profileKey;
|
||||||
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
this.lastUpdateTimestamp = lastUpdateTimestamp;
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
|
this.profileKeyCredential = profileKeyCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SignalServiceAddress getServiceAddress() {
|
public SignalServiceAddress getServiceAddress() {
|
||||||
@ -35,4 +47,16 @@ public class SignalProfileEntry {
|
|||||||
public SignalProfile getProfile() {
|
public SignalProfile getProfile() {
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProfileKeyCredential getProfileKeyCredential() {
|
||||||
|
return profileKeyCredential;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRequestPending() {
|
||||||
|
return requestPending;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRequestPending(final boolean requestPending) {
|
||||||
|
this.requestPending = requestPending;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user