import net.sourceforge.argparse4j.inf.Namespace;
import net.sourceforge.argparse4j.inf.Subparser;
+import org.asamk.signal.manager.GroupInviteLinkUrl;
import org.asamk.signal.manager.Manager;
import org.asamk.signal.storage.groups.GroupInfo;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
.map(SignalServiceAddress::getLegacyIdentifier)
.collect(Collectors.toSet());
+ final GroupInviteLinkUrl groupInviteLink = group.getGroupInviteLink();
+
System.out.println(String.format(
- "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s",
+ "Id: %s Name: %s Active: %s Blocked: %b Members: %s Pending members: %s Requesting members: %s Link: %s",
Base64.encodeBytes(group.groupId),
group.getTitle(),
group.isMember(m.getSelfAddress()),
group.isBlocked(),
members,
pendingMembers,
- requestingMembers));
+ requestingMembers,
+ groupInviteLink == null ? '-' : groupInviteLink.getUrl()));
} else {
System.out.println(String.format("Id: %s Name: %s Active: %s Blocked: %b",
Base64.encodeBytes(group.groupId),
--- /dev/null
+package org.asamk.signal.manager;
+
+import com.google.protobuf.ByteString;
+
+import org.signal.storageservice.protos.groups.GroupInviteLink;
+import org.signal.storageservice.protos.groups.local.DecryptedGroup;
+import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.groups.GroupMasterKey;
+import org.whispersystems.util.Base64UrlSafe;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public final class GroupInviteLinkUrl {
+
+ private static final String GROUP_URL_HOST = "signal.group";
+ private static final String GROUP_URL_PREFIX = "https://" + GROUP_URL_HOST + "/#";
+
+ private final GroupMasterKey groupMasterKey;
+ private final GroupLinkPassword password;
+ private final String url;
+
+ public static GroupInviteLinkUrl forGroup(GroupMasterKey groupMasterKey, DecryptedGroup group) {
+ return new GroupInviteLinkUrl(groupMasterKey,
+ GroupLinkPassword.fromBytes(group.getInviteLinkPassword().toByteArray()));
+ }
+
+ public static boolean isGroupLink(String urlString) {
+ return getGroupUrl(urlString) != null;
+ }
+
+ /**
+ * @return null iff not a group url.
+ * @throws InvalidGroupLinkException If group url, but cannot be parsed.
+ */
+ public static GroupInviteLinkUrl fromUri(String urlString) throws InvalidGroupLinkException, UnknownGroupLinkVersionException {
+ URI uri = getGroupUrl(urlString);
+
+ if (uri == null) {
+ return null;
+ }
+
+ try {
+ if (!"/".equals(uri.getPath()) && uri.getPath().length() > 0) {
+ throw new InvalidGroupLinkException("No path was expected in uri");
+ }
+
+ String encoding = uri.getFragment();
+
+ if (encoding == null || encoding.length() == 0) {
+ throw new InvalidGroupLinkException("No reference was in the uri");
+ }
+
+ byte[] bytes = Base64UrlSafe.decodePaddingAgnostic(encoding);
+ GroupInviteLink groupInviteLink = GroupInviteLink.parseFrom(bytes);
+
+ switch (groupInviteLink.getContentsCase()) {
+ case V1CONTENTS: {
+ GroupInviteLink.GroupInviteLinkContentsV1 groupInviteLinkContentsV1 = groupInviteLink.getV1Contents();
+ GroupMasterKey groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey()
+ .toByteArray());
+ GroupLinkPassword password = GroupLinkPassword.fromBytes(groupInviteLinkContentsV1.getInviteLinkPassword()
+ .toByteArray());
+
+ return new GroupInviteLinkUrl(groupMasterKey, password);
+ }
+ default:
+ throw new UnknownGroupLinkVersionException("Url contains no known group link content");
+ }
+ } catch (InvalidInputException | IOException e) {
+ throw new InvalidGroupLinkException(e);
+ }
+ }
+
+ /**
+ * @return {@link URI} if the host name matches.
+ */
+ private static URI getGroupUrl(String urlString) {
+ try {
+ URI url = new URI(urlString);
+
+ if (!"https".equalsIgnoreCase(url.getScheme()) && !"sgnl".equalsIgnoreCase(url.getScheme())) {
+ return null;
+ }
+
+ return GROUP_URL_HOST.equalsIgnoreCase(url.getHost()) ? url : null;
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ private GroupInviteLinkUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
+ this.groupMasterKey = groupMasterKey;
+ this.password = password;
+ this.url = createUrl(groupMasterKey, password);
+ }
+
+ protected static String createUrl(GroupMasterKey groupMasterKey, GroupLinkPassword password) {
+ GroupInviteLink groupInviteLink = GroupInviteLink.newBuilder()
+ .setV1Contents(GroupInviteLink.GroupInviteLinkContentsV1.newBuilder()
+ .setGroupMasterKey(ByteString.copyFrom(groupMasterKey.serialize()))
+ .setInviteLinkPassword(ByteString.copyFrom(password.serialize())))
+ .build();
+
+ String encoding = Base64UrlSafe.encodeBytesWithoutPadding(groupInviteLink.toByteArray());
+
+ return GROUP_URL_PREFIX + encoding;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public GroupMasterKey getGroupMasterKey() {
+ return groupMasterKey;
+ }
+
+ public GroupLinkPassword getPassword() {
+ return password;
+ }
+
+ public final static class InvalidGroupLinkException extends Exception {
+
+ public InvalidGroupLinkException(String message) {
+ super(message);
+ }
+
+ public InvalidGroupLinkException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ public final static class UnknownGroupLinkVersionException extends Exception {
+
+ public UnknownGroupLinkVersionException(String message) {
+ super(message);
+ }
+ }
+}
--- /dev/null
+package org.asamk.signal.manager;
+
+import java.util.Arrays;
+
+public final class GroupLinkPassword {
+
+ private static final int SIZE = 16;
+
+ private final byte[] bytes;
+
+ public static GroupLinkPassword createNew() {
+ return new GroupLinkPassword(KeyUtils.getSecretBytes(SIZE));
+ }
+
+ public static GroupLinkPassword fromBytes(byte[] bytes) {
+ return new GroupLinkPassword(bytes);
+ }
+
+ private GroupLinkPassword(byte[] bytes) {
+ this.bytes = bytes;
+ }
+
+ public byte[] serialize() {
+ return bytes.clone();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof GroupLinkPassword)) {
+ return false;
+ }
+
+ return Arrays.equals(bytes, ((GroupLinkPassword) other).bytes);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(bytes);
+ }
+}
package org.asamk.signal.storage.groups;
+import org.asamk.signal.manager.GroupInviteLinkUrl;
+import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
return this.group.getTitle();
}
+ @Override
+ public GroupInviteLinkUrl getGroupInviteLink() {
+ if (this.group == null || this.group.getInviteLinkPassword() == null || (
+ this.group.getAccessControl().getAddFromInviteLink() != AccessControl.AccessRequired.ANY
+ && this.group.getAccessControl().getAddFromInviteLink()
+ != AccessControl.AccessRequired.ADMINISTRATOR
+ )) {
+ return null;
+ }
+
+ return GroupInviteLinkUrl.forGroup(masterKey, group);
+ }
+
@Override
public Set<SignalServiceAddress> getMembers() {
if (this.group == null) {