]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Update libsignal-service-java
[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.slf4j.Logger;
19 import org.slf4j.LoggerFactory;
20 import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
21 import org.whispersystems.signalservice.api.push.ACI;
22 import org.whispersystems.signalservice.api.push.PNI;
23 import org.whispersystems.signalservice.api.push.ServiceIdType;
24 import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
25 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
26 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
27 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
28 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
29 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
30
31 import java.io.IOException;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Optional;
35 import java.util.concurrent.TimeUnit;
36
37 public class AccountHelper {
38
39 private final static Logger logger = LoggerFactory.getLogger(AccountHelper.class);
40
41 private final Context context;
42 private final SignalAccount account;
43 private final SignalDependencies dependencies;
44
45 private Callable unregisteredListener;
46
47 public AccountHelper(final Context context) {
48 this.account = context.getAccount();
49 this.dependencies = context.getDependencies();
50 this.context = context;
51 }
52
53 public void setUnregisteredListener(final Callable unregisteredListener) {
54 this.unregisteredListener = unregisteredListener;
55 }
56
57 public void checkAccountState() throws IOException {
58 if (account.getLastReceiveTimestamp() == 0) {
59 logger.info("The Signal protocol expects that incoming messages are regularly received.");
60 } else {
61 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
62 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
63 if (days > 7) {
64 logger.warn(
65 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
66 days);
67 }
68 }
69 try {
70 updateAccountAttributes();
71 context.getPreKeyHelper().refreshPreKeysIfNecessary();
72 if (account.getAci() == null || account.getPni() == null) {
73 checkWhoAmiI();
74 }
75 if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
76 context.getSyncHelper().requestSyncPniIdentity();
77 }
78 if (account.getPreviousStorageVersion() < 4
79 && account.isPrimaryDevice()
80 && account.getRegistrationLockPin() != null) {
81 migrateRegistrationPin();
82 }
83 } catch (DeprecatedVersionException e) {
84 logger.debug("Signal-Server returned deprecated version exception", e);
85 throw e;
86 } catch (AuthorizationFailedException e) {
87 account.setRegistered(false);
88 throw e;
89 }
90 }
91
92 public void checkWhoAmiI() throws IOException {
93 final var whoAmI = dependencies.getAccountManager().getWhoAmI();
94 final var number = whoAmI.getNumber();
95 final var aci = ACI.parseOrNull(whoAmI.getAci());
96 final var pni = PNI.parseOrNull(whoAmI.getPni());
97 if (number.equals(account.getNumber()) && aci.equals(account.getAci()) && pni.equals(account.getPni())) {
98 return;
99 }
100
101 updateSelfIdentifiers(number, aci, pni);
102 }
103
104 private void updateSelfIdentifiers(final String number, final ACI aci, final PNI pni) {
105 account.setNumber(number);
106 account.setAci(aci);
107 account.setPni(pni);
108 if (account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null && account.getPni() != null) {
109 account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
110 }
111 account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
112 // TODO check and update remote storage
113 context.getUnidentifiedAccessHelper().rotateSenderCertificates();
114 dependencies.resetAfterAddressChange();
115 context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
116 }
117
118 public void setPni(
119 final PNI updatedPni,
120 final IdentityKeyPair pniIdentityKeyPair,
121 final SignedPreKeyRecord pniSignedPreKey,
122 final int localPniRegistrationId
123 ) throws IOException {
124 account.setPni(updatedPni, pniIdentityKeyPair, pniSignedPreKey, localPniRegistrationId);
125 context.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType.PNI);
126 if (account.getPni() == null || !account.getPni().equals(updatedPni)) {
127 context.getGroupV2Helper().clearAuthCredentialCache();
128 }
129 }
130
131 public void startChangeNumber(
132 String newNumber, String captcha, boolean voiceVerification
133 ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
134 final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
135 String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
136 account.getSessionId(newNumber),
137 id -> account.setSessionId(newNumber, id),
138 voiceVerification,
139 captcha);
140 NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
141 }
142
143 public void finishChangeNumber(
144 String newNumber, String verificationCode, String pin
145 ) throws IncorrectPinException, PinLockedException, IOException {
146 // TODO create new PNI identity key
147 final List<OutgoingPushMessage> deviceMessages = null;
148 final Map<String, SignedPreKeyEntity> devicePniSignedPreKeys = null;
149 final Map<String, Integer> pniRegistrationIds = null;
150 var sessionId = account.getSessionId(account.getNumber());
151 final var result = NumberVerificationUtils.verifyNumber(sessionId,
152 verificationCode,
153 pin,
154 context.getPinHelper(),
155 (sessionId1, verificationCode1, registrationLock) -> {
156 final var accountManager = dependencies.getAccountManager();
157 try {
158 Utils.handleResponseException(accountManager.verifyAccount(verificationCode, sessionId1));
159 } catch (AlreadyVerifiedException e) {
160 // Already verified so can continue changing number
161 }
162 return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
163 sessionId1,
164 null,
165 newNumber,
166 registrationLock,
167 account.getPniIdentityKeyPair().getPublicKey(),
168 deviceMessages,
169 devicePniSignedPreKeys,
170 pniRegistrationIds)));
171 });
172 // TODO handle response
173 updateSelfIdentifiers(newNumber, account.getAci(), PNI.parseOrThrow(result.first().getPni()));
174 }
175
176 public void setDeviceName(String deviceName) {
177 final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
178 final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
179 account.setEncryptedDeviceName(encryptedDeviceName);
180 }
181
182 public void updateAccountAttributes() throws IOException {
183 dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
184 }
185
186 public void addDevice(DeviceLinkInfo deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
187 var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
188
189 try {
190 dependencies.getAccountManager()
191 .addDevice(deviceLinkInfo.deviceIdentifier(),
192 deviceLinkInfo.deviceKey(),
193 account.getAciIdentityKeyPair(),
194 account.getPniIdentityKeyPair(),
195 account.getProfileKey(),
196 verificationCode);
197 } catch (InvalidKeyException e) {
198 throw new InvalidDeviceLinkException("Invalid device link", e);
199 }
200 account.setMultiDevice(true);
201 }
202
203 public void removeLinkedDevices(int deviceId) throws IOException {
204 dependencies.getAccountManager().removeDevice(deviceId);
205 var devices = dependencies.getAccountManager().getDevices();
206 account.setMultiDevice(devices.size() > 1);
207 }
208
209 public void migrateRegistrationPin() throws IOException {
210 var masterKey = account.getOrCreatePinMasterKey();
211
212 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
213 }
214
215 public void setRegistrationPin(String pin) throws IOException {
216 var masterKey = account.getOrCreatePinMasterKey();
217
218 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
219
220 account.setRegistrationLockPin(pin);
221 }
222
223 public void removeRegistrationPin() throws IOException {
224 // Remove KBS Pin
225 context.getPinHelper().removeRegistrationLockPin();
226
227 account.setRegistrationLockPin(null);
228 }
229
230 public void unregister() throws IOException {
231 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
232 // If this is the primary device, other users can't send messages to this number anymore.
233 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
234 dependencies.getAccountManager().setGcmId(Optional.empty());
235
236 account.setRegistered(false);
237 unregisteredListener.call();
238 }
239
240 public void deleteAccount() throws IOException {
241 try {
242 context.getPinHelper().removeRegistrationLockPin();
243 } catch (IOException e) {
244 logger.warn("Failed to remove registration lock pin");
245 }
246 account.setRegistrationLockPin(null);
247
248 dependencies.getAccountManager().deleteAccount();
249
250 account.setRegistered(false);
251 unregisteredListener.call();
252 }
253
254 public interface Callable {
255
256 void call();
257 }
258 }