1 /*
2  * Copyright (C) 2021 Finn Herzfeld
3  *
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 package io.finn.signald.clientprotocol.v1;
19 
20 import io.finn.signald.GroupInviteLinkUrl;
21 import io.finn.signald.GroupsV2Manager;
22 import io.finn.signald.Manager;
23 import io.finn.signald.annotations.Doc;
24 import io.finn.signald.annotations.ExampleValue;
25 import io.finn.signald.annotations.ProtocolType;
26 import io.finn.signald.annotations.Required;
27 import io.finn.signald.clientprotocol.Request;
28 import io.finn.signald.clientprotocol.RequestType;
29 import io.finn.signald.clientprotocol.v1.exceptions.*;
30 import io.finn.signald.clientprotocol.v1.exceptions.InternalError;
31 import io.finn.signald.storage.AccountData;
32 import io.finn.signald.storage.Group;
33 import io.finn.signald.util.GroupsUtil;
34 import java.io.IOException;
35 import java.sql.SQLException;
36 import java.util.concurrent.ExecutionException;
37 import java.util.concurrent.TimeoutException;
38 import org.signal.storageservice.protos.groups.AccessControl;
39 import org.signal.storageservice.protos.groups.GroupChange;
40 import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
41 import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
42 import org.signal.zkgroup.VerificationFailedException;
43 import org.signal.zkgroup.groups.GroupSecretParams;
44 import org.signal.zkgroup.profiles.ProfileKeyCredential;
45 import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
46 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
47 import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
48 import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
49 import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
50 import org.whispersystems.signalservice.api.util.UuidUtil;
51 
52 @ProtocolType("join_group")
53 @Doc("Join a group using the a signal.group URL. Note that you must have a profile name set to join groups.")
54 public class JoinGroupRequest implements RequestType<JsonGroupJoinInfo> {
55   @ExampleValue(ExampleValue.LOCAL_PHONE_NUMBER) @Doc("The account to interact with") @Required public String account;
56 
57   @ExampleValue(ExampleValue.GROUP_JOIN_URI) @Doc("The signal.group URL") @Required public String uri;
58 
59   @Override
run(Request request)60   public JsonGroupJoinInfo run(Request request) throws InvalidRequestError, InvalidInviteURIError, InternalError, InvalidProxyError, ServerNotFoundError, NoSuchAccountError,
61                                                        OwnProfileKeyDoesNotExistError, GroupVerificationError, GroupNotActiveError, UnknownGroupError, InvalidGroupStateError {
62     GroupInviteLinkUrl groupInviteLinkUrl;
63     try {
64       groupInviteLinkUrl = GroupInviteLinkUrl.fromUri(uri);
65     } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) {
66       throw new InvalidRequestError(e.getMessage());
67     }
68     if (groupInviteLinkUrl == null) {
69       throw new InvalidInviteURIError();
70     }
71     GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInviteLinkUrl.getGroupMasterKey());
72 
73     Manager m = Common.getManager(account);
74     ProfileKeyCredential profileKeyCredential;
75     try {
76       profileKeyCredential = m.getRecipientProfileKeyCredential(m.getOwnRecipient()).getProfileKeyCredential();
77     } catch (InterruptedException | ExecutionException | TimeoutException | IOException | SQLException e) {
78       throw new InternalError("error getting own profile key credential", e);
79     }
80 
81     if (profileKeyCredential == null) {
82       throw new OwnProfileKeyDoesNotExistError();
83     }
84 
85     GroupsV2Operations.GroupOperations groupOperations = GroupsUtil.GetGroupsV2Operations(m.getServiceConfiguration()).forGroup(groupSecretParams);
86     GroupsV2Manager groupsV2Manager = m.getGroupsV2Manager();
87     DecryptedGroupJoinInfo groupJoinInfo;
88     try {
89       groupJoinInfo = groupsV2Manager.getGroupJoinInfo(groupSecretParams, groupInviteLinkUrl.getPassword().serialize());
90     } catch (IOException e) {
91       throw new InternalError("error getting group join info", e);
92     } catch (VerificationFailedException e) {
93       throw new GroupVerificationError(e);
94     } catch (GroupLinkNotActiveException e) {
95       throw new GroupNotActiveError(e);
96     }
97 
98     boolean requestToJoin = groupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
99     GroupChange.Actions.Builder change = requestToJoin ? groupOperations.createGroupJoinRequest(profileKeyCredential) : groupOperations.createGroupJoinDirect(profileKeyCredential);
100     change.setSourceUuid(UuidUtil.toByteString(m.getUUID()));
101 
102     int revision = groupJoinInfo.getRevision() + 1;
103 
104     DecryptedGroupChange decryptedChange;
105     GroupChange groupChange;
106     try {
107       groupChange = groupsV2Manager.commitJoinChangeWithConflictResolution(revision, change, groupSecretParams, groupInviteLinkUrl.getPassword().serialize());
108       decryptedChange = groupOperations.decryptChange(groupChange, false).get();
109     } catch (IOException e) {
110       throw new InternalError("error committing group join change", e);
111     } catch (VerificationFailedException e) {
112       throw new GroupVerificationError(e);
113     } catch (GroupLinkNotActiveException e) {
114       throw new GroupNotActiveError(e);
115     } catch (InvalidGroupStateException e) {
116       throw new InvalidGroupStateError(e);
117     }
118 
119     Group group;
120     try {
121       group = groupsV2Manager.getGroup(groupSecretParams, decryptedChange.getRevision());
122     } catch (IOException e) {
123       throw new InternalError("error getting new group information after join", e);
124     } catch (VerificationFailedException e) {
125       throw new GroupVerificationError(e);
126     } catch (InvalidGroupStateException e) {
127       throw new InvalidGroupStateError(e);
128     }
129 
130     if (group == null) {
131       throw new UnknownGroupError();
132     }
133 
134     SignalServiceGroupV2.Builder groupBuilder = SignalServiceGroupV2.newBuilder(group.getMasterKey()).withRevision(revision).withSignedGroupChange(groupChange.toByteArray());
135     SignalServiceDataMessage.Builder updateMessage = SignalServiceDataMessage.newBuilder().asGroupMessage(groupBuilder.build()).withExpiration(group.getTimer());
136     try {
137       m.sendGroupV2Message(updateMessage, group.getSignalServiceGroupV2());
138     } catch (io.finn.signald.exceptions.UnknownGroupException e) {
139       throw new UnknownGroupError();
140     } catch (SQLException | IOException e) {
141       throw new InternalError("error sending group update message", e);
142     }
143 
144     AccountData accountData = m.getAccountData();
145     accountData.groupsV2.update(group);
146     Common.saveAccount(accountData);
147 
148     return new JsonGroupJoinInfo(groupJoinInfo, groupInviteLinkUrl.getGroupMasterKey());
149   }
150 }
151