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