]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Improve behavior when pin data doesn't exist on the server
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / AccountHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.CaptchaRequiredException;
4 import org.asamk.signal.manager.api.DeviceLinkUrl;
5 import org.asamk.signal.manager.api.IncorrectPinException;
6 import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
7 import org.asamk.signal.manager.api.PinLockMissingException;
8 import org.asamk.signal.manager.api.PinLockedException;
9 import org.asamk.signal.manager.api.RateLimitException;
10 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
11 import org.asamk.signal.manager.internal.SignalDependencies;
12 import org.asamk.signal.manager.jobs.SyncStorageJob;
13 import org.asamk.signal.manager.storage.SignalAccount;
14 import org.asamk.signal.manager.util.KeyUtils;
15 import org.asamk.signal.manager.util.NumberVerificationUtils;
16 import org.asamk.signal.manager.util.Utils;
17 import org.signal.core.util.Base64;
18 import org.signal.libsignal.protocol.IdentityKeyPair;
19 import org.signal.libsignal.protocol.InvalidKeyException;
20 import org.signal.libsignal.protocol.SignalProtocolAddress;
21 import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
22 import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
23 import org.signal.libsignal.protocol.util.KeyHelper;
24 import org.signal.libsignal.usernames.BaseUsernameException;
25 import org.signal.libsignal.usernames.Username;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28 import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
29 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
30 import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
31 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
32 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
33 import org.whispersystems.signalservice.api.push.ServiceIdType;
34 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
35 import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
36 import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
37 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
38 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
39 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
40 import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
41 import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
42 import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
43 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
44 import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
45 import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
46 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
47 import org.whispersystems.signalservice.internal.push.SyncMessage;
48 import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
49
50 import java.io.IOException;
51 import java.util.ArrayList;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Objects;
55 import java.util.UUID;
56 import java.util.concurrent.TimeUnit;
57
58 import okio.ByteString;
59
60 import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
61 import static org.asamk.signal.manager.util.Utils.handleResponseException;
62 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
63
64 public class AccountHelper {
65
66 private static final Logger logger = LoggerFactory.getLogger(AccountHelper.class);
67
68 private final Context context;
69 private final SignalAccount account;
70 private final SignalDependencies dependencies;
71
72 private Callable unregisteredListener;
73
74 public AccountHelper(final Context context) {
75 this.account = context.getAccount();
76 this.dependencies = context.getDependencies();
77 this.context = context;
78 }
79
80 public void setUnregisteredListener(final Callable unregisteredListener) {
81 this.unregisteredListener = unregisteredListener;
82 }
83
84 public void checkAccountState() throws IOException {
85 if (account.getLastReceiveTimestamp() == 0) {
86 logger.info("The Signal protocol expects that incoming messages are regularly received.");
87 } else {
88 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
89 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
90 if (days > 7) {
91 logger.warn(
92 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
93 days);
94 }
95 }
96 try {
97 updateAccountAttributes();
98 if (account.getPreviousStorageVersion() < 9) {
99 context.getPreKeyHelper().forceRefreshPreKeys();
100 } else {
101 context.getPreKeyHelper().refreshPreKeysIfNecessary();
102 }
103 if (account.getAci() == null || account.getPni() == null) {
104 checkWhoAmiI();
105 }
106 if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
107 throw new IOException("Missing PNI identity key, relinking required");
108 }
109 if (account.getPreviousStorageVersion() < 10
110 && account.isPrimaryDevice()
111 && account.getRegistrationLockPin() != null) {
112 migrateRegistrationPin();
113 }
114 if (account.getUsername() != null && account.getUsernameLink() == null) {
115 try {
116 tryToSetUsernameLink(new Username(account.getUsername()));
117 } catch (BaseUsernameException e) {
118 logger.debug("Invalid local username");
119 }
120 }
121 } catch (DeprecatedVersionException e) {
122 logger.debug("Signal-Server returned deprecated version exception", e);
123 throw e;
124 } catch (AuthorizationFailedException e) {
125 account.setRegistered(false);
126 throw e;
127 }
128 }
129
130 public void checkWhoAmiI() throws IOException {
131 final var whoAmI = dependencies.getAccountManager().getWhoAmI();
132 final var number = whoAmI.getNumber();
133 final var aci = ACI.parseOrThrow(whoAmI.getAci());
134 final var pni = PNI.parseOrThrow(whoAmI.getPni());
135 if (number.equals(account.getNumber()) && aci.equals(account.getAci()) && pni.equals(account.getPni())) {
136 return;
137 }
138
139 updateSelfIdentifiers(number, aci, pni);
140 }
141
142 private void updateSelfIdentifiers(final String number, final ACI aci, final PNI pni) {
143 account.setNumber(number);
144 account.setAci(aci);
145 account.setPni(pni);
146 if (account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
147 account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
148 }
149 account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
150 context.getUnidentifiedAccessHelper().rotateSenderCertificates();
151 dependencies.resetAfterAddressChange();
152 context.getGroupV2Helper().clearAuthCredentialCache();
153 context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
154 context.getJobExecutor().enqueueJob(new SyncStorageJob());
155 }
156
157 public void setPni(
158 final PNI updatedPni,
159 final IdentityKeyPair pniIdentityKeyPair,
160 final String number,
161 final int localPniRegistrationId,
162 final SignedPreKeyRecord pniSignedPreKey,
163 final KyberPreKeyRecord lastResortKyberPreKey
164 ) throws IOException {
165 updateSelfIdentifiers(number != null ? number : account.getNumber(), account.getAci(), updatedPni);
166 account.setNewPniIdentity(pniIdentityKeyPair, pniSignedPreKey, lastResortKyberPreKey, localPniRegistrationId);
167 context.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType.PNI);
168 }
169
170 public void startChangeNumber(
171 String newNumber,
172 boolean voiceVerification,
173 String captcha
174 ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
175 final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
176 final var registrationApi = accountManager.getRegistrationApi();
177 String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
178 account.getSessionId(newNumber),
179 id -> account.setSessionId(newNumber, id),
180 voiceVerification,
181 captcha);
182 NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
183 }
184
185 public void finishChangeNumber(
186 String newNumber,
187 String verificationCode,
188 String pin
189 ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
190 for (var attempts = 0; attempts < 5; attempts++) {
191 try {
192 finishChangeNumberInternal(newNumber, verificationCode, pin);
193 break;
194 } catch (MismatchedDevicesException e) {
195 logger.debug("Change number failed with mismatched devices, retrying.");
196 try {
197 dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
198 } catch (UntrustedIdentityException ex) {
199 throw new AssertionError(ex);
200 }
201 }
202 }
203 }
204
205 private void finishChangeNumberInternal(
206 String newNumber,
207 String verificationCode,
208 String pin
209 ) throws IncorrectPinException, PinLockedException, IOException, PinLockMissingException {
210 final var pniIdentity = KeyUtils.generateIdentityKeyPair();
211 final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
212 final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
213 final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
214 final var pniRegistrationIds = new HashMap<Integer, Integer>();
215
216 final var selfDeviceId = account.getDeviceId();
217 SyncMessage.PniChangeNumber selfChangeNumber = null;
218
219 final var deviceIds = new ArrayList<Integer>();
220 deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
221 final var aci = account.getAci();
222 final var accountDataStore = account.getSignalServiceDataStore().aci();
223 final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
224 .stream()
225 .filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
226 deviceId)))
227 .toList();
228 deviceIds.addAll(subDeviceSessions);
229
230 final var messageSender = dependencies.getMessageSender();
231 for (final var deviceId : deviceIds) {
232 // Signed Prekey
233 final SignedPreKeyRecord signedPreKeyRecord;
234 try {
235 signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
236 pniIdentity.getPrivateKey());
237 final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
238 signedPreKeyRecord.getKeyPair().getPublicKey(),
239 signedPreKeyRecord.getSignature());
240 devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
241 } catch (InvalidKeyException e) {
242 throw new AssertionError("unexpected invalid key", e);
243 }
244
245 // Last-resort kyber prekey
246 final KyberPreKeyRecord lastResortKyberPreKeyRecord;
247 try {
248 lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
249 pniIdentity.getPrivateKey());
250 final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
251 lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
252 lastResortKyberPreKeyRecord.getSignature());
253 devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
254 } catch (InvalidKeyException e) {
255 throw new AssertionError("unexpected invalid key", e);
256 }
257
258 // Registration Id
259 var pniRegistrationId = -1;
260 while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
261 pniRegistrationId = KeyHelper.generateRegistrationId(false);
262 }
263 pniRegistrationIds.put(deviceId, pniRegistrationId);
264
265 // Device Message
266 final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
267 pniIdentity.serialize()))
268 .signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
269 .lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
270 .registrationId(pniRegistrationId)
271 .newE164(newNumber)
272 .build();
273
274 if (deviceId == selfDeviceId) {
275 selfChangeNumber = pniChangeNumber;
276 } else {
277 try {
278 final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
279 pniChangeNumber);
280 encryptedDeviceMessages.add(message);
281 } catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
282 throw new RuntimeException(e);
283 }
284 }
285 }
286
287 final var sessionId = account.getSessionId(newNumber);
288 final var result = NumberVerificationUtils.verifyNumber(sessionId,
289 verificationCode,
290 pin,
291 context.getPinHelper(),
292 (sessionId1, verificationCode1, registrationLock) -> {
293 final var registrationApi = dependencies.getRegistrationApi();
294 final var accountApi = dependencies.getAccountApi();
295 try {
296 handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
297 } catch (AlreadyVerifiedException e) {
298 // Already verified so can continue changing number
299 }
300 return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
301 null,
302 newNumber,
303 registrationLock,
304 pniIdentity.getPublicKey(),
305 encryptedDeviceMessages,
306 Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
307 Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
308 Utils.mapKeys(pniRegistrationIds, Object::toString))));
309 });
310
311 final var updatePni = PNI.parseOrThrow(result.first().getPni());
312 if (updatePni.equals(account.getPni())) {
313 logger.debug("PNI is unchanged after change number");
314 return;
315 }
316
317 handlePniChangeNumberMessage(selfChangeNumber, updatePni);
318 }
319
320 public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
321 if (pniChangeNumber.identityKeyPair != null
322 && pniChangeNumber.registrationId != null
323 && pniChangeNumber.signedPreKey != null) {
324 logger.debug("New PNI: {}", updatedPni);
325 try {
326 setPni(updatedPni,
327 new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
328 pniChangeNumber.newE164,
329 pniChangeNumber.registrationId,
330 new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
331 pniChangeNumber.lastResortKyberPreKey != null
332 ? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
333 : null);
334 } catch (Exception e) {
335 logger.warn("Failed to handle change number message", e);
336 }
337 }
338 }
339
340 public static final int USERNAME_MIN_LENGTH = 3;
341 public static final int USERNAME_MAX_LENGTH = 32;
342
343 public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
344 final var currentUsername = account.getUsername();
345 if (currentUsername != null) {
346 final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
347 if (currentNickname.equals(nickname)) {
348 try {
349 refreshCurrentUsername();
350 return;
351 } catch (IOException | BaseUsernameException e) {
352 logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
353 }
354 }
355 }
356
357 final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
358 reserveUsername(candidates);
359 }
360
361 public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
362 final var currentUsername = account.getUsername();
363 if (currentUsername != null) {
364 if (currentUsername.equals(username)) {
365 try {
366 refreshCurrentUsername();
367 return;
368 } catch (IOException | BaseUsernameException e) {
369 logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
370 }
371 }
372 }
373
374 final var candidates = List.of(new Username(username));
375 reserveUsername(candidates);
376 }
377
378 private void reserveUsername(final List<Username> candidates) throws IOException {
379 final var candidateHashes = new ArrayList<String>();
380 for (final var candidate : candidates) {
381 candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
382 }
383
384 final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
385 final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
386 if (hashIndex == -1) {
387 logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
388 throw new IOException("Unexpected username response");
389 }
390
391 logger.debug("[reserveUsername] Successfully reserved username.");
392 final var username = candidates.get(hashIndex);
393
394 final var linkComponents = confirmUsernameAndCreateNewLink(username);
395 account.setUsername(username.getUsername());
396 account.setUsernameLink(linkComponents);
397 account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
398 account.getRecipientStore().rotateSelfStorageId();
399 logger.debug("[confirmUsername] Successfully confirmed username.");
400 }
401
402 public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
403 try {
404 Username.UsernameLink link = username.generateLink();
405 return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
406 } catch (BaseUsernameException e) {
407 throw new AssertionError(e);
408 }
409 }
410
411 private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
412 try {
413 Username.UsernameLink link = username.generateLink();
414 UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
415
416 return new UsernameLinkComponents(link.getEntropy(), serverId);
417 } catch (BaseUsernameException e) {
418 throw new AssertionError(e);
419 }
420 }
421
422 private UsernameLinkComponents reclaimUsernameAndLink(
423 Username username,
424 UsernameLinkComponents linkComponents
425 ) throws IOException {
426 try {
427 Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
428 UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
429
430 return new UsernameLinkComponents(link.getEntropy(), serverId);
431 } catch (BaseUsernameException e) {
432 throw new AssertionError(e);
433 }
434 }
435
436 public void refreshCurrentUsername() throws IOException, BaseUsernameException {
437 final var localUsername = account.getUsername();
438 if (localUsername == null) {
439 return;
440 }
441
442 final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
443 final var serverUsernameHash = whoAmIResponse.getUsernameHash();
444 final var hasServerUsername = !isEmpty(serverUsernameHash);
445 final var username = new Username(localUsername);
446 final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
447
448 if (!hasServerUsername) {
449 logger.debug("No remote username is set.");
450 }
451
452 if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
453 logger.debug("Local username hash does not match server username hash.");
454 }
455
456 if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
457 logger.debug("Attempting to resynchronize username.");
458 try {
459 tryReserveConfirmUsername(username);
460 } catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) {
461 logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
462 e.getMessage(),
463 e.getClass().getSimpleName());
464 account.setUsername(null);
465 account.setUsernameLink(null);
466 account.getRecipientStore().rotateSelfStorageId();
467 throw e;
468 }
469 } else {
470 logger.debug("Username already set, not refreshing.");
471 }
472 }
473
474 private void tryReserveConfirmUsername(final Username username) throws IOException {
475 final var usernameLink = account.getUsernameLink();
476
477 if (usernameLink == null) {
478 handleResponseException(dependencies.getAccountApi()
479 .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
480 logger.debug("[reserveUsername] Successfully reserved existing username.");
481 final var linkComponents = confirmUsernameAndCreateNewLink(username);
482 account.setUsernameLink(linkComponents);
483 logger.debug("[confirmUsername] Successfully confirmed existing username.");
484 } else {
485 final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
486 account.setUsernameLink(linkComponents);
487 logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
488 }
489 account.getRecipientStore().rotateSelfStorageId();
490 }
491
492 private void tryToSetUsernameLink(Username username) {
493 for (var i = 1; i < 4; i++) {
494 try {
495 final var linkComponents = createUsernameLink(username);
496 account.setUsernameLink(linkComponents);
497 break;
498 } catch (IOException e) {
499 logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
500 }
501 }
502 }
503
504 public void deleteUsername() throws IOException {
505 handleResponseException(dependencies.getAccountApi().deleteUsername());
506 account.setUsernameLink(null);
507 account.setUsername(null);
508 logger.debug("[deleteUsername] Successfully deleted the username.");
509 }
510
511 public void setDeviceName(String deviceName) {
512 final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
513 final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
514 account.setEncryptedDeviceName(encryptedDeviceName);
515 }
516
517 public void updateAccountAttributes() throws IOException {
518 handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
519 }
520
521 public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
522 final var linkDeviceApi = dependencies.getLinkDeviceApi();
523 final LinkedDeviceVerificationCodeResponse verificationCode;
524 try {
525 verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
526 } catch (DeviceLimitExceededException e) {
527 throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
528 }
529
530 handleResponseException(dependencies.getLinkDeviceApi()
531 .linkDevice(account.getNumber(),
532 account.getAci(),
533 account.getPni(),
534 deviceLinkInfo.deviceIdentifier(),
535 deviceLinkInfo.deviceKey(),
536 account.getAciIdentityKeyPair(),
537 account.getPniIdentityKeyPair(),
538 account.getProfileKey(),
539 account.getOrCreateAccountEntropyPool(),
540 account.getOrCreatePinMasterKey(),
541 account.getOrCreateMediaRootBackupKey(),
542 verificationCode.getVerificationCode(),
543 null));
544 account.setMultiDevice(true);
545 context.getJobExecutor().enqueueJob(new SyncStorageJob());
546 }
547
548 public void removeLinkedDevices(int deviceId) throws IOException {
549 handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
550 var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
551 account.setMultiDevice(devices.size() > 1);
552 }
553
554 public void migrateRegistrationPin() throws IOException {
555 var masterKey = account.getOrCreatePinMasterKey();
556
557 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
558 handleResponseException(dependencies.getAccountApi()
559 .enableRegistrationLock(masterKey.deriveRegistrationLock()));
560 }
561
562 public void setRegistrationPin(String pin) throws IOException {
563 var masterKey = account.getOrCreatePinMasterKey();
564
565 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
566 handleResponseException(dependencies.getAccountApi()
567 .enableRegistrationLock(masterKey.deriveRegistrationLock()));
568
569 account.setRegistrationLockPin(pin);
570 updateAccountAttributes();
571 }
572
573 public void removeRegistrationPin() throws IOException {
574 // Remove KBS Pin
575 context.getPinHelper().removeRegistrationLockPin();
576 handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
577
578 account.setRegistrationLockPin(null);
579 }
580
581 public void unregister() throws IOException {
582 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
583 // If this is the primary device, other users can't send messages to this number anymore.
584 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
585 handleResponseException(dependencies.getAccountApi().clearFcmToken());
586
587 account.setRegistered(false);
588 unregisteredListener.call();
589 }
590
591 public void deleteAccount() throws IOException {
592 try {
593 context.getPinHelper().removeRegistrationLockPin();
594 } catch (IOException e) {
595 logger.warn("Failed to remove registration lock pin");
596 }
597 account.setRegistrationLockPin(null);
598
599 handleResponseException(dependencies.getAccountApi().deleteAccount());
600
601 account.setRegistered(false);
602 unregisteredListener.call();
603 }
604
605 public interface Callable {
606
607 void call();
608 }
609 }