1 package org
.asamk
.signal
.manager
.helper
;
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
;
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
;
58 import okio
.ByteString
;
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
;
64 public class AccountHelper
{
66 private static final Logger logger
= LoggerFactory
.getLogger(AccountHelper
.class);
68 private final Context context
;
69 private final SignalAccount account
;
70 private final SignalDependencies dependencies
;
72 private Callable unregisteredListener
;
74 public AccountHelper(final Context context
) {
75 this.account
= context
.getAccount();
76 this.dependencies
= context
.getDependencies();
77 this.context
= context
;
80 public void setUnregisteredListener(final Callable unregisteredListener
) {
81 this.unregisteredListener
= unregisteredListener
;
84 public void checkAccountState() throws IOException
{
85 if (account
.getLastReceiveTimestamp() == 0) {
86 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
88 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
89 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
92 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
97 updateAccountAttributes();
98 if (account
.getPreviousStorageVersion() < 9) {
99 context
.getPreKeyHelper().forceRefreshPreKeys();
101 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
103 if (account
.getAci() == null || account
.getPni() == null) {
106 if (!account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
107 throw new IOException("Missing PNI identity key, relinking required");
109 if (account
.getPreviousStorageVersion() < 10
110 && account
.isPrimaryDevice()
111 && account
.getRegistrationLockPin() != null) {
112 migrateRegistrationPin();
114 if (account
.getUsername() != null && account
.getUsernameLink() == null) {
116 tryToSetUsernameLink(new Username(account
.getUsername()));
117 } catch (BaseUsernameException e
) {
118 logger
.debug("Invalid local username");
121 } catch (DeprecatedVersionException e
) {
122 logger
.debug("Signal-Server returned deprecated version exception", e
);
124 } catch (AuthorizationFailedException e
) {
125 account
.setRegistered(false);
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())) {
139 updateSelfIdentifiers(number
, aci
, pni
);
142 private void updateSelfIdentifiers(final String number
, final ACI aci
, final PNI pni
) {
143 account
.setNumber(number
);
146 if (account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
147 account
.setPniIdentityKeyPair(KeyUtils
.generateIdentityKeyPair());
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());
158 final PNI updatedPni
,
159 final IdentityKeyPair pniIdentityKeyPair
,
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
);
170 public void startChangeNumber(
172 boolean voiceVerification
,
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
),
182 NumberVerificationUtils
.requestVerificationCode(registrationApi
, sessionId
, voiceVerification
);
185 public void finishChangeNumber(
187 String verificationCode
,
189 ) throws IncorrectPinException
, PinLockedException
, IOException
, PinLockMissingException
{
190 for (var attempts
= 0; attempts
< 5; attempts
++) {
192 finishChangeNumberInternal(newNumber
, verificationCode
, pin
);
194 } catch (MismatchedDevicesException e
) {
195 logger
.debug("Change number failed with mismatched devices, retrying.");
197 dependencies
.getMessageSender().handleChangeNumberMismatchDevices(e
.getMismatchedDevices());
198 } catch (UntrustedIdentityException ex
) {
199 throw new AssertionError(ex
);
205 private void finishChangeNumberInternal(
207 String verificationCode
,
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
>();
216 final var selfDeviceId
= account
.getDeviceId();
217 SyncMessage
.PniChangeNumber selfChangeNumber
= null;
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())
225 .filter(deviceId
-> accountDataStore
.containsSession(new SignalProtocolAddress(aci
.toString(),
228 deviceIds
.addAll(subDeviceSessions
);
230 final var messageSender
= dependencies
.getMessageSender();
231 for (final var deviceId
: deviceIds
) {
233 final SignedPreKeyRecord signedPreKeyRecord
;
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
);
245 // Last-resort kyber prekey
246 final KyberPreKeyRecord lastResortKyberPreKeyRecord
;
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
);
259 var pniRegistrationId
= -1;
260 while (pniRegistrationId
< 0 || pniRegistrationIds
.containsValue(pniRegistrationId
)) {
261 pniRegistrationId
= KeyHelper
.generateRegistrationId(false);
263 pniRegistrationIds
.put(deviceId
, pniRegistrationId
);
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
)
274 if (deviceId
== selfDeviceId
) {
275 selfChangeNumber
= pniChangeNumber
;
278 final var message
= messageSender
.getEncryptedSyncPniInitializeDeviceMessage(deviceId
,
280 encryptedDeviceMessages
.add(message
);
281 } catch (UntrustedIdentityException
| IOException
| InvalidKeyException e
) {
282 throw new RuntimeException(e
);
287 final var sessionId
= account
.getSessionId(newNumber
);
288 final var result
= NumberVerificationUtils
.verifyNumber(sessionId
,
291 context
.getPinHelper(),
292 (sessionId1
, verificationCode1
, registrationLock
) -> {
293 final var registrationApi
= dependencies
.getRegistrationApi();
294 final var accountApi
= dependencies
.getAccountApi();
296 handleResponseException(registrationApi
.verifyAccount(sessionId1
, verificationCode1
));
297 } catch (AlreadyVerifiedException e
) {
298 // Already verified so can continue changing number
300 return handleResponseException(accountApi
.changeNumber(new ChangePhoneNumberRequest(sessionId1
,
304 pniIdentity
.getPublicKey(),
305 encryptedDeviceMessages
,
306 Utils
.mapKeys(devicePniSignedPreKeys
, Object
::toString
),
307 Utils
.mapKeys(devicePniLastResortKyberPreKeys
, Object
::toString
),
308 Utils
.mapKeys(pniRegistrationIds
, Object
::toString
))));
311 final var updatePni
= PNI
.parseOrThrow(result
.first().getPni());
312 if (updatePni
.equals(account
.getPni())) {
313 logger
.debug("PNI is unchanged after change number");
317 handlePniChangeNumberMessage(selfChangeNumber
, updatePni
);
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
);
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())
334 } catch (Exception e
) {
335 logger
.warn("Failed to handle change number message", e
);
340 public static final int USERNAME_MIN_LENGTH
= 3;
341 public static final int USERNAME_MAX_LENGTH
= 32;
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
)) {
349 refreshCurrentUsername();
351 } catch (IOException
| BaseUsernameException e
) {
352 logger
.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
357 final var candidates
= Username
.candidatesFrom(nickname
, USERNAME_MIN_LENGTH
, USERNAME_MAX_LENGTH
);
358 reserveUsername(candidates
);
361 public void reserveExactUsername(String username
) throws IOException
, BaseUsernameException
{
362 final var currentUsername
= account
.getUsername();
363 if (currentUsername
!= null) {
364 if (currentUsername
.equals(username
)) {
366 refreshCurrentUsername();
368 } catch (IOException
| BaseUsernameException e
) {
369 logger
.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
374 final var candidates
= List
.of(new Username(username
));
375 reserveUsername(candidates
);
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()));
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");
391 logger
.debug("[reserveUsername] Successfully reserved username.");
392 final var username
= candidates
.get(hashIndex
);
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.");
402 public UsernameLinkComponents
createUsernameLink(Username username
) throws IOException
{
404 Username
.UsernameLink link
= username
.generateLink();
405 return handleResponseException(dependencies
.getAccountApi().createUsernameLink(link
));
406 } catch (BaseUsernameException e
) {
407 throw new AssertionError(e
);
411 private UsernameLinkComponents
confirmUsernameAndCreateNewLink(Username username
) throws IOException
{
413 Username
.UsernameLink link
= username
.generateLink();
414 UUID serverId
= handleResponseException(dependencies
.getAccountApi().confirmUsername(username
, link
));
416 return new UsernameLinkComponents(link
.getEntropy(), serverId
);
417 } catch (BaseUsernameException e
) {
418 throw new AssertionError(e
);
422 private UsernameLinkComponents
reclaimUsernameAndLink(
424 UsernameLinkComponents linkComponents
425 ) throws IOException
{
427 Username
.UsernameLink link
= username
.generateLink(linkComponents
.getEntropy());
428 UUID serverId
= handleResponseException(dependencies
.getAccountApi().confirmUsername(username
, link
));
430 return new UsernameLinkComponents(link
.getEntropy(), serverId
);
431 } catch (BaseUsernameException e
) {
432 throw new AssertionError(e
);
436 public void refreshCurrentUsername() throws IOException
, BaseUsernameException
{
437 final var localUsername
= account
.getUsername();
438 if (localUsername
== null) {
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());
448 if (!hasServerUsername
) {
449 logger
.debug("No remote username is set.");
452 if (!Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
453 logger
.debug("Local username hash does not match server username hash.");
456 if (!hasServerUsername
|| !Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
457 logger
.debug("Attempting to resynchronize username.");
459 tryReserveConfirmUsername(username
);
460 } catch (UsernameMalformedException
| UsernameTakenException
| UsernameIsNotReservedException e
) {
461 logger
.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
463 e
.getClass().getSimpleName());
464 account
.setUsername(null);
465 account
.setUsernameLink(null);
466 account
.getRecipientStore().rotateSelfStorageId();
470 logger
.debug("Username already set, not refreshing.");
474 private void tryReserveConfirmUsername(final Username username
) throws IOException
{
475 final var usernameLink
= account
.getUsernameLink();
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.");
485 final var linkComponents
= reclaimUsernameAndLink(username
, usernameLink
);
486 account
.setUsernameLink(linkComponents
);
487 logger
.debug("[confirmUsername] Successfully reclaimed existing username and link.");
489 account
.getRecipientStore().rotateSelfStorageId();
492 private void tryToSetUsernameLink(Username username
) {
493 for (var i
= 1; i
< 4; i
++) {
495 final var linkComponents
= createUsernameLink(username
);
496 account
.setUsernameLink(linkComponents
);
498 } catch (IOException e
) {
499 logger
.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i
, e
);
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.");
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
);
517 public void updateAccountAttributes() throws IOException
{
518 handleResponseException(dependencies
.getAccountApi().setAccountAttributes(account
.getAccountAttributes(null)));
521 public void addDevice(DeviceLinkUrl deviceLinkInfo
) throws IOException
, org
.asamk
.signal
.manager
.api
.DeviceLimitExceededException
{
522 final var linkDeviceApi
= dependencies
.getLinkDeviceApi();
523 final LinkedDeviceVerificationCodeResponse verificationCode
;
525 verificationCode
= handleResponseException(linkDeviceApi
.getDeviceVerificationCode());
526 } catch (DeviceLimitExceededException e
) {
527 throw new org
.asamk
.signal
.manager
.api
.DeviceLimitExceededException("Too many linked devices", e
);
530 handleResponseException(dependencies
.getLinkDeviceApi()
531 .linkDevice(account
.getNumber(),
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(),
544 account
.setMultiDevice(true);
545 context
.getJobExecutor().enqueueJob(new SyncStorageJob());
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);
554 public void migrateRegistrationPin() throws IOException
{
555 var masterKey
= account
.getOrCreatePinMasterKey();
557 context
.getPinHelper().migrateRegistrationLockPin(account
.getRegistrationLockPin(), masterKey
);
558 handleResponseException(dependencies
.getAccountApi()
559 .enableRegistrationLock(masterKey
.deriveRegistrationLock()));
562 public void setRegistrationPin(String pin
) throws IOException
{
563 var masterKey
= account
.getOrCreatePinMasterKey();
565 context
.getPinHelper().setRegistrationLockPin(pin
, masterKey
);
566 handleResponseException(dependencies
.getAccountApi()
567 .enableRegistrationLock(masterKey
.deriveRegistrationLock()));
569 account
.setRegistrationLockPin(pin
);
570 updateAccountAttributes();
573 public void removeRegistrationPin() throws IOException
{
575 context
.getPinHelper().removeRegistrationLockPin();
576 handleResponseException(dependencies
.getAccountApi().disableRegistrationLock());
578 account
.setRegistrationLockPin(null);
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());
587 account
.setRegistered(false);
588 unregisteredListener
.call();
591 public void deleteAccount() throws IOException
{
593 context
.getPinHelper().removeRegistrationLockPin();
594 } catch (IOException e
) {
595 logger
.warn("Failed to remove registration lock pin");
597 account
.setRegistrationLockPin(null);
599 handleResponseException(dependencies
.getAccountApi().deleteAccount());
601 account
.setRegistered(false);
602 unregisteredListener
.call();
605 public interface Callable
{