]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Update dependencies
[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.state.SignedPreKeyRecord;
18 import org.signal.libsignal.usernames.BaseUsernameException;
19 import org.signal.libsignal.usernames.Username;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22 import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
23 import org.whispersystems.signalservice.api.push.ACI;
24 import org.whispersystems.signalservice.api.push.PNI;
25 import org.whispersystems.signalservice.api.push.ServiceIdType;
26 import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
27 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
28 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
29 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
30 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
31 import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
32 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
33 import org.whispersystems.util.Base64UrlSafe;
34
35 import java.io.IOException;
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Optional;
41 import java.util.concurrent.TimeUnit;
42
43 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
44
45 public class AccountHelper {
46
47 private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
48
49 private final Context context;
50 private final SignalAccount account;
51 private final SignalDependencies dependencies;
52
53 private Callable unregisteredListener;
54
55 public AccountHelper(final Context context) {
56 this.account = context.getAccount();
57 this.dependencies = context.getDependencies();
58 this.context = context;
59 }
60
61 public void setUnregisteredListener(final Callable unregisteredListener) {
62 this.unregisteredListener = unregisteredListener;
63 }
64
65 public void checkAccountState() throws IOException {
66 if (account.getLastReceiveTimestamp() == 0) {
67 logger.info("The Signal protocol expects that incoming messages are regularly received.");
68 } else {
69 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
70 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
71 if (days > 7) {
72 logger.warn(
73 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
74 days);
75 }
76 }
77 try {
78 updateAccountAttributes();
79 context.getPreKeyHelper().refreshPreKeysIfNecessary();
80 if (account.getAci() == null || account.getPni() == null) {
81 checkWhoAmiI();
82 }
83 if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
84 context.getSyncHelper().requestSyncPniIdentity();
85 }
86 if (account.getPreviousStorageVersion() < 4
87 && account.isPrimaryDevice()
88 && account.getRegistrationLockPin() != null) {
89 migrateRegistrationPin();
90 }
91 } catch (DeprecatedVersionException e) {
92 logger.debug("Signal-Server returned deprecated version exception", e);
93 throw e;
94 } catch (AuthorizationFailedException e) {
95 account.setRegistered(false);
96 throw e;
97 }
98 }
99
100 public void checkWhoAmiI() throws IOException {
101 final var whoAmI = dependencies.getAccountManager().getWhoAmI();
102 final var number = whoAmI.getNumber();
103 final var aci = ACI.parseOrNull(whoAmI.getAci());
104 final var pni = PNI.parseOrNull(whoAmI.getPni());
105 if (number.equals(account.getNumber()) && aci.equals(account.getAci()) && pni.equals(account.getPni())) {
106 return;
107 }
108
109 updateSelfIdentifiers(number, aci, pni);
110 }
111
112 private void updateSelfIdentifiers(final String number, final ACI aci, final PNI pni) {
113 account.setNumber(number);
114 account.setAci(aci);
115 account.setPni(pni);
116 if (account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null && account.getPni() != null) {
117 account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
118 }
119 account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
120 // TODO check and update remote storage
121 context.getUnidentifiedAccessHelper().rotateSenderCertificates();
122 dependencies.resetAfterAddressChange();
123 context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
124 }
125
126 public void setPni(
127 final PNI updatedPni,
128 final IdentityKeyPair pniIdentityKeyPair,
129 final SignedPreKeyRecord pniSignedPreKey,
130 final int localPniRegistrationId
131 ) throws IOException {
132 account.setPni(updatedPni, pniIdentityKeyPair, pniSignedPreKey, localPniRegistrationId);
133 context.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType.PNI);
134 if (account.getPni() == null || !account.getPni().equals(updatedPni)) {
135 context.getGroupV2Helper().clearAuthCredentialCache();
136 }
137 }
138
139 public void startChangeNumber(
140 String newNumber, String captcha, boolean voiceVerification
141 ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
142 final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
143 String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
144 account.getSessionId(newNumber),
145 id -> account.setSessionId(newNumber, id),
146 voiceVerification,
147 captcha);
148 NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
149 }
150
151 public void finishChangeNumber(
152 String newNumber, String verificationCode, String pin
153 ) throws IncorrectPinException, PinLockedException, IOException {
154 // TODO create new PNI identity key
155 final List<OutgoingPushMessage> deviceMessages = null;
156 final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
157 final Map<String, KyberPreKeyEntity> devicePniLastResortKyberPrekeys = null;
158 final Map<String, Integer> pniRegistrationIds = null;
159 var sessionId = account.getSessionId(account.getNumber());
160 final var result = NumberVerificationUtils.verifyNumber(sessionId,
161 verificationCode,
162 pin,
163 context.getPinHelper(),
164 (sessionId1, verificationCode1, registrationLock) -> {
165 final var accountManager = dependencies.getAccountManager();
166 try {
167 Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
168 } catch (AlreadyVerifiedException e) {
169 // Already verified so can continue changing number
170 }
171 return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
172 sessionId1,
173 null,
174 newNumber,
175 registrationLock,
176 account.getPniIdentityKeyPair().getPublicKey(),
177 deviceMessages,
178 devicePniSignedPreKeys,
179 devicePniLastResortKyberPrekeys,
180 pniRegistrationIds)));
181 });
182 // TODO handle response
183 updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
184 }
185
186 public static final int USERNAME_MIN_LENGTH = 3;
187 public static final int USERNAME_MAX_LENGTH = 32;
188
189 public String reserveUsername(String nickname) throws IOException, BaseUsernameException {
190 final var currentUsername = account.getUsername();
191 if (currentUsername != null) {
192 final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
193 if (currentNickname.equals(nickname)) {
194 refreshCurrentUsername();
195 return currentUsername;
196 }
197 }
198
199 final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
200 final var candidateHashes = new ArrayList<String>();
201 for (final var candidate : candidates) {
202 candidateHashes.add(Base64UrlSafe.encodeBytesWithoutPadding(candidate.getHash()));
203 }
204
205 final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
206 final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
207 if (hashIndex == -1) {
208 logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
209 throw new IOException("Unexpected username response");
210 }
211
212 logger.debug("[reserveUsername] Successfully reserved username.");
213 final var username = candidates.get(hashIndex).getUsername();
214
215 dependencies.getAccountManager().confirmUsername(username, response);
216 account.setUsername(username);
217 account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
218 logger.debug("[confirmUsername] Successfully confirmed username.");
219
220 return username;
221 }
222
223 public void refreshCurrentUsername() throws IOException, BaseUsernameException {
224 final var localUsername = account.getUsername();
225 if (localUsername == null) {
226 return;
227 }
228
229 final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
230 final var serverUsernameHash = whoAmIResponse.getUsernameHash();
231 final var hasServerUsername = !isEmpty(serverUsernameHash);
232 final var localUsernameHash = Base64UrlSafe.encodeBytesWithoutPadding(new Username(localUsername).getHash());
233
234 if (!hasServerUsername) {
235 logger.debug("No remote username is set.");
236 }
237
238 if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
239 logger.debug("Local username hash does not match server username hash.");
240 }
241
242 if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
243 logger.debug("Attempting to resynchronize username.");
244 tryReserveConfirmUsername(localUsername, localUsernameHash);
245 } else {
246 logger.debug("Username already set, not refreshing.");
247 }
248 }
249
250 private void tryReserveConfirmUsername(final String username, String localUsernameHash) throws IOException {
251 final var response = dependencies.getAccountManager().reserveUsername(List.of(localUsernameHash));
252 logger.debug("[reserveUsername] Successfully reserved existing username.");
253 dependencies.getAccountManager().confirmUsername(username, response);
254 logger.debug("[confirmUsername] Successfully confirmed existing username.");
255 }
256
257 public void deleteUsername() throws IOException {
258 dependencies.getAccountManager().deleteUsername();
259 account.setUsername(null);
260 logger.debug("[deleteUsername] Successfully deleted the username.");
261 }
262
263 public void setDeviceName(String deviceName) {
264 final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
265 final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
266 account.setEncryptedDeviceName(encryptedDeviceName);
267 }
268
269 public void updateAccountAttributes() throws IOException {
270 dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
271 }
272
273 public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
274 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
275
276 try {
277 dependencies.getAccountManager()
278 .addDevice(deviceLinkInfo.deviceIdentifier(),
279 deviceLinkInfo.deviceKey(),
280 account.getAciIdentityKeyPair(),
281 account.getPniIdentityKeyPair(),
282 account.getProfileKey(),
283 verificationCode);
284 } catch (InvalidKeyException e) {
285 throw new InvalidDeviceLinkException("Invalid device link", e);
286 }
287 account.setMultiDevice(true);
288 }
289
290 public void removeLinkedDevices(int deviceId) throws IOException {
291 dependencies.getAccountManager().removeDevice(deviceId);
292 var devices = dependencies.getAccountManager().getDevices();
293 account.setMultiDevice(devices.size() > 1);
294 }
295
296 public void migrateRegistrationPin() throws IOException {
297 var masterKey = account.getOrCreatePinMasterKey();
298
299 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
300 }
301
302 public void setRegistrationPin(String pin) throws IOException {
303 var masterKey = account.getOrCreatePinMasterKey();
304
305 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
306
307 account.setRegistrationLockPin(pin);
308 }
309
310 public void removeRegistrationPin() throws IOException {
311 // Remove KBS Pin
312 context.getPinHelper().removeRegistrationLockPin();
313
314 account.setRegistrationLockPin(null);
315 }
316
317 public void unregister() throws IOException {
318 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
319 // If this is the primary device, other users can't send messages to this number anymore.
320 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
321 dependencies.getAccountManager().setGcmId(Optional.empty());
322
323 account.setRegistered(false);
324 unregisteredListener.call();
325 }
326
327 public void deleteAccount() throws IOException {
328 try {
329 context.getPinHelper().removeRegistrationLockPin();
330 } catch (IOException e) {
331 logger.warn("Failed to remove registration lock pin");
332 }
333 account.setRegistrationLockPin(null);
334
335 dependencies.getAccountManager().deleteAccount();
336
337 account.setRegistered(false);
338 unregisteredListener.call();
339 }
340
341 public interface Callable {
342
343 void call();
344 }
345 }