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