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()));
30 * @return null iff not a group url.
31 * @throws InvalidGroupLinkException If group url, but cannot be parsed.
33 public static GroupInviteLinkUrl
fromUri(String urlString
) throws InvalidGroupLinkException
, UnknownGroupLinkVersionException
{
34 var uri
= getGroupUrl(urlString
);
41 if (!"/".equals(uri
.getPath()) && uri
.getPath().length() > 0) {
42 throw new InvalidGroupLinkException("No path was expected in uri");
45 var encoding
= uri
.getFragment();
47 if (encoding
== null || encoding
.length() == 0) {
48 throw new InvalidGroupLinkException("No reference was in the uri");
51 var bytes
= Base64UrlSafe
.decodePaddingAgnostic(encoding
);
52 var groupInviteLink
= GroupInviteLink
.parseFrom(bytes
);
54 switch (groupInviteLink
.getContentsCase()) {
56 var groupInviteLinkContentsV1
= groupInviteLink
.getV1Contents();
57 var groupMasterKey
= new GroupMasterKey(groupInviteLinkContentsV1
.getGroupMasterKey()
59 var password
= GroupLinkPassword
.fromBytes(groupInviteLinkContentsV1
.getInviteLinkPassword()
62 return new GroupInviteLinkUrl(groupMasterKey
, password
);
64 default -> throw new UnknownGroupLinkVersionException("Url contains no known group link content");
66 } catch (InvalidInputException
| IOException e
) {
67 throw new InvalidGroupLinkException(e
);
72 * @return {@link URI} if the host name matches.
74 private static URI
getGroupUrl(String urlString
) {
76 var url
= new URI(urlString
);
78 if (!"https".equalsIgnoreCase(url
.getScheme()) && !"sgnl".equalsIgnoreCase(url
.getScheme())) {
82 return GROUP_URL_HOST
.equalsIgnoreCase(url
.getHost()) ? url
: null;
83 } catch (URISyntaxException e
) {
88 private GroupInviteLinkUrl(GroupMasterKey groupMasterKey
, GroupLinkPassword password
) {
89 this.groupMasterKey
= groupMasterKey
;
90 this.password
= password
;
91 this.url
= createUrl(groupMasterKey
, password
);
94 private static String
createUrl(GroupMasterKey groupMasterKey
, GroupLinkPassword password
) {
95 var groupInviteLink
= GroupInviteLink
.newBuilder()
96 .setV1Contents(GroupInviteLink
.GroupInviteLinkContentsV1
.newBuilder()
97 .setGroupMasterKey(ByteString
.copyFrom(groupMasterKey
.serialize()))
98 .setInviteLinkPassword(ByteString
.copyFrom(password
.serialize())))
101 var encoding
= Base64UrlSafe
.encodeBytesWithoutPadding(groupInviteLink
.toByteArray());
103 return GROUP_URL_PREFIX
+ encoding
;
106 public String
getUrl() {
110 public GroupMasterKey
getGroupMasterKey() {
111 return groupMasterKey
;
114 public GroupLinkPassword
getPassword() {
118 public final static class InvalidGroupLinkException
extends Exception
{
120 public InvalidGroupLinkException(String message
) {
124 public InvalidGroupLinkException(Throwable cause
) {
129 public final static class UnknownGroupLinkVersionException
extends Exception
{
131 public UnknownGroupLinkVersionException(String message
) {