]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
f2002347fd652a7cb890515f20f2bbdb930d6834
[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.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;
41
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;
49
50 import okio.ByteString;
51
52 import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
53 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
54
55 public class AccountHelper {
56
57 private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
58
59 private final Context context;
60 private final SignalAccount account;
61 private final SignalDependencies dependencies;
62
63 private Callable unregisteredListener;
64
65 public AccountHelper(final Context context) {
66 this.account = context.getAccount();
67 this.dependencies = context.getDependencies();
68 this.context = context;
69 }
70
71 public void setUnregisteredListener(final Callable unregisteredListener) {
72 this.unregisteredListener = unregisteredListener;
73 }
74
75 public void checkAccountState() throws IOException {
76 if (account.getLastReceiveTimestamp() == 0) {
77 logger.info("The Signal protocol expects that incoming messages are regularly received.");
78 } else {
79 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
80 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
81 if (days > 7) {
82 logger.warn(
83 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
84 days);
85 }
86 }
87 try {
88 updateAccountAttributes();
89 context.getPreKeyHelper().refreshPreKeysIfNecessary();
90 if (account.getAci() == null || account.getPni() == null) {
91 checkWhoAmiI();
92 }
93 if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
94 context.getSyncHelper().requestSyncPniIdentity();
95 }
96 if (account.getPreviousStorageVersion() < 4
97 && account.isPrimaryDevice()
98 && account.getRegistrationLockPin() != null) {
99 migrateRegistrationPin();
100 }
101 } catch (DeprecatedVersionException e) {
102 logger.debug("Signal-Server returned deprecated version exception", e);
103 throw e;
104 } catch (AuthorizationFailedException e) {
105 account.setRegistered(false);
106 throw e;
107 }
108 }
109
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())) {
116 return;
117 }
118
119 updateSelfIdentifiers(number, aci, pni);
120 }
121
122 private void updateSelfIdentifiers(final String number, final ACI aci, final PNI pni) {
123 account.setNumber(number);
124 account.setAci(aci);
125 account.setPni(pni);
126 if (account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
127 account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
128 }
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());
135 }
136
137 public void setPni(
138 final PNI updatedPni,
139 final IdentityKeyPair pniIdentityKeyPair,
140 final String number,
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);
148 }
149
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),
157 voiceVerification,
158 captcha);
159 NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
160 }
161
162 public void finishChangeNumber(
163 String newNumber, String verificationCode, String pin
164 ) throws IncorrectPinException, PinLockedException, IOException {
165 for (var attempts = 0; attempts < 5; attempts++) {
166 try {
167 finishChangeNumberInternal(newNumber, verificationCode, pin);
168 break;
169 } catch (MismatchedDevicesException e) {
170 logger.debug("Change number failed with mismatched devices, retrying.");
171 try {
172 dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
173 } catch (UntrustedIdentityException ex) {
174 throw new AssertionError(ex);
175 }
176 }
177 }
178 }
179
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>();
188
189 final var selfDeviceId = account.getDeviceId();
190 SyncMessage.PniChangeNumber selfChangeNumber = null;
191
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())
197 .stream()
198 .filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
199 deviceId)))
200 .toList();
201 deviceIds.addAll(subDeviceSessions);
202
203 final var messageSender = dependencies.getMessageSender();
204 for (final var deviceId : deviceIds) {
205 // Signed Prekey
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);
212
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);
220
221 // Registration Id
222 var pniRegistrationId = -1;
223 while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
224 pniRegistrationId = KeyHelper.generateRegistrationId(false);
225 }
226 pniRegistrationIds.put(deviceId, pniRegistrationId);
227
228 // Device Message
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)
234 .newE164(newNumber)
235 .build();
236
237 if (deviceId == selfDeviceId) {
238 selfChangeNumber = pniChangeNumber;
239 } else {
240 try {
241 final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
242 pniChangeNumber);
243 encryptedDeviceMessages.add(message);
244 } catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
245 throw new RuntimeException(e);
246 }
247 }
248 }
249
250 final var sessionId = account.getSessionId(newNumber);
251 final var result = NumberVerificationUtils.verifyNumber(sessionId,
252 verificationCode,
253 pin,
254 context.getPinHelper(),
255 (sessionId1, verificationCode1, registrationLock) -> {
256 final var accountManager = dependencies.getAccountManager();
257 try {
258 Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1));
259 } catch (AlreadyVerifiedException e) {
260 // Already verified so can continue changing number
261 }
262 return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
263 sessionId1,
264 null,
265 newNumber,
266 registrationLock,
267 pniIdentity.getPublicKey(),
268 encryptedDeviceMessages,
269 Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
270 Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
271 Utils.mapKeys(pniRegistrationIds, Object::toString))));
272 });
273
274 final var updatePni = PNI.parseOrThrow(result.first().getPni());
275 if (updatePni.equals(account.getPni())) {
276 logger.debug("PNI is unchanged after change number");
277 return;
278 }
279
280 handlePniChangeNumberMessage(selfChangeNumber, updatePni);
281 }
282
283 public void handlePniChangeNumberMessage(
284 final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
285 ) {
286 if (pniChangeNumber.identityKeyPair != null
287 && pniChangeNumber.registrationId != null
288 && pniChangeNumber.signedPreKey != null) {
289 logger.debug("New PNI: {}", updatedPni);
290 try {
291 setPni(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())
298 : null);
299 } catch (Exception e) {
300 logger.warn("Failed to handle change number message", e);
301 }
302 }
303 }
304
305 public static final int USERNAME_MIN_LENGTH = 3;
306 public static final int USERNAME_MAX_LENGTH = 32;
307
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;
315 }
316 }
317
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()));
322 }
323
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");
329 }
330
331 logger.debug("[reserveUsername] Successfully reserved username.");
332 final var username = candidates.get(hashIndex).getUsername();
333
334 dependencies.getAccountManager().confirmUsername(username, response);
335 account.setUsername(username);
336 account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
337 logger.debug("[confirmUsername] Successfully confirmed username.");
338
339 return username;
340 }
341
342 public void refreshCurrentUsername() throws IOException, BaseUsernameException {
343 final var localUsername = account.getUsername();
344 if (localUsername == null) {
345 return;
346 }
347
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());
352
353 if (!hasServerUsername) {
354 logger.debug("No remote username is set.");
355 }
356
357 if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
358 logger.debug("Local username hash does not match server username hash.");
359 }
360
361 if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
362 logger.debug("Attempting to resynchronize username.");
363 tryReserveConfirmUsername(localUsername, localUsernameHash);
364 } else {
365 logger.debug("Username already set, not refreshing.");
366 }
367 }
368
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.");
374 }
375
376 public void deleteUsername() throws IOException {
377 dependencies.getAccountManager().deleteUsername();
378 account.setUsername(null);
379 logger.debug("[deleteUsername] Successfully deleted the username.");
380 }
381
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);
386 }
387
388 public void updateAccountAttributes() throws IOException {
389 dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
390 }
391
392 public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
393 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
394
395 try {
396 dependencies.getAccountManager()
397 .addDevice(deviceLinkInfo.deviceIdentifier(),
398 deviceLinkInfo.deviceKey(),
399 account.getAciIdentityKeyPair(),
400 account.getPniIdentityKeyPair(),
401 account.getProfileKey(),
402 verificationCode);
403 } catch (InvalidKeyException e) {
404 throw new InvalidDeviceLinkException("Invalid device link", e);
405 }
406 account.setMultiDevice(true);
407 }
408
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);
413 }
414
415 public void migrateRegistrationPin() throws IOException {
416 var masterKey = account.getOrCreatePinMasterKey();
417
418 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
419 dependencies.getAccountManager().enableRegistrationLock(masterKey);
420 }
421
422 public void setRegistrationPin(String pin) throws IOException {
423 var masterKey = account.getOrCreatePinMasterKey();
424
425 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
426 dependencies.getAccountManager().enableRegistrationLock(masterKey);
427
428 account.setRegistrationLockPin(pin);
429 }
430
431 public void removeRegistrationPin() throws IOException {
432 // Remove KBS Pin
433 context.getPinHelper().removeRegistrationLockPin();
434 dependencies.getAccountManager().disableRegistrationLock();
435
436 account.setRegistrationLockPin(null);
437 }
438
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());
444
445 account.setRegistered(false);
446 unregisteredListener.call();
447 }
448
449 public void deleteAccount() throws IOException {
450 try {
451 context.getPinHelper().removeRegistrationLockPin();
452 } catch (IOException e) {
453 logger.warn("Failed to remove registration lock pin");
454 }
455 account.setRegistrationLockPin(null);
456
457 dependencies.getAccountManager().deleteAccount();
458
459 account.setRegistered(false);
460 unregisteredListener.call();
461 }
462
463 public interface Callable {
464
465 void call();
466 }
467 }