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