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