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