]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
d49ab67a69a09b4e5c1ee8924c1206bc857a14de
[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 refreshCurrentUsername();
324 return;
325 }
326 }
327
328 final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
329 final var candidateHashes = new ArrayList<String>();
330 for (final var candidate : candidates) {
331 candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
332 }
333
334 final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
335 final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
336 if (hashIndex == -1) {
337 logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
338 throw new IOException("Unexpected username response");
339 }
340
341 logger.debug("[reserveUsername] Successfully reserved username.");
342 final var username = candidates.get(hashIndex);
343
344 dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
345 account.setUsername(username.getUsername());
346 account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
347 logger.debug("[confirmUsername] Successfully confirmed username.");
348 tryToSetUsernameLink(username);
349 }
350
351 public void refreshCurrentUsername() throws IOException, BaseUsernameException {
352 final var localUsername = account.getUsername();
353 if (localUsername == null) {
354 return;
355 }
356
357 final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
358 final var serverUsernameHash = whoAmIResponse.getUsernameHash();
359 final var hasServerUsername = !isEmpty(serverUsernameHash);
360 final var username = new Username(localUsername);
361 final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
362
363 if (!hasServerUsername) {
364 logger.debug("No remote username is set.");
365 }
366
367 if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
368 logger.debug("Local username hash does not match server username hash.");
369 }
370
371 if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
372 logger.debug("Attempting to resynchronize username.");
373 try {
374 tryReserveConfirmUsername(username);
375 } catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) {
376 logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
377 e.getMessage(),
378 e.getClass().getSimpleName());
379 account.setUsername(null);
380 account.setUsernameLink(null);
381 throw e;
382 }
383 } else {
384 logger.debug("Username already set, not refreshing.");
385 }
386 }
387
388 private void tryReserveConfirmUsername(final Username username) throws IOException {
389 final var response = dependencies.getAccountManager()
390 .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
391 logger.debug("[reserveUsername] Successfully reserved existing username.");
392 dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
393 logger.debug("[confirmUsername] Successfully confirmed existing username.");
394 tryToSetUsernameLink(username);
395 }
396
397 private void tryToSetUsernameLink(Username username) {
398 for (var i = 1; i < 4; i++) {
399 try {
400 final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
401 account.setUsernameLink(linkComponents);
402 break;
403 } catch (IOException e) {
404 logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
405 }
406 }
407 }
408
409 public void deleteUsername() throws IOException {
410 dependencies.getAccountManager().deleteUsername();
411 account.setUsername(null);
412 logger.debug("[deleteUsername] Successfully deleted the username.");
413 }
414
415 public void setDeviceName(String deviceName) {
416 final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
417 final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
418 account.setEncryptedDeviceName(encryptedDeviceName);
419 }
420
421 public void updateAccountAttributes() throws IOException {
422 dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
423 }
424
425 public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
426 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
427
428 try {
429 dependencies.getAccountManager()
430 .addDevice(deviceLinkInfo.deviceIdentifier(),
431 deviceLinkInfo.deviceKey(),
432 account.getAciIdentityKeyPair(),
433 account.getPniIdentityKeyPair(),
434 account.getProfileKey(),
435 account.getOrCreatePinMasterKey(),
436 verificationCode);
437 } catch (InvalidKeyException e) {
438 throw new InvalidDeviceLinkException("Invalid device link", e);
439 }
440 account.setMultiDevice(true);
441 }
442
443 public void removeLinkedDevices(int deviceId) throws IOException {
444 dependencies.getAccountManager().removeDevice(deviceId);
445 var devices = dependencies.getAccountManager().getDevices();
446 account.setMultiDevice(devices.size() > 1);
447 }
448
449 public void migrateRegistrationPin() throws IOException {
450 var masterKey = account.getOrCreatePinMasterKey();
451
452 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
453 dependencies.getAccountManager().enableRegistrationLock(masterKey);
454 }
455
456 public void setRegistrationPin(String pin) throws IOException {
457 var masterKey = account.getOrCreatePinMasterKey();
458
459 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
460 dependencies.getAccountManager().enableRegistrationLock(masterKey);
461
462 account.setRegistrationLockPin(pin);
463 }
464
465 public void removeRegistrationPin() throws IOException {
466 // Remove KBS Pin
467 context.getPinHelper().removeRegistrationLockPin();
468 dependencies.getAccountManager().disableRegistrationLock();
469
470 account.setRegistrationLockPin(null);
471 }
472
473 public void unregister() throws IOException {
474 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
475 // If this is the primary device, other users can't send messages to this number anymore.
476 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
477 dependencies.getAccountManager().setGcmId(Optional.empty());
478
479 account.setRegistered(false);
480 unregisteredListener.call();
481 }
482
483 public void deleteAccount() throws IOException {
484 try {
485 context.getPinHelper().removeRegistrationLockPin();
486 } catch (IOException e) {
487 logger.warn("Failed to remove registration lock pin");
488 }
489 account.setRegistrationLockPin(null);
490
491 dependencies.getAccountManager().deleteAccount();
492
493 account.setRegistered(false);
494 unregisteredListener.call();
495 }
496
497 public interface Callable {
498
499 void call();
500 }
501 }