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