mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-30 02:50:39 +00:00
Implement creating V2 Groups
This commit is contained in:
parent
d267974223
commit
4f2261e86f
19 changed files with 1157 additions and 356 deletions
|
@ -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();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue