1 package org
.asamk
.signal
.manager
.groups
;
3 import com
.google
.protobuf
.ByteString
;
5 import org
.signal
.storageservice
.protos
.groups
.GroupInviteLink
;
6 import org
.signal
.storageservice
.protos
.groups
.local
.DecryptedGroup
;
7 import org
.signal
.zkgroup
.InvalidInputException
;
8 import org
.signal
.zkgroup
.groups
.GroupMasterKey
;
9 import org
.whispersystems
.util
.Base64UrlSafe
;
11 import java
.io
.IOException
;
13 import java
.net
.URISyntaxException
;
15 public final class GroupInviteLinkUrl
{
17 private static final String GROUP_URL_HOST
= "signal.group";
18 private static final String GROUP_URL_PREFIX
= "https://" + GROUP_URL_HOST
+ "/#";
20 private final GroupMasterKey groupMasterKey
;
21 private final GroupLinkPassword password
;
22 private final String url
;
24 public static GroupInviteLinkUrl
forGroup(GroupMasterKey groupMasterKey
, DecryptedGroup group
) {
25 return new GroupInviteLinkUrl(groupMasterKey
,
26 GroupLinkPassword
.fromBytes(group
.getInviteLinkPassword().toByteArray()));
29 public static boolean isGroupLink(String urlString
) {
30 return getGroupUrl(urlString
) != null;
34 * @return null iff not a group url.
35 * @throws InvalidGroupLinkException If group url, but cannot be parsed.
37 public static GroupInviteLinkUrl
fromUri(String urlString
) throws InvalidGroupLinkException
, UnknownGroupLinkVersionException
{
38 var uri
= getGroupUrl(urlString
);
45 if (!"/".equals(uri
.getPath()) && uri
.getPath().length() > 0) {
46 throw new InvalidGroupLinkException("No path was expected in uri");
49 var encoding
= uri
.getFragment();
51 if (encoding
== null || encoding
.length() == 0) {
52 throw new InvalidGroupLinkException("No reference was in the uri");
55 var bytes
= Base64UrlSafe
.decodePaddingAgnostic(encoding
);
56 var groupInviteLink
= GroupInviteLink
.parseFrom(bytes
);
58 switch (groupInviteLink
.getContentsCase()) {
60 var groupInviteLinkContentsV1
= groupInviteLink
.getV1Contents();
61 var groupMasterKey
= new GroupMasterKey(groupInviteLinkContentsV1
.getGroupMasterKey()
63 var password
= GroupLinkPassword
.fromBytes(groupInviteLinkContentsV1
.getInviteLinkPassword()
66 return new GroupInviteLinkUrl(groupMasterKey
, password
);
68 default -> throw new UnknownGroupLinkVersionException("Url contains no known group link content");
70 } catch (InvalidInputException
| IOException e
) {
71 throw new InvalidGroupLinkException(e
);
76 * @return {@link URI} if the host name matches.
78 private static URI
getGroupUrl(String urlString
) {
80 var url
= new URI(urlString
);
82 if (!"https".equalsIgnoreCase(url
.getScheme()) && !"sgnl".equalsIgnoreCase(url
.getScheme())) {
86 return GROUP_URL_HOST
.equalsIgnoreCase(url
.getHost()) ? url
: null;
87 } catch (URISyntaxException e
) {
92 private GroupInviteLinkUrl(GroupMasterKey groupMasterKey
, GroupLinkPassword password
) {
93 this.groupMasterKey
= groupMasterKey
;
94 this.password
= password
;
95 this.url
= createUrl(groupMasterKey
, password
);
98 protected static String
createUrl(GroupMasterKey groupMasterKey
, GroupLinkPassword password
) {
99 var groupInviteLink
= GroupInviteLink
.newBuilder()
100 .setV1Contents(GroupInviteLink
.GroupInviteLinkContentsV1
.newBuilder()
101 .setGroupMasterKey(ByteString
.copyFrom(groupMasterKey
.serialize()))
102 .setInviteLinkPassword(ByteString
.copyFrom(password
.serialize())))
105 var encoding
= Base64UrlSafe
.encodeBytesWithoutPadding(groupInviteLink
.toByteArray());
107 return GROUP_URL_PREFIX
+ encoding
;
110 public String
getUrl() {
114 public GroupMasterKey
getGroupMasterKey() {
115 return groupMasterKey
;
118 public GroupLinkPassword
getPassword() {
122 public final static class InvalidGroupLinkException
extends Exception
{
124 public InvalidGroupLinkException(String message
) {
128 public InvalidGroupLinkException(Throwable cause
) {
133 public final static class UnknownGroupLinkVersionException
extends Exception
{
135 public UnknownGroupLinkVersionException(String message
) {