]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java
Implement multi account commands for dbus client
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / RegistrationManagerImpl.java
1 /*
2 Copyright (C) 2015-2021 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;
18
19 import org.asamk.signal.manager.api.CaptchaRequiredException;
20 import org.asamk.signal.manager.api.IncorrectPinException;
21 import org.asamk.signal.manager.api.PinLockedException;
22 import org.asamk.signal.manager.config.ServiceConfig;
23 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
24 import org.asamk.signal.manager.helper.PinHelper;
25 import org.asamk.signal.manager.storage.SignalAccount;
26 import org.asamk.signal.manager.util.Utils;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
29 import org.whispersystems.libsignal.util.guava.Optional;
30 import org.whispersystems.signalservice.api.KbsPinData;
31 import org.whispersystems.signalservice.api.KeyBackupServicePinException;
32 import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
33 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
34 import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
35 import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
36 import org.whispersystems.signalservice.api.kbs.MasterKey;
37 import org.whispersystems.signalservice.api.push.ACI;
38 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
39 import org.whispersystems.signalservice.internal.ServiceResponse;
40 import org.whispersystems.signalservice.internal.push.LockedException;
41 import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
42 import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
43 import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
44
45 import java.io.IOException;
46 import java.util.function.Consumer;
47
48 import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
49
50 public class RegistrationManagerImpl implements RegistrationManager {
51
52 private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
53
54 private SignalAccount account;
55 private final PathConfig pathConfig;
56 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
57 private final String userAgent;
58 private final Consumer<Manager> newManagerListener;
59
60 private final SignalServiceAccountManager accountManager;
61 private final PinHelper pinHelper;
62
63 RegistrationManagerImpl(
64 SignalAccount account,
65 PathConfig pathConfig,
66 ServiceEnvironmentConfig serviceEnvironmentConfig,
67 String userAgent,
68 Consumer<Manager> newManagerListener
69 ) {
70 this.account = account;
71 this.pathConfig = pathConfig;
72 this.serviceEnvironmentConfig = serviceEnvironmentConfig;
73 this.userAgent = userAgent;
74 this.newManagerListener = newManagerListener;
75
76 GroupsV2Operations groupsV2Operations;
77 try {
78 groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
79 } catch (Throwable ignored) {
80 groupsV2Operations = null;
81 }
82 this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
83 new DynamicCredentialsProvider(
84 // Using empty UUID, because registering doesn't work otherwise
85 null, account.getAccount(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
86 userAgent,
87 groupsV2Operations,
88 ServiceConfig.AUTOMATIC_NETWORK_RETRY);
89 final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
90 serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
91 serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
92 serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
93 10);
94 this.pinHelper = new PinHelper(keyBackupService);
95 }
96
97 @Override
98 public void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException {
99 captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
100 if (account.getAci() != null) {
101 try {
102 final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
103 new DynamicCredentialsProvider(account.getAci(),
104 account.getAccount(),
105 account.getPassword(),
106 account.getDeviceId()),
107 userAgent,
108 null,
109 ServiceConfig.AUTOMATIC_NETWORK_RETRY);
110 accountManager.setAccountAttributes(account.getEncryptedDeviceName(),
111 null,
112 account.getLocalRegistrationId(),
113 true,
114 null,
115 account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
116 account.getSelfUnidentifiedAccessKey(),
117 account.isUnrestrictedUnidentifiedAccess(),
118 capabilities,
119 account.isDiscoverableByPhoneNumber());
120 account.setRegistered(true);
121 logger.info("Reactivated existing account, verify is not necessary.");
122 if (newManagerListener != null) {
123 final var m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
124 account = null;
125 newManagerListener.accept(m);
126 }
127 return;
128 } catch (IOException e) {
129 logger.debug("Failed to reactivate account");
130 }
131 }
132 final ServiceResponse<RequestVerificationCodeResponse> response;
133 if (voiceVerification) {
134 response = accountManager.requestVoiceVerificationCode(Utils.getDefaultLocale(),
135 Optional.fromNullable(captcha),
136 Optional.absent(),
137 Optional.absent());
138 } else {
139 response = accountManager.requestSmsVerificationCode(false,
140 Optional.fromNullable(captcha),
141 Optional.absent(),
142 Optional.absent());
143 }
144 try {
145 handleResponseException(response);
146 } catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
147 throw new CaptchaRequiredException(e.getMessage(), e);
148 }
149 }
150
151 @Override
152 public void verifyAccount(
153 String verificationCode, String pin
154 ) throws IOException, PinLockedException, IncorrectPinException {
155 verificationCode = verificationCode.replace("-", "");
156 VerifyAccountResponse response;
157 MasterKey masterKey;
158 try {
159 response = verifyAccountWithCode(verificationCode, null);
160
161 masterKey = null;
162 pin = null;
163 } catch (LockedException e) {
164 if (pin == null) {
165 throw new PinLockedException(e.getTimeRemaining());
166 }
167
168 KbsPinData registrationLockData;
169 try {
170 registrationLockData = pinHelper.getRegistrationLockData(pin, e);
171 } catch (KeyBackupSystemNoDataException ex) {
172 throw new IOException(e);
173 } catch (KeyBackupServicePinException ex) {
174 throw new IncorrectPinException(ex.getTriesRemaining());
175 }
176 if (registrationLockData == null) {
177 throw e;
178 }
179
180 var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
181 try {
182 response = verifyAccountWithCode(verificationCode, registrationLock);
183 } catch (LockedException _e) {
184 throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
185 }
186 masterKey = registrationLockData.getMasterKey();
187 }
188
189 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
190 account.finishRegistration(ACI.parseOrNull(response.getUuid()), masterKey, pin);
191
192 ManagerImpl m = null;
193 try {
194 m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
195 account = null;
196
197 m.refreshPreKeys();
198 if (response.isStorageCapable()) {
199 m.retrieveRemoteStorage();
200 }
201 // Set an initial empty profile so user can be added to groups
202 try {
203 m.setProfile(null, null, null, null, null);
204 } catch (NoClassDefFoundError e) {
205 logger.warn("Failed to set default profile: {}", e.getMessage());
206 }
207
208 if (newManagerListener != null) {
209 newManagerListener.accept(m);
210 m = null;
211 }
212 } finally {
213 if (m != null) {
214 m.close();
215 }
216 }
217 }
218
219 private VerifyAccountResponse verifyAccountWithCode(
220 final String verificationCode, final String registrationLock
221 ) throws IOException {
222 final ServiceResponse<VerifyAccountResponse> response;
223 if (registrationLock == null) {
224 response = accountManager.verifyAccount(verificationCode,
225 account.getLocalRegistrationId(),
226 true,
227 account.getSelfUnidentifiedAccessKey(),
228 account.isUnrestrictedUnidentifiedAccess(),
229 ServiceConfig.capabilities,
230 account.isDiscoverableByPhoneNumber());
231 } else {
232 response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode,
233 account.getLocalRegistrationId(),
234 true,
235 registrationLock,
236 account.getSelfUnidentifiedAccessKey(),
237 account.isUnrestrictedUnidentifiedAccess(),
238 ServiceConfig.capabilities,
239 account.isDiscoverableByPhoneNumber());
240 }
241 handleResponseException(response);
242 return response.getResult().get();
243 }
244
245 @Override
246 public void close() {
247 if (account != null) {
248 account.close();
249 account = null;
250 }
251 }
252
253 private void handleResponseException(final ServiceResponse<?> response) throws IOException {
254 final var throwableOptional = response.getExecutionError().or(response.getApplicationError());
255 if (throwableOptional.isPresent()) {
256 if (throwableOptional.get() instanceof IOException) {
257 throw (IOException) throwableOptional.get();
258 } else {
259 throw new IOException(throwableOptional.get());
260 }
261 }
262 }
263 }