1 package org
.asamk
.signal
.manager
.helper
;
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
;
31 import java
.io
.IOException
;
32 import java
.util
.List
;
34 import java
.util
.Optional
;
35 import java
.util
.concurrent
.TimeUnit
;
37 public class AccountHelper
{
39 private final static Logger logger
= LoggerFactory
.getLogger(AccountHelper
.class);
41 private final Context context
;
42 private final SignalAccount account
;
43 private final SignalDependencies dependencies
;
45 private Callable unregisteredListener
;
47 public AccountHelper(final Context context
) {
48 this.account
= context
.getAccount();
49 this.dependencies
= context
.getDependencies();
50 this.context
= context
;
53 public void setUnregisteredListener(final Callable unregisteredListener
) {
54 this.unregisteredListener
= unregisteredListener
;
57 public void checkAccountState() throws IOException
{
58 if (account
.getLastReceiveTimestamp() == 0) {
59 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
61 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
62 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
65 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
70 updateAccountAttributes();
71 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
72 if (account
.getAci() == null || account
.getPni() == null) {
75 if (!account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
76 context
.getSyncHelper().requestSyncPniIdentity();
78 if (account
.getPreviousStorageVersion() < 4
79 && account
.isPrimaryDevice()
80 && account
.getRegistrationLockPin() != null) {
81 migrateRegistrationPin();
83 } catch (DeprecatedVersionException e
) {
84 logger
.debug("Signal-Server returned deprecated version exception", e
);
86 } catch (AuthorizationFailedException e
) {
87 account
.setRegistered(false);
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())) {
101 updateSelfIdentifiers(number
, aci
, pni
);
104 private void updateSelfIdentifiers(final String number
, final ACI aci
, final PNI pni
) {
105 account
.setNumber(number
);
108 if (account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null && account
.getPni() != null) {
109 account
.setPniIdentityKeyPair(KeyUtils
.generateIdentityKeyPair());
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());
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();
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
),
140 NumberVerificationUtils
.requestVerificationCode(accountManager
, sessionId
, voiceVerification
);
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
,
154 context
.getPinHelper(),
155 (sessionId1
, verificationCode1
, registrationLock
) -> {
156 final var accountManager
= dependencies
.getAccountManager();
158 Utils
.handleResponseException(accountManager
.verifyAccount(verificationCode
, sessionId1
));
159 } catch (AlreadyVerifiedException e
) {
160 // Already verified so can continue changing number
162 return Utils
.handleResponseException(accountManager
.changeNumber(new ChangePhoneNumberRequest(
167 account
.getPniIdentityKeyPair().getPublicKey(),
169 devicePniSignedPreKeys
,
170 pniRegistrationIds
)));
172 // TODO handle response
173 updateSelfIdentifiers(newNumber
, account
.getAci(), PNI
.parseOrThrow(result
.first().getPni()));
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
);
182 public void updateAccountAttributes() throws IOException
{
183 dependencies
.getAccountManager().setAccountAttributes(account
.getAccountAttributes(null));
186 public void addDevice(DeviceLinkInfo deviceLinkInfo
) throws IOException
, InvalidDeviceLinkException
{
187 var verificationCode
= dependencies
.getAccountManager().getNewDeviceVerificationCode();
190 dependencies
.getAccountManager()
191 .addDevice(deviceLinkInfo
.deviceIdentifier(),
192 deviceLinkInfo
.deviceKey(),
193 account
.getAciIdentityKeyPair(),
194 account
.getPniIdentityKeyPair(),
195 account
.getProfileKey(),
197 } catch (InvalidKeyException e
) {
198 throw new InvalidDeviceLinkException("Invalid device link", e
);
200 account
.setMultiDevice(true);
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);
209 public void migrateRegistrationPin() throws IOException
{
210 var masterKey
= account
.getOrCreatePinMasterKey();
212 context
.getPinHelper().migrateRegistrationLockPin(account
.getRegistrationLockPin(), masterKey
);
215 public void setRegistrationPin(String pin
) throws IOException
{
216 var masterKey
= account
.getOrCreatePinMasterKey();
218 context
.getPinHelper().setRegistrationLockPin(pin
, masterKey
);
220 account
.setRegistrationLockPin(pin
);
223 public void removeRegistrationPin() throws IOException
{
225 context
.getPinHelper().removeRegistrationLockPin();
227 account
.setRegistrationLockPin(null);
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());
236 account
.setRegistered(false);
237 unregisteredListener
.call();
240 public void deleteAccount() throws IOException
{
242 context
.getPinHelper().removeRegistrationLockPin();
243 } catch (IOException e
) {
244 logger
.warn("Failed to remove registration lock pin");
246 account
.setRegistrationLockPin(null);
248 dependencies
.getAccountManager().deleteAccount();
250 account
.setRegistered(false);
251 unregisteredListener
.call();
254 public interface Callable
{