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