1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.CaptchaRequiredException
;
4 import org
.asamk
.signal
.manager
.api
.DeviceLinkUrl
;
5 import org
.asamk
.signal
.manager
.api
.IncorrectPinException
;
6 import org
.asamk
.signal
.manager
.api
.InvalidDeviceLinkException
;
7 import org
.asamk
.signal
.manager
.api
.NonNormalizedPhoneNumberException
;
8 import org
.asamk
.signal
.manager
.api
.PinLockedException
;
9 import org
.asamk
.signal
.manager
.api
.RateLimitException
;
10 import org
.asamk
.signal
.manager
.internal
.SignalDependencies
;
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
.KyberPreKeyRecord
;
18 import org
.signal
.libsignal
.protocol
.state
.SignedPreKeyRecord
;
19 import org
.signal
.libsignal
.usernames
.BaseUsernameException
;
20 import org
.signal
.libsignal
.usernames
.Username
;
21 import org
.slf4j
.Logger
;
22 import org
.slf4j
.LoggerFactory
;
23 import org
.whispersystems
.signalservice
.api
.account
.ChangePhoneNumberRequest
;
24 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
25 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
26 import org
.whispersystems
.signalservice
.api
.push
.ServiceIdType
;
27 import org
.whispersystems
.signalservice
.api
.push
.SignedPreKeyEntity
;
28 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AlreadyVerifiedException
;
29 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.AuthorizationFailedException
;
30 import org
.whispersystems
.signalservice
.api
.push
.exceptions
.DeprecatedVersionException
;
31 import org
.whispersystems
.signalservice
.api
.util
.DeviceNameUtil
;
32 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
33 import org
.whispersystems
.signalservice
.internal
.push
.KyberPreKeyEntity
;
34 import org
.whispersystems
.signalservice
.internal
.push
.OutgoingPushMessage
;
35 import org
.whispersystems
.util
.Base64UrlSafe
;
37 import java
.io
.IOException
;
38 import java
.util
.ArrayList
;
39 import java
.util
.List
;
41 import java
.util
.Objects
;
42 import java
.util
.Optional
;
43 import java
.util
.concurrent
.TimeUnit
;
45 import static org
.whispersystems
.signalservice
.internal
.util
.Util
.isEmpty
;
47 public class AccountHelper
{
49 private final static Logger logger
= LoggerFactory
.getLogger(AccountHelper
.class);
51 private final Context context
;
52 private final SignalAccount account
;
53 private final SignalDependencies dependencies
;
55 private Callable unregisteredListener
;
57 public AccountHelper(final Context context
) {
58 this.account
= context
.getAccount();
59 this.dependencies
= context
.getDependencies();
60 this.context
= context
;
63 public void setUnregisteredListener(final Callable unregisteredListener
) {
64 this.unregisteredListener
= unregisteredListener
;
67 public void checkAccountState() throws IOException
{
68 if (account
.getLastReceiveTimestamp() == 0) {
69 logger
.info("The Signal protocol expects that incoming messages are regularly received.");
71 var diffInMilliseconds
= System
.currentTimeMillis() - account
.getLastReceiveTimestamp();
72 long days
= TimeUnit
.DAYS
.convert(diffInMilliseconds
, TimeUnit
.MILLISECONDS
);
75 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
80 updateAccountAttributes();
81 context
.getPreKeyHelper().refreshPreKeysIfNecessary();
82 if (account
.getAci() == null || account
.getPni() == null) {
85 if (!account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
86 context
.getSyncHelper().requestSyncPniIdentity();
88 if (account
.getPreviousStorageVersion() < 4
89 && account
.isPrimaryDevice()
90 && account
.getRegistrationLockPin() != null) {
91 migrateRegistrationPin();
93 } catch (DeprecatedVersionException e
) {
94 logger
.debug("Signal-Server returned deprecated version exception", e
);
96 } catch (AuthorizationFailedException e
) {
97 account
.setRegistered(false);
102 public void checkWhoAmiI() throws IOException
{
103 final var whoAmI
= dependencies
.getAccountManager().getWhoAmI();
104 final var number
= whoAmI
.getNumber();
105 final var aci
= ACI
.parseOrThrow(whoAmI
.getAci());
106 final var pni
= PNI
.from(UuidUtil
.parseOrThrow(whoAmI
.getPni()));
107 if (number
.equals(account
.getNumber()) && aci
.equals(account
.getAci()) && pni
.equals(account
.getPni())) {
111 updateSelfIdentifiers(number
, aci
, pni
);
114 private void updateSelfIdentifiers(final String number
, final ACI aci
, final PNI pni
) {
115 account
.setNumber(number
);
118 if (account
.isPrimaryDevice() && account
.getPniIdentityKeyPair() == null) {
119 account
.setPniIdentityKeyPair(KeyUtils
.generateIdentityKeyPair());
121 account
.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account
.getSelfRecipientAddress());
122 // TODO check and update remote storage
123 context
.getUnidentifiedAccessHelper().rotateSenderCertificates();
124 dependencies
.resetAfterAddressChange();
125 context
.getGroupV2Helper().clearAuthCredentialCache();
126 context
.getAccountFileUpdater().updateAccountIdentifiers(account
.getNumber(), account
.getAci());
130 final PNI updatedPni
,
131 final IdentityKeyPair pniIdentityKeyPair
,
133 final int localPniRegistrationId
,
134 final SignedPreKeyRecord pniSignedPreKey
,
135 final KyberPreKeyRecord lastResortKyberPreKey
136 ) throws IOException
{
137 updateSelfIdentifiers(number
!= null ? number
: account
.getNumber(), account
.getAci(), updatedPni
);
138 account
.setNewPniIdentity(pniIdentityKeyPair
, pniSignedPreKey
, lastResortKyberPreKey
, localPniRegistrationId
);
139 context
.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType
.PNI
);
142 public void startChangeNumber(
143 String newNumber
, String captcha
, boolean voiceVerification
144 ) throws IOException
, CaptchaRequiredException
, NonNormalizedPhoneNumberException
, RateLimitException
{
145 final var accountManager
= dependencies
.createUnauthenticatedAccountManager(newNumber
, account
.getPassword());
146 String sessionId
= NumberVerificationUtils
.handleVerificationSession(accountManager
,
147 account
.getSessionId(newNumber
),
148 id
-> account
.setSessionId(newNumber
, id
),
151 NumberVerificationUtils
.requestVerificationCode(accountManager
, sessionId
, voiceVerification
);
154 public void finishChangeNumber(
155 String newNumber
, String verificationCode
, String pin
156 ) throws IncorrectPinException
, PinLockedException
, IOException
{
157 // TODO create new PNI identity key
158 final List
<OutgoingPushMessage
> deviceMessages
= null;
159 final Map
<String
, SignedPreKeyEntity
> devicePniSignedPreKeys
= null;
160 final Map
<String
, KyberPreKeyEntity
> devicePniLastResortKyberPrekeys
= null;
161 final Map
<String
, Integer
> pniRegistrationIds
= null;
162 var sessionId
= account
.getSessionId(account
.getNumber());
163 final var result
= NumberVerificationUtils
.verifyNumber(sessionId
,
166 context
.getPinHelper(),
167 (sessionId1
, verificationCode1
, registrationLock
) -> {
168 final var accountManager
= dependencies
.getAccountManager();
170 Utils
.handleResponseException(accountManager
.verifyAccount(verificationCode
, sessionId1
));
171 } catch (AlreadyVerifiedException e
) {
172 // Already verified so can continue changing number
174 return Utils
.handleResponseException(accountManager
.changeNumber(new ChangePhoneNumberRequest(
179 account
.getPniIdentityKeyPair().getPublicKey(),
181 devicePniSignedPreKeys
,
182 devicePniLastResortKyberPrekeys
,
183 pniRegistrationIds
)));
185 // TODO handle response
186 updateSelfIdentifiers(newNumber
, account
.getAci(), PNI
.parseOrThrow(result
.first().getPni()));
189 public static final int USERNAME_MIN_LENGTH
= 3;
190 public static final int USERNAME_MAX_LENGTH
= 32;
192 public String
reserveUsername(String nickname
) throws IOException
, BaseUsernameException
{
193 final var currentUsername
= account
.getUsername();
194 if (currentUsername
!= null) {
195 final var currentNickname
= currentUsername
.substring(0, currentUsername
.indexOf('.'));
196 if (currentNickname
.equals(nickname
)) {
197 refreshCurrentUsername();
198 return currentUsername
;
202 final var candidates
= Username
.candidatesFrom(nickname
, USERNAME_MIN_LENGTH
, USERNAME_MAX_LENGTH
);
203 final var candidateHashes
= new ArrayList
<String
>();
204 for (final var candidate
: candidates
) {
205 candidateHashes
.add(Base64UrlSafe
.encodeBytesWithoutPadding(candidate
.getHash()));
208 final var response
= dependencies
.getAccountManager().reserveUsername(candidateHashes
);
209 final var hashIndex
= candidateHashes
.indexOf(response
.getUsernameHash());
210 if (hashIndex
== -1) {
211 logger
.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
212 throw new IOException("Unexpected username response");
215 logger
.debug("[reserveUsername] Successfully reserved username.");
216 final var username
= candidates
.get(hashIndex
).getUsername();
218 dependencies
.getAccountManager().confirmUsername(username
, response
);
219 account
.setUsername(username
);
220 account
.getRecipientStore().resolveSelfRecipientTrusted(account
.getSelfRecipientAddress());
221 logger
.debug("[confirmUsername] Successfully confirmed username.");
226 public void refreshCurrentUsername() throws IOException
, BaseUsernameException
{
227 final var localUsername
= account
.getUsername();
228 if (localUsername
== null) {
232 final var whoAmIResponse
= dependencies
.getAccountManager().getWhoAmI();
233 final var serverUsernameHash
= whoAmIResponse
.getUsernameHash();
234 final var hasServerUsername
= !isEmpty(serverUsernameHash
);
235 final var localUsernameHash
= Base64UrlSafe
.encodeBytesWithoutPadding(new Username(localUsername
).getHash());
237 if (!hasServerUsername
) {
238 logger
.debug("No remote username is set.");
241 if (!Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
242 logger
.debug("Local username hash does not match server username hash.");
245 if (!hasServerUsername
|| !Objects
.equals(localUsernameHash
, serverUsernameHash
)) {
246 logger
.debug("Attempting to resynchronize username.");
247 tryReserveConfirmUsername(localUsername
, localUsernameHash
);
249 logger
.debug("Username already set, not refreshing.");
253 private void tryReserveConfirmUsername(final String username
, String localUsernameHash
) throws IOException
{
254 final var response
= dependencies
.getAccountManager().reserveUsername(List
.of(localUsernameHash
));
255 logger
.debug("[reserveUsername] Successfully reserved existing username.");
256 dependencies
.getAccountManager().confirmUsername(username
, response
);
257 logger
.debug("[confirmUsername] Successfully confirmed existing username.");
260 public void deleteUsername() throws IOException
{
261 dependencies
.getAccountManager().deleteUsername();
262 account
.setUsername(null);
263 logger
.debug("[deleteUsername] Successfully deleted the username.");
266 public void setDeviceName(String deviceName
) {
267 final var privateKey
= account
.getAciIdentityKeyPair().getPrivateKey();
268 final var encryptedDeviceName
= DeviceNameUtil
.encryptDeviceName(deviceName
, privateKey
);
269 account
.setEncryptedDeviceName(encryptedDeviceName
);
272 public void updateAccountAttributes() throws IOException
{
273 dependencies
.getAccountManager().setAccountAttributes(account
.getAccountAttributes(null));
276 public void addDevice(DeviceLinkUrl deviceLinkInfo
) throws IOException
, InvalidDeviceLinkException
{
277 var verificationCode
= dependencies
.getAccountManager().getNewDeviceVerificationCode();
280 dependencies
.getAccountManager()
281 .addDevice(deviceLinkInfo
.deviceIdentifier(),
282 deviceLinkInfo
.deviceKey(),
283 account
.getAciIdentityKeyPair(),
284 account
.getPniIdentityKeyPair(),
285 account
.getProfileKey(),
287 } catch (InvalidKeyException e
) {
288 throw new InvalidDeviceLinkException("Invalid device link", e
);
290 account
.setMultiDevice(true);
293 public void removeLinkedDevices(int deviceId
) throws IOException
{
294 dependencies
.getAccountManager().removeDevice(deviceId
);
295 var devices
= dependencies
.getAccountManager().getDevices();
296 account
.setMultiDevice(devices
.size() > 1);
299 public void migrateRegistrationPin() throws IOException
{
300 var masterKey
= account
.getOrCreatePinMasterKey();
302 context
.getPinHelper().migrateRegistrationLockPin(account
.getRegistrationLockPin(), masterKey
);
305 public void setRegistrationPin(String pin
) throws IOException
{
306 var masterKey
= account
.getOrCreatePinMasterKey();
308 context
.getPinHelper().setRegistrationLockPin(pin
, masterKey
);
310 account
.setRegistrationLockPin(pin
);
313 public void removeRegistrationPin() throws IOException
{
315 context
.getPinHelper().removeRegistrationLockPin();
317 account
.setRegistrationLockPin(null);
320 public void unregister() throws IOException
{
321 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
322 // If this is the primary device, other users can't send messages to this number anymore.
323 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
324 dependencies
.getAccountManager().setGcmId(Optional
.empty());
326 account
.setRegistered(false);
327 unregisteredListener
.call();
330 public void deleteAccount() throws IOException
{
332 context
.getPinHelper().removeRegistrationLockPin();
333 } catch (IOException e
) {
334 logger
.warn("Failed to remove registration lock pin");
336 account
.setRegistrationLockPin(null);
338 dependencies
.getAccountManager().deleteAccount();
340 account
.setRegistered(false);
341 unregisteredListener
.call();
344 public interface Callable
{