]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Update libsignal-service
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / AccountHelper.java
index d49ab67a69a09b4e5c1ee8924c1206bc857a14de..7d40f99c41114386265e6914052e1cf388f7cee8 100644 (file)
@@ -3,11 +3,12 @@ package org.asamk.signal.manager.helper;
 import org.asamk.signal.manager.api.CaptchaRequiredException;
 import org.asamk.signal.manager.api.DeviceLinkUrl;
 import org.asamk.signal.manager.api.IncorrectPinException;
-import org.asamk.signal.manager.api.InvalidDeviceLinkException;
 import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
 import org.asamk.signal.manager.api.PinLockedException;
 import org.asamk.signal.manager.api.RateLimitException;
+import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
 import org.asamk.signal.manager.internal.SignalDependencies;
+import org.asamk.signal.manager.jobs.SyncStorageJob;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.util.KeyUtils;
 import org.asamk.signal.manager.util.NumberVerificationUtils;
@@ -25,11 +26,13 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
+import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse;
 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.push.ServiceIdType;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
+import org.whispersystems.signalservice.api.push.UsernameLinkComponents;
 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
@@ -37,6 +40,7 @@ import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReserve
 import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
 import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
+import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
 import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
 import org.whispersystems.signalservice.internal.push.SyncMessage;
@@ -47,12 +51,13 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
-import java.util.Optional;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 import okio.ByteString;
 
 import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
+import static org.asamk.signal.manager.util.Utils.handleResponseException;
 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
 
 public class AccountHelper {
@@ -89,12 +94,16 @@ public class AccountHelper {
         }
         try {
             updateAccountAttributes();
-            context.getPreKeyHelper().refreshPreKeysIfNecessary();
+            if (account.getPreviousStorageVersion() < 9) {
+                context.getPreKeyHelper().forceRefreshPreKeys();
+            } else {
+                context.getPreKeyHelper().refreshPreKeysIfNecessary();
+            }
             if (account.getAci() == null || account.getPni() == null) {
                 checkWhoAmiI();
             }
             if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
-                context.getSyncHelper().requestSyncPniIdentity();
+                throw new IOException("Missing PNI identity key, relinking required");
             }
             if (account.getPreviousStorageVersion() < 4
                     && account.isPrimaryDevice()
@@ -137,11 +146,11 @@ public class AccountHelper {
             account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
         }
         account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
-        // TODO check and update remote storage
         context.getUnidentifiedAccessHelper().rotateSenderCertificates();
         dependencies.resetAfterAddressChange();
         context.getGroupV2Helper().clearAuthCredentialCache();
         context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
+        context.getJobExecutor().enqueueJob(new SyncStorageJob());
     }
 
     public void setPni(
@@ -158,19 +167,24 @@ public class AccountHelper {
     }
 
     public void startChangeNumber(
-            String newNumber, boolean voiceVerification, String captcha
-    ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException {
+            String newNumber,
+            boolean voiceVerification,
+            String captcha
+    ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
         final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
-        String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
+        final var registrationApi = accountManager.getRegistrationApi();
+        String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
                 account.getSessionId(newNumber),
                 id -> account.setSessionId(newNumber, id),
                 voiceVerification,
                 captcha);
-        NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
+        NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
     }
 
     public void finishChangeNumber(
-            String newNumber, String verificationCode, String pin
+            String newNumber,
+            String verificationCode,
+            String pin
     ) throws IncorrectPinException, PinLockedException, IOException {
         for (var attempts = 0; attempts < 5; attempts++) {
             try {
@@ -188,7 +202,9 @@ public class AccountHelper {
     }
 
     private void finishChangeNumberInternal(
-            String newNumber, String verificationCode, String pin
+            String newNumber,
+            String verificationCode,
+            String pin
     ) throws IncorrectPinException, PinLockedException, IOException {
         final var pniIdentity = KeyUtils.generateIdentityKeyPair();
         final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
@@ -213,20 +229,30 @@ public class AccountHelper {
         final var messageSender = dependencies.getMessageSender();
         for (final var deviceId : deviceIds) {
             // Signed Prekey
-            final var signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
-                    pniIdentity.getPrivateKey());
-            final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
-                    signedPreKeyRecord.getKeyPair().getPublicKey(),
-                    signedPreKeyRecord.getSignature());
-            devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
+            final SignedPreKeyRecord signedPreKeyRecord;
+            try {
+                signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
+                        pniIdentity.getPrivateKey());
+                final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
+                        signedPreKeyRecord.getKeyPair().getPublicKey(),
+                        signedPreKeyRecord.getSignature());
+                devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
+            } catch (InvalidKeyException e) {
+                throw new AssertionError("unexpected invalid key", e);
+            }
 
             // Last-resort kyber prekey
-            final var lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(
-                    PREKEY_MAXIMUM_ID), pniIdentity.getPrivateKey());
-            final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
-                    lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
-                    lastResortKyberPreKeyRecord.getSignature());
-            devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
+            final KyberPreKeyRecord lastResortKyberPreKeyRecord;
+            try {
+                lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
+                        pniIdentity.getPrivateKey());
+                final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
+                        lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
+                        lastResortKyberPreKeyRecord.getSignature());
+                devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
+            } catch (InvalidKeyException e) {
+                throw new AssertionError("unexpected invalid key", e);
+            }
 
             // Registration Id
             var pniRegistrationId = -1;
@@ -263,14 +289,14 @@ public class AccountHelper {
                 pin,
                 context.getPinHelper(),
                 (sessionId1, verificationCode1, registrationLock) -> {
-                    final var accountManager = dependencies.getAccountManager();
+                    final var registrationApi = dependencies.getRegistrationApi();
+                    final var accountApi = dependencies.getAccountApi();
                     try {
-                        Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1));
+                        handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
                     } catch (AlreadyVerifiedException e) {
                         // Already verified so can continue changing number
                     }
-                    return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
-                            sessionId1,
+                    return handleResponseException(accountApi.changeNumber(new ChangePhoneNumberRequest(sessionId1,
                             null,
                             newNumber,
                             registrationLock,
@@ -290,9 +316,7 @@ public class AccountHelper {
         handlePniChangeNumberMessage(selfChangeNumber, updatePni);
     }
 
-    public void handlePniChangeNumberMessage(
-            final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
-    ) {
+    public void handlePniChangeNumberMessage(final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni) {
         if (pniChangeNumber.identityKeyPair != null
                 && pniChangeNumber.registrationId != null
                 && pniChangeNumber.signedPreKey != null) {
@@ -315,23 +339,48 @@ public class AccountHelper {
     public static final int USERNAME_MIN_LENGTH = 3;
     public static final int USERNAME_MAX_LENGTH = 32;
 
-    public void reserveUsername(String nickname) throws IOException, BaseUsernameException {
+    public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
         final var currentUsername = account.getUsername();
         if (currentUsername != null) {
             final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
             if (currentNickname.equals(nickname)) {
-                refreshCurrentUsername();
-                return;
+                try {
+                    refreshCurrentUsername();
+                    return;
+                } catch (IOException | BaseUsernameException e) {
+                    logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
+                }
             }
         }
 
         final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
+        reserveUsername(candidates);
+    }
+
+    public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
+        final var currentUsername = account.getUsername();
+        if (currentUsername != null) {
+            if (currentUsername.equals(username)) {
+                try {
+                    refreshCurrentUsername();
+                    return;
+                } catch (IOException | BaseUsernameException e) {
+                    logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
+                }
+            }
+        }
+
+        final var candidates = List.of(new Username(username));
+        reserveUsername(candidates);
+    }
+
+    private void reserveUsername(final List<Username> candidates) throws IOException {
         final var candidateHashes = new ArrayList<String>();
         for (final var candidate : candidates) {
             candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
         }
 
-        final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
+        final var response = handleResponseException(dependencies.getAccountApi().reserveUsername(candidateHashes));
         final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
         if (hashIndex == -1) {
             logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
@@ -341,11 +390,46 @@ public class AccountHelper {
         logger.debug("[reserveUsername] Successfully reserved username.");
         final var username = candidates.get(hashIndex);
 
-        dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
+        final var linkComponents = confirmUsernameAndCreateNewLink(username);
         account.setUsername(username.getUsername());
+        account.setUsernameLink(linkComponents);
         account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
+        account.getRecipientStore().rotateSelfStorageId();
         logger.debug("[confirmUsername] Successfully confirmed username.");
-        tryToSetUsernameLink(username);
+    }
+
+    public UsernameLinkComponents createUsernameLink(Username username) throws IOException {
+        try {
+            Username.UsernameLink link = username.generateLink();
+            return handleResponseException(dependencies.getAccountApi().createUsernameLink(link));
+        } catch (BaseUsernameException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private UsernameLinkComponents confirmUsernameAndCreateNewLink(Username username) throws IOException {
+        try {
+            Username.UsernameLink link = username.generateLink();
+            UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
+
+            return new UsernameLinkComponents(link.getEntropy(), serverId);
+        } catch (BaseUsernameException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private UsernameLinkComponents reclaimUsernameAndLink(
+            Username username,
+            UsernameLinkComponents linkComponents
+    ) throws IOException {
+        try {
+            Username.UsernameLink link = username.generateLink(linkComponents.getEntropy());
+            UUID serverId = handleResponseException(dependencies.getAccountApi().confirmUsername(username, link));
+
+            return new UsernameLinkComponents(link.getEntropy(), serverId);
+        } catch (BaseUsernameException e) {
+            throw new AssertionError(e);
+        }
     }
 
     public void refreshCurrentUsername() throws IOException, BaseUsernameException {
@@ -378,6 +462,7 @@ public class AccountHelper {
                         e.getClass().getSimpleName());
                 account.setUsername(null);
                 account.setUsernameLink(null);
+                account.getRecipientStore().rotateSelfStorageId();
                 throw e;
             }
         } else {
@@ -386,18 +471,27 @@ public class AccountHelper {
     }
 
     private void tryReserveConfirmUsername(final Username username) throws IOException {
-        final var response = dependencies.getAccountManager()
-                .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
-        logger.debug("[reserveUsername] Successfully reserved existing username.");
-        dependencies.getAccountManager().confirmUsername(username.getUsername(), response);
-        logger.debug("[confirmUsername] Successfully confirmed existing username.");
-        tryToSetUsernameLink(username);
+        final var usernameLink = account.getUsernameLink();
+
+        if (usernameLink == null) {
+            handleResponseException(dependencies.getAccountApi()
+                    .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash()))));
+            logger.debug("[reserveUsername] Successfully reserved existing username.");
+            final var linkComponents = confirmUsernameAndCreateNewLink(username);
+            account.setUsernameLink(linkComponents);
+            logger.debug("[confirmUsername] Successfully confirmed existing username.");
+        } else {
+            final var linkComponents = reclaimUsernameAndLink(username, usernameLink);
+            account.setUsernameLink(linkComponents);
+            logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
+        }
+        account.getRecipientStore().rotateSelfStorageId();
     }
 
     private void tryToSetUsernameLink(Username username) {
         for (var i = 1; i < 4; i++) {
             try {
-                final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
+                final var linkComponents = createUsernameLink(username);
                 account.setUsernameLink(linkComponents);
                 break;
             } catch (IOException e) {
@@ -407,7 +501,8 @@ public class AccountHelper {
     }
 
     public void deleteUsername() throws IOException {
-        dependencies.getAccountManager().deleteUsername();
+        handleResponseException(dependencies.getAccountApi().deleteUsername());
+        account.setUsernameLink(null);
         account.setUsername(null);
         logger.debug("[deleteUsername] Successfully deleted the username.");
     }
@@ -419,30 +514,39 @@ public class AccountHelper {
     }
 
     public void updateAccountAttributes() throws IOException {
-        dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
+        handleResponseException(dependencies.getAccountApi().setAccountAttributes(account.getAccountAttributes(null)));
     }
 
-    public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException {
-        var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
-
+    public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, org.asamk.signal.manager.api.DeviceLimitExceededException {
+        final var linkDeviceApi = dependencies.getLinkDeviceApi();
+        final LinkedDeviceVerificationCodeResponse verificationCode;
         try {
-            dependencies.getAccountManager()
-                    .addDevice(deviceLinkInfo.deviceIdentifier(),
-                            deviceLinkInfo.deviceKey(),
-                            account.getAciIdentityKeyPair(),
-                            account.getPniIdentityKeyPair(),
-                            account.getProfileKey(),
-                            account.getOrCreatePinMasterKey(),
-                            verificationCode);
-        } catch (InvalidKeyException e) {
-            throw new InvalidDeviceLinkException("Invalid device link", e);
+            verificationCode = handleResponseException(linkDeviceApi.getDeviceVerificationCode());
+        } catch (DeviceLimitExceededException e) {
+            throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
         }
+
+        handleResponseException(dependencies.getLinkDeviceApi()
+                .linkDevice(account.getNumber(),
+                        account.getAci(),
+                        account.getPni(),
+                        deviceLinkInfo.deviceIdentifier(),
+                        deviceLinkInfo.deviceKey(),
+                        account.getAciIdentityKeyPair(),
+                        account.getPniIdentityKeyPair(),
+                        account.getProfileKey(),
+                        account.getOrCreatePinMasterKey(),
+                        account.getOrCreateMediaRootBackupKey(),
+                        account.getOrCreateAccountEntropyPool(),
+                        verificationCode.getVerificationCode(),
+                        null));
         account.setMultiDevice(true);
+        context.getJobExecutor().enqueueJob(new SyncStorageJob());
     }
 
     public void removeLinkedDevices(int deviceId) throws IOException {
-        dependencies.getAccountManager().removeDevice(deviceId);
-        var devices = dependencies.getAccountManager().getDevices();
+        handleResponseException(dependencies.getLinkDeviceApi().removeDevice(deviceId));
+        var devices = handleResponseException(dependencies.getLinkDeviceApi().getDevices());
         account.setMultiDevice(devices.size() > 1);
     }
 
@@ -450,22 +554,25 @@ public class AccountHelper {
         var masterKey = account.getOrCreatePinMasterKey();
 
         context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
-        dependencies.getAccountManager().enableRegistrationLock(masterKey);
+        handleResponseException(dependencies.getAccountApi()
+                .enableRegistrationLock(masterKey.deriveRegistrationLock()));
     }
 
     public void setRegistrationPin(String pin) throws IOException {
         var masterKey = account.getOrCreatePinMasterKey();
 
         context.getPinHelper().setRegistrationLockPin(pin, masterKey);
-        dependencies.getAccountManager().enableRegistrationLock(masterKey);
+        handleResponseException(dependencies.getAccountApi()
+                .enableRegistrationLock(masterKey.deriveRegistrationLock()));
 
         account.setRegistrationLockPin(pin);
+        updateAccountAttributes();
     }
 
     public void removeRegistrationPin() throws IOException {
         // Remove KBS Pin
         context.getPinHelper().removeRegistrationLockPin();
-        dependencies.getAccountManager().disableRegistrationLock();
+        handleResponseException(dependencies.getAccountApi().disableRegistrationLock());
 
         account.setRegistrationLockPin(null);
     }
@@ -474,7 +581,7 @@ public class AccountHelper {
         // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
         // If this is the primary device, other users can't send messages to this number anymore.
         // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
-        dependencies.getAccountManager().setGcmId(Optional.empty());
+        handleResponseException(dependencies.getAccountApi().clearFcmToken());
 
         account.setRegistered(false);
         unregisteredListener.call();