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
.InvalidDeviceLinkException
;
7 import org
.asamk
.signal
.manager
.api
.NonNormalizedPhoneNumberException
;
8 import org
.asamk
.signal
.manager
.api
.PinLockedException
;
9 import org
.asamk
.signal
.manager
.api
.RateLimitException
;
10 import org
.asamk
.signal
.manager
.internal
.SignalDependencies
;
11 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
12 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
13 import org
.asamk
.signal
.manager
.util
.NumberVerificationUtils
;
14 import org
.asamk
.signal
.manager
.util
.Utils
;
15 import org
.signal
.libsignal
.protocol
.IdentityKeyPair
;
16 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
17 import org
.signal
.libsignal
.protocol
.SignalProtocolAddress
;
18 import org
.signal
.libsignal
.protocol
.state
.KyberPreKeyRecord
;
19 import org
.signal
.libsignal
.protocol
.state
.SignedPreKeyRecord
;
20 import org
.signal
.libsignal
.protocol
.util
.KeyHelper
;
21 import org
.signal
.libsignal
.usernames
.BaseUsernameException
;
22 import org
.signal
.libsignal
.usernames
.Username
;
23 import org
.slf4j
.Logger
;
24 import org
.slf4j
.LoggerFactory
;
25 import org
.whispersystems
.signalservice
.api
.account
.ChangePhoneNumberRequest
;
26 import org
.whispersystems
.signalservice
.api
.crypto
.UntrustedIdentityException
;
27 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
28 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
29 import org
.whispersystems
.signalservice
.api
.push
.ServiceIdType
;
30 import org
.whispersystems
.signalservice
.api
.push
.SignalServiceAddress
;
31 import org
.whispersystems
.signalservice
.api
.push
.SignedPreKeyEntity
;
32 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AlreadyVerifiedException
;
33 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AuthorizationFailedException
;
34 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.DeprecatedVersionException
;
35 import org
.whispersystems
.signalservice
.api
.util
.DeviceNameUtil
;
36 import org
.whispersystems
.signalservice
.internal
.push
.KyberPreKeyEntity
;
37 import org
.whispersystems
.signalservice
.internal
.push
.OutgoingPushMessage
;
38 import org
.whispersystems
.signalservice
.internal
.push
.SyncMessage
;
39 import org
.whispersystems
.signalservice
.internal
.push
.exceptions
.MismatchedDevicesException
;
40 import org
.whispersystems
.util
.Base64UrlSafe
;
42 import java
.io
.IOException
;
43 import java
.util
.ArrayList
;
44 import java
.util
.HashMap
;
45 import java
.util
.List
;
46 import java
.util
.Objects
;
47 import java
.util
.Optional
;
48 import java
.util
.concurrent
.TimeUnit
;
50 import okio
.ByteString
;
52 import static org
.asamk
.signal
.manager
.config
.ServiceConfig
.PREKEY_MAXIMUM_ID
;
53 import static org
.whispersystems
.signalservice
.internal
.util
.Util
.isEmpty
;
55 public class AccountHelper
{
57 private final static Logger logger
= LoggerFactory
.getLogger(AccountHelper
.class);
59 private final Context context
;
60 private final SignalAccount account
;
61 private final SignalDependencies dependencies
;
63 private Callable unregisteredListener
;
65 public AccountHelper(final Context context
) {
66 this.account
= context
.getAccount();
67 this.dependencies
= context
.getDependencies();
68 this.context
= context
;
71 public void setUnregisteredListener(final Callable unregisteredListener
) {
72 this.unregisteredListener
= unregisteredListener
;
75 public void checkAccountState() throws IOException
{
76 if (account
.getLastReceiveTimestamp() == 0) {
77 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
79 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
80 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
83 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
88 updateAccountAttributes();
89 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
90 if (account
.getAci() == null || account
.getPni() == null) {
93 if (!account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
94 context
.getSyncHelper().requestSyncPniIdentity();
96 if (account
.getPreviousStorageVersion() < 4
97 && account
.isPrimaryDevice()
98 && account
.getRegistrationLockPin() != null) {
99 migrateRegistrationPin();
101 } catch (DeprecatedVersionException e
) {
102 logger
.debug("Signal-Server returned deprecated version exception", e
);
104 } catch (AuthorizationFailedException e
) {
105 account
.setRegistered(false);
110 public void checkWhoAmiI() throws IOException
{
111 final var whoAmI
= dependencies
.getAccountManager().getWhoAmI();
112 final var number
= whoAmI
.getNumber();
113 final var aci
= ACI
.parseOrThrow(whoAmI
.getAci());
114 final var pni
= PNI
.parseOrThrow(whoAmI
.getPni());
115 if (number
.equals(account
.getNumber()) && aci
.equals(account
.getAci()) && pni
.equals(account
.getPni())) {
119 updateSelfIdentifiers(number
, aci
, pni
);
122 private void updateSelfIdentifiers(final String number
, final ACI aci
, final PNI pni
) {
123 account
.setNumber(number
);
126 if (account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
127 account
.setPniIdentityKeyPair(KeyUtils
.generateIdentityKeyPair());
129 account
.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account
.getSelfRecipientAddress());
130 // TODO check and update remote storage
131 context
.getUnidentifiedAccessHelper().rotateSenderCertificates();
132 dependencies
.resetAfterAddressChange();
133 context
.getGroupV2Helper().clearAuthCredentialCache();
134 context
.getAccountFileUpdater().updateAccountIdentifiers(account
.getNumber(), account
.getAci());
138 final PNI updatedPni
,
139 final IdentityKeyPair pniIdentityKeyPair
,
141 final int localPniRegistrationId
,
142 final SignedPreKeyRecord pniSignedPreKey
,
143 final KyberPreKeyRecord lastResortKyberPreKey
144 ) throws IOException
{
145 updateSelfIdentifiers(number
!= null ? number
: account
.getNumber(), account
.getAci(), updatedPni
);
146 account
.setNewPniIdentity(pniIdentityKeyPair
, pniSignedPreKey
, lastResortKyberPreKey
, localPniRegistrationId
);
147 context
.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType
.PNI
);
150 public void startChangeNumber(
151 String newNumber
, boolean voiceVerification
, String captcha
152 ) throws IOException
, CaptchaRequiredException
, NonNormalizedPhoneNumberException
, RateLimitException
{
153 final var accountManager
= dependencies
.createUnauthenticatedAccountManager(newNumber
, account
.getPassword());
154 String sessionId
= NumberVerificationUtils
.handleVerificationSession(accountManager
,
155 account
.getSessionId(newNumber
),
156 id
-> account
.setSessionId(newNumber
, id
),
159 NumberVerificationUtils
.requestVerificationCode(accountManager
, sessionId
, voiceVerification
);
162 public void finishChangeNumber(
163 String newNumber
, String verificationCode
, String pin
164 ) throws IncorrectPinException
, PinLockedException
, IOException
{
165 for (var attempts
= 0; attempts
< 5; attempts
++) {
167 finishChangeNumberInternal(newNumber
, verificationCode
, pin
);
169 } catch (MismatchedDevicesException e
) {
170 logger
.debug("Change number failed with mismatched devices, retrying.");
172 dependencies
.getMessageSender().handleChangeNumberMismatchDevices(e
.getMismatchedDevices());
173 } catch (UntrustedIdentityException ex
) {
174 throw new AssertionError(ex
);
180 private void finishChangeNumberInternal(
181 String newNumber
, String verificationCode
, String pin
182 ) throws IncorrectPinException
, PinLockedException
, IOException
{
183 final var pniIdentity
= KeyUtils
.generateIdentityKeyPair();
184 final var encryptedDeviceMessages
= new ArrayList
<OutgoingPushMessage
>();
185 final var devicePniSignedPreKeys
= new HashMap
<Integer
, SignedPreKeyEntity
>();
186 final var devicePniLastResortKyberPreKeys
= new HashMap
<Integer
, KyberPreKeyEntity
>();
187 final var pniRegistrationIds
= new HashMap
<Integer
, Integer
>();
189 final var selfDeviceId
= account
.getDeviceId();
190 SyncMessage
.PniChangeNumber selfChangeNumber
= null;
192 final var deviceIds
= new ArrayList
<Integer
>();
193 deviceIds
.add(SignalServiceAddress
.DEFAULT_DEVICE_ID
);
194 final var aci
= account
.getAci();
195 final var accountDataStore
= account
.getSignalServiceDataStore().aci();
196 final var subDeviceSessions
= accountDataStore
.getSubDeviceSessions(aci
.toString())
198 .filter(deviceId
-> accountDataStore
.containsSession(new SignalProtocolAddress(aci
.toString(),
201 deviceIds
.addAll(subDeviceSessions
);
203 final var messageSender
= dependencies
.getMessageSender();
204 for (final var deviceId
: deviceIds
) {
206 final var signedPreKeyRecord
= KeyUtils
.generateSignedPreKeyRecord(KeyUtils
.getRandomInt(PREKEY_MAXIMUM_ID
),
207 pniIdentity
.getPrivateKey());
208 final var signedPreKeyEntity
= new SignedPreKeyEntity(signedPreKeyRecord
.getId(),
209 signedPreKeyRecord
.getKeyPair().getPublicKey(),
210 signedPreKeyRecord
.getSignature());
211 devicePniSignedPreKeys
.put(deviceId
, signedPreKeyEntity
);
213 // Last-resort kyber prekey
214 final var lastResortKyberPreKeyRecord
= KeyUtils
.generateKyberPreKeyRecord(KeyUtils
.getRandomInt(
215 PREKEY_MAXIMUM_ID
), pniIdentity
.getPrivateKey());
216 final var kyberPreKeyEntity
= new KyberPreKeyEntity(lastResortKyberPreKeyRecord
.getId(),
217 lastResortKyberPreKeyRecord
.getKeyPair().getPublicKey(),
218 lastResortKyberPreKeyRecord
.getSignature());
219 devicePniLastResortKyberPreKeys
.put(deviceId
, kyberPreKeyEntity
);
222 var pniRegistrationId
= -1;
223 while (pniRegistrationId
< 0 || pniRegistrationIds
.containsValue(pniRegistrationId
)) {
224 pniRegistrationId
= KeyHelper
.generateRegistrationId(false);
226 pniRegistrationIds
.put(deviceId
, pniRegistrationId
);
229 final var pniChangeNumber
= new SyncMessage
.PniChangeNumber
.Builder().identityKeyPair(ByteString
.of(
230 pniIdentity
.serialize()))
231 .signedPreKey(ByteString
.of(signedPreKeyRecord
.serialize()))
232 .lastResortKyberPreKey(ByteString
.of(lastResortKyberPreKeyRecord
.serialize()))
233 .registrationId(pniRegistrationId
)
237 if (deviceId
== selfDeviceId
) {
238 selfChangeNumber
= pniChangeNumber
;
241 final var message
= messageSender
.getEncryptedSyncPniInitializeDeviceMessage(deviceId
,
243 encryptedDeviceMessages
.add(message
);
244 } catch (UntrustedIdentityException
| IOException
| InvalidKeyException e
) {
245 throw new RuntimeException(e
);
250 final var sessionId
= account
.getSessionId(newNumber
);
251 final var result
= NumberVerificationUtils
.verifyNumber(sessionId
,
254 context
.getPinHelper(),
255 (sessionId1
, verificationCode1
, registrationLock
) -> {
256 final var accountManager
= dependencies
.getAccountManager();
258 Utils
.handleResponseException(accountManager
.verifyAccount(verificationCode1
, sessionId1
));
259 } catch (AlreadyVerifiedException e
) {
260 // Already verified so can continue changing number
262 return Utils
.handleResponseException(accountManager
.changeNumber(new ChangePhoneNumberRequest(
267 pniIdentity
.getPublicKey(),
268 encryptedDeviceMessages
,
269 Utils
.mapKeys(devicePniSignedPreKeys
, Object
::toString
),
270 Utils
.mapKeys(devicePniLastResortKyberPreKeys
, Object
::toString
),
271 Utils
.mapKeys(pniRegistrationIds
, Object
::toString
))));
274 final var updatePni
= PNI
.parseOrThrow(result
.first().getPni());
275 if (updatePni
.equals(account
.getPni())) {
276 logger
.debug("PNI is unchanged after change number");
280 handlePniChangeNumberMessage(selfChangeNumber
, updatePni
);
283 public void handlePniChangeNumberMessage(
284 final SyncMessage
.PniChangeNumber pniChangeNumber
, final PNI updatedPni
286 if (pniChangeNumber
.identityKeyPair
!= null
287 && pniChangeNumber
.registrationId
!= null
288 && pniChangeNumber
.signedPreKey
!= null) {
289 logger
.debug("New PNI: {}", updatedPni
);
292 new IdentityKeyPair(pniChangeNumber
.identityKeyPair
.toByteArray()),
293 pniChangeNumber
.newE164
,
294 pniChangeNumber
.registrationId
,
295 new SignedPreKeyRecord(pniChangeNumber
.signedPreKey
.toByteArray()),
296 pniChangeNumber
.lastResortKyberPreKey
!= null
297 ?
new KyberPreKeyRecord(pniChangeNumber
.lastResortKyberPreKey
.toByteArray())
299 } catch (Exception e
) {
300 logger
.warn("Failed to handle change number message", e
);
305 public static final int USERNAME_MIN_LENGTH
= 3;
306 public static final int USERNAME_MAX_LENGTH
= 32;
308 public String
reserveUsername(String nickname
) throws IOException
, BaseUsernameException
{
309 final var currentUsername
= account
.getUsername();
310 if (currentUsername
!= null) {
311 final var currentNickname
= currentUsername
.substring(0, currentUsername
.indexOf('.'));
312 if (currentNickname
.equals(nickname
)) {
313 refreshCurrentUsername();
314 return currentUsername
;
318 final var candidates
= Username
.candidatesFrom(nickname
, USERNAME_MIN_LENGTH
, USERNAME_MAX_LENGTH
);
319 final var candidateHashes
= new ArrayList
<String
>();
320 for (final var candidate
: candidates
) {
321 candidateHashes
.add(Base64UrlSafe
.encodeBytesWithoutPadding(candidate
.getHash()));
324 final var response
= dependencies
.getAccountManager().reserveUsername(candidateHashes
);
325 final var hashIndex
= candidateHashes
.indexOf(response
.getUsernameHash());
326 if (hashIndex
== -1) {
327 logger
.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
328 throw new IOException("Unexpected username response");
331 logger
.debug("[reserveUsername] Successfully reserved username.");
332 final var username
= candidates
.get(hashIndex
).getUsername();
334 dependencies
.getAccountManager().confirmUsername(username
, response
);
335 account
.setUsername(username
);
336 account
.getRecipientStore().resolveSelfRecipientTrusted(account
.getSelfRecipientAddress());
337 logger
.debug("[confirmUsername] Successfully confirmed username.");
342 public void refreshCurrentUsername() throws IOException
, BaseUsernameException
{
343 final var localUsername
= account
.getUsername();
344 if (localUsername
== null) {
348 final var whoAmIResponse
= dependencies
.getAccountManager().getWhoAmI();
349 final var serverUsernameHash
= whoAmIResponse
.getUsernameHash();
350 final var hasServerUsername
= !isEmpty(serverUsernameHash
);
351 final var localUsernameHash
= Base64UrlSafe
.encodeBytesWithoutPadding(new Username(localUsername
).getHash());
353 if (!hasServerUsername
) {
354 logger
.debug("No remote username is set.");
357 if (!Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
358 logger
.debug("Local username hash does not match server username hash.");
361 if (!hasServerUsername
|| !Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
362 logger
.debug("Attempting to resynchronize username.");
363 tryReserveConfirmUsername(localUsername
, localUsernameHash
);
365 logger
.debug("Username already set, not refreshing.");
369 private void tryReserveConfirmUsername(final String username
, String localUsernameHash
) throws IOException
{
370 final var response
= dependencies
.getAccountManager().reserveUsername(List
.of(localUsernameHash
));
371 logger
.debug("[reserveUsername] Successfully reserved existing username.");
372 dependencies
.getAccountManager().confirmUsername(username
, response
);
373 logger
.debug("[confirmUsername] Successfully confirmed existing username.");
376 public void deleteUsername() throws IOException
{
377 dependencies
.getAccountManager().deleteUsername();
378 account
.setUsername(null);
379 logger
.debug("[deleteUsername] Successfully deleted the username.");
382 public void setDeviceName(String deviceName
) {
383 final var privateKey
= account
.getAciIdentityKeyPair().getPrivateKey();
384 final var encryptedDeviceName
= DeviceNameUtil
.encryptDeviceName(deviceName
, privateKey
);
385 account
.setEncryptedDeviceName(encryptedDeviceName
);
388 public void updateAccountAttributes() throws IOException
{
389 dependencies
.getAccountManager().setAccountAttributes(account
.getAccountAttributes(null));
392 public void addDevice(DeviceLinkUrl deviceLinkInfo
) throws IOException
, InvalidDeviceLinkException
{
393 var verificationCode
= dependencies
.getAccountManager().getNewDeviceVerificationCode();
396 dependencies
.getAccountManager()
397 .addDevice(deviceLinkInfo
.deviceIdentifier(),
398 deviceLinkInfo
.deviceKey(),
399 account
.getAciIdentityKeyPair(),
400 account
.getPniIdentityKeyPair(),
401 account
.getProfileKey(),
403 } catch (InvalidKeyException e
) {
404 throw new InvalidDeviceLinkException("Invalid device link", e
);
406 account
.setMultiDevice(true);
409 public void removeLinkedDevices(int deviceId
) throws IOException
{
410 dependencies
.getAccountManager().removeDevice(deviceId
);
411 var devices
= dependencies
.getAccountManager().getDevices();
412 account
.setMultiDevice(devices
.size() > 1);
415 public void migrateRegistrationPin() throws IOException
{
416 var masterKey
= account
.getOrCreatePinMasterKey();
418 context
.getPinHelper().migrateRegistrationLockPin(account
.getRegistrationLockPin(), masterKey
);
419 dependencies
.getAccountManager().enableRegistrationLock(masterKey
);
422 public void setRegistrationPin(String pin
) throws IOException
{
423 var masterKey
= account
.getOrCreatePinMasterKey();
425 context
.getPinHelper().setRegistrationLockPin(pin
, masterKey
);
426 dependencies
.getAccountManager().enableRegistrationLock(masterKey
);
428 account
.setRegistrationLockPin(pin
);
431 public void removeRegistrationPin() throws IOException
{
433 context
.getPinHelper().removeRegistrationLockPin();
434 dependencies
.getAccountManager().disableRegistrationLock();
436 account
.setRegistrationLockPin(null);
439 public void unregister() throws IOException
{
440 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
441 // If this is the primary device, other users can't send messages to this number anymore.
442 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
443 dependencies
.getAccountManager().setGcmId(Optional
.empty());
445 account
.setRegistered(false);
446 unregisteredListener
.call();
449 public void deleteAccount() throws IOException
{
451 context
.getPinHelper().removeRegistrationLockPin();
452 } catch (IOException e
) {
453 logger
.warn("Failed to remove registration lock pin");
455 account
.setRegistrationLockPin(null);
457 dependencies
.getAccountManager().deleteAccount();
459 account
.setRegistered(false);
460 unregisteredListener
.call();
463 public interface Callable
{