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