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