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
.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
;
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
;
57 import okio
.ByteString
;
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
;
63 public class AccountHelper
{
65 private static final Logger logger
= LoggerFactory
.getLogger(AccountHelper
.class);
67 private final Context context
;
68 private final SignalAccount account
;
69 private final SignalDependencies dependencies
;
71 private Callable unregisteredListener
;
73 public AccountHelper(final Context context
) {
74 this.account
= context
.getAccount();
75 this.dependencies
= context
.getDependencies();
76 this.context
= context
;
79 public void setUnregisteredListener(final Callable unregisteredListener
) {
80 this.unregisteredListener
= unregisteredListener
;
83 public void checkAccountState() throws IOException
{
84 if (account
.getLastReceiveTimestamp() == 0) {
85 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
87 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
88 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
91 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
96 updateAccountAttributes();
97 if (account
.getPreviousStorageVersion() < 9) {
98 context
.getPreKeyHelper().forceRefreshPreKeys();
100 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
102 if (account
.getAci() == null || account
.getPni() == null) {
105 if (!account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
106 throw new IOException("Missing PNI identity key, relinking required");
108 if (account
.getPreviousStorageVersion() < 10
109 && account
.isPrimaryDevice()
110 && account
.getRegistrationLockPin() != null) {
111 migrateRegistrationPin();
113 if (account
.getUsername() != null && account
.getUsernameLink() == null) {
115 tryToSetUsernameLink(new Username(account
.getUsername()));
116 } catch (BaseUsernameException e
) {
117 logger
.debug("Invalid local username");
120 } catch (DeprecatedVersionException e
) {
121 logger
.debug("Signal-Server returned deprecated version exception", e
);
123 } catch (AuthorizationFailedException e
) {
124 account
.setRegistered(false);
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())) {
138 updateSelfIdentifiers(number
, aci
, pni
);
141 private void updateSelfIdentifiers(final String number
, final ACI aci
, final PNI pni
) {
142 account
.setNumber(number
);
145 if (account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
146 account
.setPniIdentityKeyPair(KeyUtils
.generateIdentityKeyPair());
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());
157 final PNI updatedPni
,
158 final IdentityKeyPair pniIdentityKeyPair
,
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
);
169 public void startChangeNumber(
171 boolean voiceVerification
,
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
),
181 NumberVerificationUtils
.requestVerificationCode(registrationApi
, sessionId
, voiceVerification
);
184 public void finishChangeNumber(
186 String verificationCode
,
188 ) throws IncorrectPinException
, PinLockedException
, IOException
{
189 for (var attempts
= 0; attempts
< 5; attempts
++) {
191 finishChangeNumberInternal(newNumber
, verificationCode
, pin
);
193 } catch (MismatchedDevicesException e
) {
194 logger
.debug("Change number failed with mismatched devices, retrying.");
196 dependencies
.getMessageSender().handleChangeNumberMismatchDevices(e
.getMismatchedDevices());
197 } catch (UntrustedIdentityException ex
) {
198 throw new AssertionError(ex
);
204 private void finishChangeNumberInternal(
206 String verificationCode
,
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
>();
215 final var selfDeviceId
= account
.getDeviceId();
216 SyncMessage
.PniChangeNumber selfChangeNumber
= null;
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())
224 .filter(deviceId
-> accountDataStore
.containsSession(new SignalProtocolAddress(aci
.toString(),
227 deviceIds
.addAll(subDeviceSessions
);
229 final var messageSender
= dependencies
.getMessageSender();
230 for (final var deviceId
: deviceIds
) {
232 final SignedPreKeyRecord signedPreKeyRecord
;
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
);
244 // Last-resort kyber prekey
245 final KyberPreKeyRecord lastResortKyberPreKeyRecord
;
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
);
258 var pniRegistrationId
= -1;
259 while (pniRegistrationId
< 0 || pniRegistrationIds
.containsValue(pniRegistrationId
)) {
260 pniRegistrationId
= KeyHelper
.generateRegistrationId(false);
262 pniRegistrationIds
.put(deviceId
, pniRegistrationId
);
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
)
273 if (deviceId
== selfDeviceId
) {
274 selfChangeNumber
= pniChangeNumber
;
277 final var message
= messageSender
.getEncryptedSyncPniInitializeDeviceMessage(deviceId
,
279 encryptedDeviceMessages
.add(message
);
280 } catch (UntrustedIdentityException
| IOException
| InvalidKeyException e
) {
281 throw new RuntimeException(e
);
286 final var sessionId
= account
.getSessionId(newNumber
);
287 final var result
= NumberVerificationUtils
.verifyNumber(sessionId
,
290 context
.getPinHelper(),
291 (sessionId1
, verificationCode1
, registrationLock
) -> {
292 final var registrationApi
= dependencies
.getRegistrationApi();
293 final var accountApi
= dependencies
.getAccountApi();
295 handleResponseException(registrationApi
.verifyAccount(sessionId1
, verificationCode1
));
296 } catch (AlreadyVerifiedException e
) {
297 // Already verified so can continue changing number
299 return handleResponseException(accountApi
.changeNumber(new ChangePhoneNumberRequest(sessionId1
,
303 pniIdentity
.getPublicKey(),
304 encryptedDeviceMessages
,
305 Utils
.mapKeys(devicePniSignedPreKeys
, Object
::toString
),
306 Utils
.mapKeys(devicePniLastResortKyberPreKeys
, Object
::toString
),
307 Utils
.mapKeys(pniRegistrationIds
, Object
::toString
))));
310 final var updatePni
= PNI
.parseOrThrow(result
.first().getPni());
311 if (updatePni
.equals(account
.getPni())) {
312 logger
.debug("PNI is unchanged after change number");
316 handlePniChangeNumberMessage(selfChangeNumber
, updatePni
);
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
);
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())
333 } catch (Exception e
) {
334 logger
.warn("Failed to handle change number message", e
);
339 public static final int USERNAME_MIN_LENGTH
= 3;
340 public static final int USERNAME_MAX_LENGTH
= 32;
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
)) {
348 refreshCurrentUsername();
350 } catch (IOException
| BaseUsernameException e
) {
351 logger
.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
356 final var candidates
= Username
.candidatesFrom(nickname
, USERNAME_MIN_LENGTH
, USERNAME_MAX_LENGTH
);
357 reserveUsername(candidates
);
360 public void reserveExactUsername(String username
) throws IOException
, BaseUsernameException
{
361 final var currentUsername
= account
.getUsername();
362 if (currentUsername
!= null) {
363 if (currentUsername
.equals(username
)) {
365 refreshCurrentUsername();
367 } catch (IOException
| BaseUsernameException e
) {
368 logger
.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
373 final var candidates
= List
.of(new Username(username
));
374 reserveUsername(candidates
);
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()));
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");
390 logger
.debug("[reserveUsername] Successfully reserved username.");
391 final var username
= candidates
.get(hashIndex
);
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.");
401 public UsernameLinkComponents
createUsernameLink(Username username
) throws IOException
{
403 Username
.UsernameLink link
= username
.generateLink();
404 return handleResponseException(dependencies
.getAccountApi().createUsernameLink(link
));
405 } catch (BaseUsernameException e
) {
406 throw new AssertionError(e
);
410 private UsernameLinkComponents
confirmUsernameAndCreateNewLink(Username username
) throws IOException
{
412 Username
.UsernameLink link
= username
.generateLink();
413 UUID serverId
= handleResponseException(dependencies
.getAccountApi().confirmUsername(username
, link
));
415 return new UsernameLinkComponents(link
.getEntropy(), serverId
);
416 } catch (BaseUsernameException e
) {
417 throw new AssertionError(e
);
421 private UsernameLinkComponents
reclaimUsernameAndLink(
423 UsernameLinkComponents linkComponents
424 ) throws IOException
{
426 Username
.UsernameLink link
= username
.generateLink(linkComponents
.getEntropy());
427 UUID serverId
= handleResponseException(dependencies
.getAccountApi().confirmUsername(username
, link
));
429 return new UsernameLinkComponents(link
.getEntropy(), serverId
);
430 } catch (BaseUsernameException e
) {
431 throw new AssertionError(e
);
435 public void refreshCurrentUsername() throws IOException
, BaseUsernameException
{
436 final var localUsername
= account
.getUsername();
437 if (localUsername
== null) {
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());
447 if (!hasServerUsername
) {
448 logger
.debug("No remote username is set.");
451 if (!Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
452 logger
.debug("Local username hash does not match server username hash.");
455 if (!hasServerUsername
|| !Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
456 logger
.debug("Attempting to resynchronize username.");
458 tryReserveConfirmUsername(username
);
459 } catch (UsernameMalformedException
| UsernameTakenException
| UsernameIsNotReservedException e
) {
460 logger
.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
462 e
.getClass().getSimpleName());
463 account
.setUsername(null);
464 account
.setUsernameLink(null);
465 account
.getRecipientStore().rotateSelfStorageId();
469 logger
.debug("Username already set, not refreshing.");
473 private void tryReserveConfirmUsername(final Username username
) throws IOException
{
474 final var usernameLink
= account
.getUsernameLink();
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.");
484 final var linkComponents
= reclaimUsernameAndLink(username
, usernameLink
);
485 account
.setUsernameLink(linkComponents
);
486 logger
.debug("[confirmUsername] Successfully reclaimed existing username and link.");
488 account
.getRecipientStore().rotateSelfStorageId();
491 private void tryToSetUsernameLink(Username username
) {
492 for (var i
= 1; i
< 4; i
++) {
494 final var linkComponents
= createUsernameLink(username
);
495 account
.setUsernameLink(linkComponents
);
497 } catch (IOException e
) {
498 logger
.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i
, e
);
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.");
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
);
516 public void updateAccountAttributes() throws IOException
{
517 handleResponseException(dependencies
.getAccountApi().setAccountAttributes(account
.getAccountAttributes(null)));
520 public void addDevice(DeviceLinkUrl deviceLinkInfo
) throws IOException
, org
.asamk
.signal
.manager
.api
.DeviceLimitExceededException
{
521 final var linkDeviceApi
= dependencies
.getLinkDeviceApi();
522 final LinkedDeviceVerificationCodeResponse verificationCode
;
524 verificationCode
= handleResponseException(linkDeviceApi
.getDeviceVerificationCode());
525 } catch (DeviceLimitExceededException e
) {
526 throw new org
.asamk
.signal
.manager
.api
.DeviceLimitExceededException("Too many linked devices", e
);
529 handleResponseException(dependencies
.getLinkDeviceApi()
530 .linkDevice(account
.getNumber(),
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(),
543 account
.setMultiDevice(true);
544 context
.getJobExecutor().enqueueJob(new SyncStorageJob());
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);
553 public void migrateRegistrationPin() throws IOException
{
554 var masterKey
= account
.getOrCreatePinMasterKey();
556 context
.getPinHelper().migrateRegistrationLockPin(account
.getRegistrationLockPin(), masterKey
);
557 handleResponseException(dependencies
.getAccountApi()
558 .enableRegistrationLock(masterKey
.deriveRegistrationLock()));
561 public void setRegistrationPin(String pin
) throws IOException
{
562 var masterKey
= account
.getOrCreatePinMasterKey();
564 context
.getPinHelper().setRegistrationLockPin(pin
, masterKey
);
565 handleResponseException(dependencies
.getAccountApi()
566 .enableRegistrationLock(masterKey
.deriveRegistrationLock()));
568 account
.setRegistrationLockPin(pin
);
569 updateAccountAttributes();
572 public void removeRegistrationPin() throws IOException
{
574 context
.getPinHelper().removeRegistrationLockPin();
575 handleResponseException(dependencies
.getAccountApi().disableRegistrationLock());
577 account
.setRegistrationLockPin(null);
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());
586 account
.setRegistered(false);
587 unregisteredListener
.call();
590 public void deleteAccount() throws IOException
{
592 context
.getPinHelper().removeRegistrationLockPin();
593 } catch (IOException e
) {
594 logger
.warn("Failed to remove registration lock pin");
596 account
.setRegistrationLockPin(null);
598 handleResponseException(dependencies
.getAccountApi().deleteAccount());
600 account
.setRegistered(false);
601 unregisteredListener
.call();
604 public interface Callable
{