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

@ -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();
}