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
.config
.ServiceConfig
;
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
.signal
.libsignal
.protocol
.IdentityKeyPair
;
15 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
16 import org
.signal
.libsignal
.protocol
.state
.SignedPreKeyRecord
;
17 import org
.slf4j
.Logger
;
18 import org
.slf4j
.LoggerFactory
;
19 import org
.whispersystems
.signalservice
.api
.account
.ChangePhoneNumberRequest
;
20 import org
.whispersystems
.signalservice
.api
.push
.ACI
;
21 import org
.whispersystems
.signalservice
.api
.push
.PNI
;
22 import org
.whispersystems
.signalservice
.api
.push
.ServiceIdType
;
23 import org
.whispersystems
.signalservice
.api
.push
.SignedPreKeyEntity
;
24 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AuthorizationFailedException
;
25 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.DeprecatedVersionException
;
26 import org
.whispersystems
.signalservice
.api
.util
.DeviceNameUtil
;
27 import org
.whispersystems
.signalservice
.internal
.push
.OutgoingPushMessage
;
29 import java
.io
.IOException
;
30 import java
.util
.List
;
32 import java
.util
.Optional
;
33 import java
.util
.concurrent
.TimeUnit
;
35 public class AccountHelper
{
37 private final static Logger logger
= LoggerFactory
.getLogger(AccountHelper
.class);
39 private final Context context
;
40 private final SignalAccount account
;
41 private final SignalDependencies dependencies
;
43 private Callable unregisteredListener
;
45 public AccountHelper(final Context context
) {
46 this.account
= context
.getAccount();
47 this.dependencies
= context
.getDependencies();
48 this.context
= context
;
51 public void setUnregisteredListener(final Callable unregisteredListener
) {
52 this.unregisteredListener
= unregisteredListener
;
55 public void checkAccountState() throws IOException
{
56 if (account
.getLastReceiveTimestamp() == 0) {
57 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
59 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
60 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
63 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
68 updateAccountAttributes();
69 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
70 if (account
.getAci() == null || account
.getPni() == null) {
73 if (!account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
74 context
.getSyncHelper().requestSyncPniIdentity();
76 if (account
.getPreviousStorageVersion() < 4
77 && account
.isPrimaryDevice()
78 && account
.getRegistrationLockPin() != null) {
79 migrateRegistrationPin();
81 } catch (DeprecatedVersionException e
) {
82 logger
.debug("Signal-Server returned deprecated version exception", e
);
84 } catch (AuthorizationFailedException e
) {
85 account
.setRegistered(false);
90 public void checkWhoAmiI() throws IOException
{
91 final var whoAmI
= dependencies
.getAccountManager().getWhoAmI();
92 final var number
= whoAmI
.getNumber();
93 final var aci
= ACI
.parseOrNull(whoAmI
.getAci());
94 final var pni
= PNI
.parseOrNull(whoAmI
.getPni());
95 if (number
.equals(account
.getNumber()) && aci
.equals(account
.getAci()) && pni
.equals(account
.getPni())) {
99 updateSelfIdentifiers(number
, aci
, pni
);
102 private void updateSelfIdentifiers(final String number
, final ACI aci
, final PNI pni
) {
103 account
.setNumber(number
);
106 if (account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null && account
.getPni() != null) {
107 account
.setPniIdentityKeyPair(KeyUtils
.generateIdentityKeyPair());
109 account
.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account
.getSelfRecipientAddress());
110 // TODO check and update remote storage
111 context
.getUnidentifiedAccessHelper().rotateSenderCertificates();
112 dependencies
.resetAfterAddressChange();
113 dependencies
.getSignalWebSocket().forceNewWebSockets();
114 context
.getAccountFileUpdater().updateAccountIdentifiers(account
.getNumber(), account
.getAci());
118 final PNI updatedPni
,
119 final IdentityKeyPair pniIdentityKeyPair
,
120 final SignedPreKeyRecord pniSignedPreKey
,
121 final int localPniRegistrationId
122 ) throws IOException
{
123 account
.setPni(updatedPni
, pniIdentityKeyPair
, pniSignedPreKey
, localPniRegistrationId
);
124 context
.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType
.PNI
);
125 if (account
.getPni() == null || !account
.getPni().equals(updatedPni
)) {
126 context
.getGroupV2Helper().clearAuthCredentialCache();
130 public void startChangeNumber(
131 String newNumber
, String captcha
, boolean voiceVerification
132 ) throws IOException
, CaptchaRequiredException
, NonNormalizedPhoneNumberException
{
133 final var accountManager
= dependencies
.createUnauthenticatedAccountManager(newNumber
, account
.getPassword());
134 NumberVerificationUtils
.requestVerificationCode(accountManager
, captcha
, voiceVerification
);
137 public void finishChangeNumber(
138 String newNumber
, String verificationCode
, String pin
139 ) throws IncorrectPinException
, PinLockedException
, IOException
{
140 // TODO create new PNI identity key
141 final List
<OutgoingPushMessage
> deviceMessages
= null;
142 final Map
<String
, SignedPreKeyEntity
> devicePniSignedPreKeys
= null;
143 final Map
<String
, Integer
> pniRegistrationIds
= null;
144 final var result
= NumberVerificationUtils
.verifyNumber(verificationCode
,
146 context
.getPinHelper(),
147 (verificationCode1
, registrationLock
) -> dependencies
.getAccountManager()
148 .changeNumber(new ChangePhoneNumberRequest(newNumber
,
151 account
.getPniIdentityKeyPair().getPublicKey(),
153 devicePniSignedPreKeys
,
154 pniRegistrationIds
)));
155 // TODO handle response
156 updateSelfIdentifiers(newNumber
, account
.getAci(), PNI
.parseOrThrow(result
.first().getPni()));
159 public void setDeviceName(String deviceName
) {
160 final var privateKey
= account
.getAciIdentityKeyPair().getPrivateKey();
161 final var encryptedDeviceName
= DeviceNameUtil
.encryptDeviceName(deviceName
, privateKey
);
162 account
.setEncryptedDeviceName(encryptedDeviceName
);
165 public void updateAccountAttributes() throws IOException
{
166 dependencies
.getAccountManager()
167 .setAccountAttributes(null,
168 account
.getLocalRegistrationId(),
171 account
.getRegistrationLock(),
172 account
.getSelfUnidentifiedAccessKey(),
173 account
.isUnrestrictedUnidentifiedAccess(),
174 ServiceConfig
.capabilities
,
175 account
.isDiscoverableByPhoneNumber(),
176 account
.getEncryptedDeviceName(),
177 account
.getLocalPniRegistrationId());
180 public void addDevice(DeviceLinkInfo deviceLinkInfo
) throws IOException
, InvalidDeviceLinkException
{
181 var verificationCode
= dependencies
.getAccountManager().getNewDeviceVerificationCode();
184 dependencies
.getAccountManager()
185 .addDevice(deviceLinkInfo
.deviceIdentifier(),
186 deviceLinkInfo
.deviceKey(),
187 account
.getAciIdentityKeyPair(),
188 account
.getPniIdentityKeyPair(),
189 account
.getProfileKey(),
191 } catch (InvalidKeyException e
) {
192 throw new InvalidDeviceLinkException("Invalid device link", e
);
194 account
.setMultiDevice(true);
197 public void removeLinkedDevices(int deviceId
) throws IOException
{
198 dependencies
.getAccountManager().removeDevice(deviceId
);
199 var devices
= dependencies
.getAccountManager().getDevices();
200 account
.setMultiDevice(devices
.size() > 1);
203 public void migrateRegistrationPin() throws IOException
{
204 var masterKey
= account
.getOrCreatePinMasterKey();
206 context
.getPinHelper().migrateRegistrationLockPin(account
.getRegistrationLockPin(), masterKey
);
209 public void setRegistrationPin(String pin
) throws IOException
{
210 var masterKey
= account
.getOrCreatePinMasterKey();
212 context
.getPinHelper().setRegistrationLockPin(pin
, masterKey
);
214 account
.setRegistrationLockPin(pin
);
217 public void removeRegistrationPin() throws IOException
{
219 context
.getPinHelper().removeRegistrationLockPin();
221 account
.setRegistrationLockPin(null);
224 public void unregister() throws IOException
{
225 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
226 // If this is the primary device, other users can't send messages to this number anymore.
227 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
228 dependencies
.getAccountManager().setGcmId(Optional
.empty());
230 account
.setRegistered(false);
231 unregisteredListener
.call();
234 public void deleteAccount() throws IOException
{
236 context
.getPinHelper().removeRegistrationLockPin();
237 } catch (IOException e
) {
238 logger
.warn("Failed to remove registration lock pin");
240 account
.setRegistrationLockPin(null);
242 dependencies
.getAccountManager().deleteAccount();
244 account
.setRegistered(false);
245 unregisteredListener
.call();
248 public interface Callable
{