]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/internal/ProvisioningManagerImpl.java
fcef536a6d8bb000b3cc3862b98b60ba97cbf78f
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / internal / ProvisioningManagerImpl.java
1 /*
2 Copyright (C) 2015-2022 AsamK and contributors
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 package org.asamk.signal.manager.internal;
18
19 import org.asamk.signal.manager.Manager;
20 import org.asamk.signal.manager.ProvisioningManager;
21 import org.asamk.signal.manager.Settings;
22 import org.asamk.signal.manager.api.DeviceLinkUrl;
23 import org.asamk.signal.manager.api.UserAlreadyExistsException;
24 import org.asamk.signal.manager.config.ServiceConfig;
25 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
26 import org.asamk.signal.manager.storage.SignalAccount;
27 import org.asamk.signal.manager.storage.accounts.AccountsStore;
28 import org.asamk.signal.manager.util.KeyUtils;
29 import org.signal.libsignal.protocol.IdentityKeyPair;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
33 import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
34 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
35 import org.whispersystems.signalservice.api.push.ServiceIdType;
36 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
37 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
38 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
39 import org.whispersystems.signalservice.internal.push.ProvisioningSocket;
40 import org.whispersystems.signalservice.internal.push.PushServiceSocket;
41 import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
42
43 import java.io.IOException;
44 import java.net.URI;
45 import java.nio.channels.OverlappingFileLockException;
46 import java.util.concurrent.TimeoutException;
47 import java.util.function.Consumer;
48
49 import static org.asamk.signal.manager.util.KeyUtils.generatePreKeysForType;
50
51 public class ProvisioningManagerImpl implements ProvisioningManager {
52
53 private static final Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
54
55 private final PathConfig pathConfig;
56 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
57 private final String userAgent;
58 private final Consumer<Manager> newManagerListener;
59 private final AccountsStore accountsStore;
60
61 private final SignalServiceAccountManager accountManager;
62 private final IdentityKeyPair tempIdentityKey;
63 private final String password;
64
65 public ProvisioningManagerImpl(
66 PathConfig pathConfig,
67 ServiceEnvironmentConfig serviceEnvironmentConfig,
68 String userAgent,
69 final Consumer<Manager> newManagerListener,
70 final AccountsStore accountsStore
71 ) {
72 this.pathConfig = pathConfig;
73 this.serviceEnvironmentConfig = serviceEnvironmentConfig;
74 this.userAgent = userAgent;
75 this.newManagerListener = newManagerListener;
76 this.accountsStore = accountsStore;
77
78 tempIdentityKey = KeyUtils.generateIdentityKeyPair();
79 password = KeyUtils.createPassword();
80 final var clientZkOperations = ClientZkOperations.create(serviceEnvironmentConfig.signalServiceConfiguration());
81 final var groupsV2Operations = new GroupsV2Operations(clientZkOperations, ServiceConfig.GROUP_MAX_SIZE);
82 final var credentialsProvider = new DynamicCredentialsProvider(null,
83 null,
84 null,
85 password,
86 SignalServiceAddress.DEFAULT_DEVICE_ID);
87 final var pushServiceSocket = new PushServiceSocket(serviceEnvironmentConfig.signalServiceConfiguration(),
88 credentialsProvider,
89 userAgent,
90 clientZkOperations.getProfileOperations(),
91 ServiceConfig.AUTOMATIC_NETWORK_RETRY);
92 accountManager = new SignalServiceAccountManager(pushServiceSocket,
93 new ProvisioningSocket(serviceEnvironmentConfig.signalServiceConfiguration(), userAgent),
94 groupsV2Operations);
95 }
96
97 @Override
98 public URI getDeviceLinkUri() throws TimeoutException, IOException {
99 var deviceUuid = accountManager.getNewDeviceUuid();
100
101 return new DeviceLinkUrl(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
102 }
103
104 @Override
105 public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
106 var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
107 var number = ret.getNumber();
108 var aci = ret.getAci();
109 var pni = ret.getPni();
110
111 logger.info("Received link information from {}, linking in progress ...", number);
112
113 var accountPath = accountsStore.getPathByAci(aci);
114 if (accountPath == null) {
115 accountPath = accountsStore.getPathByNumber(number);
116 }
117 final var accountExists = accountPath != null && SignalAccount.accountFileExists(pathConfig.dataPath(),
118 accountPath);
119 if (accountExists && !canRelinkExistingAccount(accountPath)) {
120 throw new UserAlreadyExistsException(number, SignalAccount.getFileName(pathConfig.dataPath(), accountPath));
121 }
122 if (accountPath == null) {
123 accountPath = accountsStore.addAccount(number, aci);
124 } else {
125 accountsStore.updateAccount(accountPath, number, aci);
126 }
127
128 var encryptedDeviceName = deviceName == null
129 ? null
130 : DeviceNameUtil.encryptDeviceName(deviceName, ret.getAciIdentity().getPrivateKey());
131 // Create new account with the synced identity
132 var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
133
134 SignalAccount account = null;
135 try {
136 if (!accountExists) {
137 account = SignalAccount.createLinkedAccount(pathConfig.dataPath(),
138 accountPath,
139 serviceEnvironmentConfig.type(),
140 Settings.DEFAULT);
141 } else {
142 account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, Settings.DEFAULT);
143 }
144
145 account.setProvisioningData(number,
146 aci,
147 pni,
148 password,
149 encryptedDeviceName,
150 ret.getAciIdentity(),
151 ret.getPniIdentity(),
152 profileKey,
153 ret.getMasterKey(),
154 ret.getAccountEntropyPool(),
155 ret.getMediaRootBackupKey());
156
157 account.getConfigurationStore().setReadReceipts(ret.isReadReceipts());
158
159 final var aciPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.ACI));
160 final var pniPreKeys = generatePreKeysForType(account.getAccountData(ServiceIdType.PNI));
161
162 logger.debug("Finishing new device registration");
163 var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
164 account.getAccountAttributes(null),
165 aciPreKeys,
166 pniPreKeys);
167
168 account.finishLinking(deviceId, aciPreKeys, pniPreKeys);
169
170 ManagerImpl m = null;
171 try {
172 m = new ManagerImpl(account,
173 pathConfig,
174 new AccountFileUpdaterImpl(accountsStore, accountPath),
175 serviceEnvironmentConfig,
176 userAgent);
177 account = null;
178
179 logger.debug("Refreshing pre keys");
180 try {
181 m.refreshPreKeys();
182 } catch (Exception e) {
183 logger.error("Failed to refresh pre keys.", e);
184 }
185
186 logger.debug("Requesting sync data");
187 try {
188 m.requestAllSyncData();
189 } catch (Exception e) {
190 logger.error(
191 "Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.",
192 e);
193 }
194
195 if (newManagerListener != null) {
196 newManagerListener.accept(m);
197 m = null;
198 }
199 return number;
200 } finally {
201 if (m != null) {
202 m.close();
203 }
204 }
205 } finally {
206 if (account != null) {
207 account.close();
208 }
209 }
210 }
211
212 private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
213 final SignalAccount signalAccount;
214 try {
215 signalAccount = SignalAccount.load(pathConfig.dataPath(), accountPath, false, Settings.DEFAULT);
216 } catch (IOException e) {
217 logger.debug("Account in use or failed to load.", e);
218 return false;
219 } catch (OverlappingFileLockException e) {
220 logger.debug("Account in use.", e);
221 return false;
222 }
223
224 try (signalAccount) {
225 if (signalAccount.isPrimaryDevice()) {
226 logger.debug("Account is a primary device.");
227 return false;
228 }
229 if (signalAccount.isRegistered()
230 && signalAccount.getServiceEnvironment() != null
231 && signalAccount.getServiceEnvironment() != serviceEnvironmentConfig.type()) {
232 logger.debug("Account is registered in another environment: {}.",
233 signalAccount.getServiceEnvironment());
234 return false;
235 }
236
237 final var m = new ManagerImpl(signalAccount,
238 pathConfig,
239 new AccountFileUpdaterImpl(accountsStore, accountPath),
240 serviceEnvironmentConfig,
241 userAgent);
242 try (m) {
243 m.checkAccountState();
244 } catch (AuthorizationFailedException ignored) {
245 return true;
246 }
247
248 logger.debug("Account is still successfully linked.");
249 return false;
250 }
251 }
252 }