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