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