]> nmode's Git Repositories - signal-cli/commitdiff
Extract ProvisioningManager and RegistrationManager interfaces
authorAsamK <asamk@gmx.de>
Fri, 31 Dec 2021 15:39:33 +0000 (16:39 +0100)
committerAsamK <asamk@gmx.de>
Fri, 31 Dec 2021 15:39:33 +0000 (16:39 +0100)
lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java
lib/src/main/java/org/asamk/signal/manager/ProvisioningManagerImpl.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java
lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java [new file with mode: 0644]

index cfb4fe4b5863f319a9aa6e5a8170e21536ff2b13..3aa26f31fc81d36bcd34e0b57e41e7a4342bac77 100644 (file)
@@ -1,38 +1,7 @@
-/*
-  Copyright (C) 2015-2021 AsamK and contributors
-
-  This program is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation, either version 3 of the License, or
-  (at your option) any later version.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
 package org.asamk.signal.manager;
 
 import org.asamk.signal.manager.config.ServiceConfig;
 import org.asamk.signal.manager.config.ServiceEnvironment;
-import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
-import org.asamk.signal.manager.storage.SignalAccount;
-import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
-import org.asamk.signal.manager.util.KeyUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.whispersystems.libsignal.IdentityKeyPair;
-import org.whispersystems.libsignal.util.KeyHelper;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
-import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
-import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
-import org.whispersystems.signalservice.api.util.DeviceNameUtil;
-import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
 
 import java.io.File;
 import java.io.IOException;
@@ -40,54 +9,15 @@ import java.net.URI;
 import java.util.concurrent.TimeoutException;
 import java.util.function.Consumer;
 
-public class ProvisioningManager {
-
-    private final static Logger logger = LoggerFactory.getLogger(ProvisioningManager.class);
+public interface ProvisioningManager {
 
-    private final PathConfig pathConfig;
-    private final ServiceEnvironmentConfig serviceEnvironmentConfig;
-    private final String userAgent;
-    private final Consumer<Manager> newManagerListener;
-
-    private final SignalServiceAccountManager accountManager;
-    private final IdentityKeyPair tempIdentityKey;
-    private final int registrationId;
-    private final String password;
-
-    ProvisioningManager(
-            PathConfig pathConfig,
-            ServiceEnvironmentConfig serviceEnvironmentConfig,
-            String userAgent,
-            final Consumer<Manager> newManagerListener
-    ) {
-        this.pathConfig = pathConfig;
-        this.serviceEnvironmentConfig = serviceEnvironmentConfig;
-        this.userAgent = userAgent;
-        this.newManagerListener = newManagerListener;
-
-        tempIdentityKey = KeyUtils.generateIdentityKeyPair();
-        registrationId = KeyHelper.generateRegistrationId(false);
-        password = KeyUtils.createPassword();
-        GroupsV2Operations groupsV2Operations;
-        try {
-            groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
-        } catch (Throwable ignored) {
-            groupsV2Operations = null;
-        }
-        accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
-                new DynamicCredentialsProvider(null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID),
-                userAgent,
-                groupsV2Operations,
-                ServiceConfig.AUTOMATIC_NETWORK_RETRY);
-    }
-
-    public static ProvisioningManager init(
+    static ProvisioningManager init(
             File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
     ) {
         return init(settingsPath, serviceEnvironment, userAgent, null);
     }
 
-    public static ProvisioningManager init(
+    static ProvisioningManager init(
             File settingsPath,
             ServiceEnvironment serviceEnvironment,
             String userAgent,
@@ -97,113 +27,10 @@ public class ProvisioningManager {
 
         final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
 
-        return new ProvisioningManager(pathConfig, serviceConfiguration, userAgent, newManagerListener);
+        return new ProvisioningManagerImpl(pathConfig, serviceConfiguration, userAgent, newManagerListener);
     }
 
-    public URI getDeviceLinkUri() throws TimeoutException, IOException {
-        var deviceUuid = accountManager.getNewDeviceUuid();
-
-        return new DeviceLinkInfo(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
-    }
-
-    public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists {
-        var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
-        var number = ret.getNumber();
-
-        logger.info("Received link information from {}, linking in progress ...", number);
+    URI getDeviceLinkUri() throws TimeoutException, IOException;
 
-        if (SignalAccount.userExists(pathConfig.dataPath(), number) && !canRelinkExistingAccount(number)) {
-            throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.dataPath(), number));
-        }
-
-        var encryptedDeviceName = deviceName == null
-                ? null
-                : DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey());
-
-        logger.debug("Finishing new device registration");
-        var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
-                false,
-                true,
-                registrationId,
-                encryptedDeviceName);
-
-        // Create new account with the synced identity
-        var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
-
-        SignalAccount account = null;
-        try {
-            account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
-                    number,
-                    ret.getAci(),
-                    password,
-                    encryptedDeviceName,
-                    deviceId,
-                    ret.getIdentity(),
-                    registrationId,
-                    profileKey,
-                    TrustNewIdentity.ON_FIRST_USE);
-
-            ManagerImpl m = null;
-            try {
-                m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
-                account = null;
-
-                logger.debug("Refreshing pre keys");
-                try {
-                    m.refreshPreKeys();
-                } catch (Exception e) {
-                    logger.error("Failed to refresh pre keys.");
-                }
-
-                logger.debug("Requesting sync data");
-                try {
-                    m.requestAllSyncData();
-                } catch (Exception e) {
-                    logger.error(
-                            "Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.");
-                }
-
-                if (newManagerListener != null) {
-                    newManagerListener.accept(m);
-                    m = null;
-                }
-                return number;
-            } finally {
-                if (m != null) {
-                    m.close();
-                }
-            }
-        } finally {
-            if (account != null) {
-                account.close();
-            }
-        }
-    }
-
-    private boolean canRelinkExistingAccount(final String number) throws IOException {
-        final SignalAccount signalAccount;
-        try {
-            signalAccount = SignalAccount.load(pathConfig.dataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
-        } catch (IOException e) {
-            logger.debug("Account in use or failed to load.", e);
-            return false;
-        }
-
-        try (signalAccount) {
-            if (signalAccount.isMasterDevice()) {
-                logger.debug("Account is a master device.");
-                return false;
-            }
-
-            final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
-            try (m) {
-                m.checkAccountState();
-            } catch (AuthorizationFailedException ignored) {
-                return true;
-            }
-
-            logger.debug("Account is still successfully linked.");
-            return false;
-        }
-    }
+    String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists;
 }
diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManagerImpl.java
new file mode 100644 (file)
index 0000000..466286b
--- /dev/null
@@ -0,0 +1,190 @@
+/*
+  Copyright (C) 2015-2021 AsamK and contributors
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.asamk.signal.manager;
+
+import org.asamk.signal.manager.config.ServiceConfig;
+import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
+import org.asamk.signal.manager.util.KeyUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.IdentityKeyPair;
+import org.whispersystems.libsignal.util.KeyHelper;
+import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
+import org.whispersystems.signalservice.api.util.DeviceNameUtil;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+
+public class ProvisioningManagerImpl implements ProvisioningManager {
+
+    private final static Logger logger = LoggerFactory.getLogger(ProvisioningManagerImpl.class);
+
+    private final PathConfig pathConfig;
+    private final ServiceEnvironmentConfig serviceEnvironmentConfig;
+    private final String userAgent;
+    private final Consumer<Manager> newManagerListener;
+
+    private final SignalServiceAccountManager accountManager;
+    private final IdentityKeyPair tempIdentityKey;
+    private final int registrationId;
+    private final String password;
+
+    ProvisioningManagerImpl(
+            PathConfig pathConfig,
+            ServiceEnvironmentConfig serviceEnvironmentConfig,
+            String userAgent,
+            final Consumer<Manager> newManagerListener
+    ) {
+        this.pathConfig = pathConfig;
+        this.serviceEnvironmentConfig = serviceEnvironmentConfig;
+        this.userAgent = userAgent;
+        this.newManagerListener = newManagerListener;
+
+        tempIdentityKey = KeyUtils.generateIdentityKeyPair();
+        registrationId = KeyHelper.generateRegistrationId(false);
+        password = KeyUtils.createPassword();
+        GroupsV2Operations groupsV2Operations;
+        try {
+            groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
+        } catch (Throwable ignored) {
+            groupsV2Operations = null;
+        }
+        accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
+                new DynamicCredentialsProvider(null, null, password, SignalServiceAddress.DEFAULT_DEVICE_ID),
+                userAgent,
+                groupsV2Operations,
+                ServiceConfig.AUTOMATIC_NETWORK_RETRY);
+    }
+
+    @Override
+    public URI getDeviceLinkUri() throws TimeoutException, IOException {
+        var deviceUuid = accountManager.getNewDeviceUuid();
+
+        return new DeviceLinkInfo(deviceUuid, tempIdentityKey.getPublicKey().getPublicKey()).createDeviceLinkUri();
+    }
+
+    @Override
+    public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExists {
+        var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
+        var number = ret.getNumber();
+
+        logger.info("Received link information from {}, linking in progress ...", number);
+
+        if (SignalAccount.userExists(pathConfig.dataPath(), number) && !canRelinkExistingAccount(number)) {
+            throw new UserAlreadyExists(number, SignalAccount.getFileName(pathConfig.dataPath(), number));
+        }
+
+        var encryptedDeviceName = deviceName == null
+                ? null
+                : DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey());
+
+        logger.debug("Finishing new device registration");
+        var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(),
+                false,
+                true,
+                registrationId,
+                encryptedDeviceName);
+
+        // Create new account with the synced identity
+        var profileKey = ret.getProfileKey() == null ? KeyUtils.createProfileKey() : ret.getProfileKey();
+
+        SignalAccount account = null;
+        try {
+            account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
+                    number,
+                    ret.getAci(),
+                    password,
+                    encryptedDeviceName,
+                    deviceId,
+                    ret.getIdentity(),
+                    registrationId,
+                    profileKey,
+                    TrustNewIdentity.ON_FIRST_USE);
+
+            ManagerImpl m = null;
+            try {
+                m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
+                account = null;
+
+                logger.debug("Refreshing pre keys");
+                try {
+                    m.refreshPreKeys();
+                } catch (Exception e) {
+                    logger.error("Failed to refresh pre keys.");
+                }
+
+                logger.debug("Requesting sync data");
+                try {
+                    m.requestAllSyncData();
+                } catch (Exception e) {
+                    logger.error(
+                            "Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`.");
+                }
+
+                if (newManagerListener != null) {
+                    newManagerListener.accept(m);
+                    m = null;
+                }
+                return number;
+            } finally {
+                if (m != null) {
+                    m.close();
+                }
+            }
+        } finally {
+            if (account != null) {
+                account.close();
+            }
+        }
+    }
+
+    private boolean canRelinkExistingAccount(final String number) throws IOException {
+        final SignalAccount signalAccount;
+        try {
+            signalAccount = SignalAccount.load(pathConfig.dataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
+        } catch (IOException e) {
+            logger.debug("Account in use or failed to load.", e);
+            return false;
+        }
+
+        try (signalAccount) {
+            if (signalAccount.isMasterDevice()) {
+                logger.debug("Account is a master device.");
+                return false;
+            }
+
+            final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
+            try (m) {
+                m.checkAccountState();
+            } catch (AuthorizationFailedException ignored) {
+                return true;
+            }
+
+            logger.debug("Account is still successfully linked.");
+            return false;
+        }
+    }
+}
index 24f0d5baf2adf1e6f0296734d32748d624b73943..f094ed92e9d125dca9652b2b60115f719dd53443 100644 (file)
@@ -1,19 +1,3 @@
-/*
-  Copyright (C) 2015-2021 AsamK and contributors
-
-  This program is free software: you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation, either version 3 of the License, or
-  (at your option) any later version.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License
-  along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
 package org.asamk.signal.manager;
 
 import org.asamk.signal.manager.api.CaptchaRequiredException;
@@ -21,92 +5,25 @@ import org.asamk.signal.manager.api.IncorrectPinException;
 import org.asamk.signal.manager.api.PinLockedException;
 import org.asamk.signal.manager.config.ServiceConfig;
 import org.asamk.signal.manager.config.ServiceEnvironment;
-import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
-import org.asamk.signal.manager.helper.PinHelper;
 import org.asamk.signal.manager.storage.SignalAccount;
 import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
 import org.asamk.signal.manager.util.KeyUtils;
-import org.asamk.signal.manager.util.Utils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.whispersystems.libsignal.util.KeyHelper;
-import org.whispersystems.libsignal.util.guava.Optional;
-import org.whispersystems.signalservice.api.KbsPinData;
-import org.whispersystems.signalservice.api.KeyBackupServicePinException;
-import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
-import org.whispersystems.signalservice.api.SignalServiceAccountManager;
-import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
-import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
-import org.whispersystems.signalservice.api.kbs.MasterKey;
-import org.whispersystems.signalservice.api.push.ACI;
-import org.whispersystems.signalservice.api.push.SignalServiceAddress;
-import org.whispersystems.signalservice.internal.ServiceResponse;
-import org.whispersystems.signalservice.internal.push.LockedException;
-import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
-import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
-import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
 
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
 import java.util.function.Consumer;
 
-import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
-
-public class RegistrationManager implements Closeable {
-
-    private final static Logger logger = LoggerFactory.getLogger(RegistrationManager.class);
-
-    private SignalAccount account;
-    private final PathConfig pathConfig;
-    private final ServiceEnvironmentConfig serviceEnvironmentConfig;
-    private final String userAgent;
-    private final Consumer<Manager> newManagerListener;
-
-    private final SignalServiceAccountManager accountManager;
-    private final PinHelper pinHelper;
-
-    private RegistrationManager(
-            SignalAccount account,
-            PathConfig pathConfig,
-            ServiceEnvironmentConfig serviceEnvironmentConfig,
-            String userAgent,
-            Consumer<Manager> newManagerListener
-    ) {
-        this.account = account;
-        this.pathConfig = pathConfig;
-        this.serviceEnvironmentConfig = serviceEnvironmentConfig;
-        this.userAgent = userAgent;
-        this.newManagerListener = newManagerListener;
+public interface RegistrationManager extends Closeable {
 
-        GroupsV2Operations groupsV2Operations;
-        try {
-            groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
-        } catch (Throwable ignored) {
-            groupsV2Operations = null;
-        }
-        this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
-                new DynamicCredentialsProvider(
-                        // Using empty UUID, because registering doesn't work otherwise
-                        null, account.getAccount(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
-                userAgent,
-                groupsV2Operations,
-                ServiceConfig.AUTOMATIC_NETWORK_RETRY);
-        final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
-                serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
-                serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
-                serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
-                10);
-        this.pinHelper = new PinHelper(keyBackupService);
-    }
-
-    public static RegistrationManager init(
+    static RegistrationManager init(
             String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
     ) throws IOException {
         return init(number, settingsPath, serviceEnvironment, userAgent, null);
     }
 
-    public static RegistrationManager init(
+    static RegistrationManager init(
             String number,
             File settingsPath,
             ServiceEnvironment serviceEnvironment,
@@ -128,176 +45,21 @@ public class RegistrationManager implements Closeable {
                     profileKey,
                     TrustNewIdentity.ON_FIRST_USE);
 
-            return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
+            return new RegistrationManagerImpl(account,
+                    pathConfig,
+                    serviceConfiguration,
+                    userAgent,
+                    newManagerListener);
         }
 
         var account = SignalAccount.load(pathConfig.dataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
 
-        return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
+        return new RegistrationManagerImpl(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
     }
 
-    public void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException {
-        captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
-        if (account.getAci() != null) {
-            try {
-                final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
-                        new DynamicCredentialsProvider(account.getAci(),
-                                account.getAccount(),
-                                account.getPassword(),
-                                account.getDeviceId()),
-                        userAgent,
-                        null,
-                        ServiceConfig.AUTOMATIC_NETWORK_RETRY);
-                accountManager.setAccountAttributes(account.getEncryptedDeviceName(),
-                        null,
-                        account.getLocalRegistrationId(),
-                        true,
-                        null,
-                        account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
-                        account.getSelfUnidentifiedAccessKey(),
-                        account.isUnrestrictedUnidentifiedAccess(),
-                        capabilities,
-                        account.isDiscoverableByPhoneNumber());
-                account.setRegistered(true);
-                logger.info("Reactivated existing account, verify is not necessary.");
-                if (newManagerListener != null) {
-                    final var m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
-                    account = null;
-                    newManagerListener.accept(m);
-                }
-                return;
-            } catch (IOException e) {
-                logger.debug("Failed to reactivate account");
-            }
-        }
-        final ServiceResponse<RequestVerificationCodeResponse> response;
-        if (voiceVerification) {
-            response = accountManager.requestVoiceVerificationCode(Utils.getDefaultLocale(),
-                    Optional.fromNullable(captcha),
-                    Optional.absent(),
-                    Optional.absent());
-        } else {
-            response = accountManager.requestSmsVerificationCode(false,
-                    Optional.fromNullable(captcha),
-                    Optional.absent(),
-                    Optional.absent());
-        }
-        try {
-            handleResponseException(response);
-        } catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
-            throw new CaptchaRequiredException(e.getMessage(), e);
-        }
-    }
+    void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException;
 
-    public void verifyAccount(
+    void verifyAccount(
             String verificationCode, String pin
-    ) throws IOException, PinLockedException, IncorrectPinException {
-        verificationCode = verificationCode.replace("-", "");
-        VerifyAccountResponse response;
-        MasterKey masterKey;
-        try {
-            response = verifyAccountWithCode(verificationCode, null);
-
-            masterKey = null;
-            pin = null;
-        } catch (LockedException e) {
-            if (pin == null) {
-                throw new PinLockedException(e.getTimeRemaining());
-            }
-
-            KbsPinData registrationLockData;
-            try {
-                registrationLockData = pinHelper.getRegistrationLockData(pin, e);
-            } catch (KeyBackupSystemNoDataException ex) {
-                throw new IOException(e);
-            } catch (KeyBackupServicePinException ex) {
-                throw new IncorrectPinException(ex.getTriesRemaining());
-            }
-            if (registrationLockData == null) {
-                throw e;
-            }
-
-            var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
-            try {
-                response = verifyAccountWithCode(verificationCode, registrationLock);
-            } catch (LockedException _e) {
-                throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
-            }
-            masterKey = registrationLockData.getMasterKey();
-        }
-
-        //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
-        account.finishRegistration(ACI.parseOrNull(response.getUuid()), masterKey, pin);
-
-        ManagerImpl m = null;
-        try {
-            m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
-            account = null;
-
-            m.refreshPreKeys();
-            if (response.isStorageCapable()) {
-                m.retrieveRemoteStorage();
-            }
-            // Set an initial empty profile so user can be added to groups
-            try {
-                m.setProfile(null, null, null, null, null);
-            } catch (NoClassDefFoundError e) {
-                logger.warn("Failed to set default profile: {}", e.getMessage());
-            }
-
-            if (newManagerListener != null) {
-                newManagerListener.accept(m);
-                m = null;
-            }
-        } finally {
-            if (m != null) {
-                m.close();
-            }
-        }
-    }
-
-    private VerifyAccountResponse verifyAccountWithCode(
-            final String verificationCode, final String registrationLock
-    ) throws IOException {
-        final ServiceResponse<VerifyAccountResponse> response;
-        if (registrationLock == null) {
-            response = accountManager.verifyAccount(verificationCode,
-                    account.getLocalRegistrationId(),
-                    true,
-                    account.getSelfUnidentifiedAccessKey(),
-                    account.isUnrestrictedUnidentifiedAccess(),
-                    ServiceConfig.capabilities,
-                    account.isDiscoverableByPhoneNumber());
-        } else {
-            response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode,
-                    account.getLocalRegistrationId(),
-                    true,
-                    registrationLock,
-                    account.getSelfUnidentifiedAccessKey(),
-                    account.isUnrestrictedUnidentifiedAccess(),
-                    ServiceConfig.capabilities,
-                    account.isDiscoverableByPhoneNumber());
-        }
-        handleResponseException(response);
-        return response.getResult().get();
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (account != null) {
-            account.close();
-            account = null;
-        }
-    }
-
-    private void handleResponseException(final ServiceResponse<?> response) throws IOException {
-        final var throwableOptional = response.getExecutionError().or(response.getApplicationError());
-        if (throwableOptional.isPresent()) {
-            if (throwableOptional.get() instanceof IOException) {
-                throw (IOException) throwableOptional.get();
-            } else {
-                throw new IOException(throwableOptional.get());
-            }
-        }
-    }
+    ) throws IOException, PinLockedException, IncorrectPinException;
 }
diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java
new file mode 100644 (file)
index 0000000..ddb884c
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+  Copyright (C) 2015-2021 AsamK and contributors
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package org.asamk.signal.manager;
+
+import org.asamk.signal.manager.api.CaptchaRequiredException;
+import org.asamk.signal.manager.api.IncorrectPinException;
+import org.asamk.signal.manager.api.PinLockedException;
+import org.asamk.signal.manager.config.ServiceConfig;
+import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
+import org.asamk.signal.manager.helper.PinHelper;
+import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.util.Utils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.whispersystems.libsignal.util.guava.Optional;
+import org.whispersystems.signalservice.api.KbsPinData;
+import org.whispersystems.signalservice.api.KeyBackupServicePinException;
+import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException;
+import org.whispersystems.signalservice.api.SignalServiceAccountManager;
+import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
+import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
+import org.whispersystems.signalservice.api.kbs.MasterKey;
+import org.whispersystems.signalservice.api.push.ACI;
+import org.whispersystems.signalservice.api.push.SignalServiceAddress;
+import org.whispersystems.signalservice.internal.ServiceResponse;
+import org.whispersystems.signalservice.internal.push.LockedException;
+import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse;
+import org.whispersystems.signalservice.internal.push.VerifyAccountResponse;
+import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import static org.asamk.signal.manager.config.ServiceConfig.capabilities;
+
+public class RegistrationManagerImpl implements RegistrationManager {
+
+    private final static Logger logger = LoggerFactory.getLogger(RegistrationManagerImpl.class);
+
+    private SignalAccount account;
+    private final PathConfig pathConfig;
+    private final ServiceEnvironmentConfig serviceEnvironmentConfig;
+    private final String userAgent;
+    private final Consumer<Manager> newManagerListener;
+
+    private final SignalServiceAccountManager accountManager;
+    private final PinHelper pinHelper;
+
+    RegistrationManagerImpl(
+            SignalAccount account,
+            PathConfig pathConfig,
+            ServiceEnvironmentConfig serviceEnvironmentConfig,
+            String userAgent,
+            Consumer<Manager> newManagerListener
+    ) {
+        this.account = account;
+        this.pathConfig = pathConfig;
+        this.serviceEnvironmentConfig = serviceEnvironmentConfig;
+        this.userAgent = userAgent;
+        this.newManagerListener = newManagerListener;
+
+        GroupsV2Operations groupsV2Operations;
+        try {
+            groupsV2Operations = new GroupsV2Operations(ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()));
+        } catch (Throwable ignored) {
+            groupsV2Operations = null;
+        }
+        this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
+                new DynamicCredentialsProvider(
+                        // Using empty UUID, because registering doesn't work otherwise
+                        null, account.getAccount(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
+                userAgent,
+                groupsV2Operations,
+                ServiceConfig.AUTOMATIC_NETWORK_RETRY);
+        final var keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(),
+                serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(),
+                serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(),
+                serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(),
+                10);
+        this.pinHelper = new PinHelper(keyBackupService);
+    }
+
+    @Override
+    public void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException {
+        captcha = captcha == null ? null : captcha.replace("signalcaptcha://", "");
+        if (account.getAci() != null) {
+            try {
+                final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
+                        new DynamicCredentialsProvider(account.getAci(),
+                                account.getAccount(),
+                                account.getPassword(),
+                                account.getDeviceId()),
+                        userAgent,
+                        null,
+                        ServiceConfig.AUTOMATIC_NETWORK_RETRY);
+                accountManager.setAccountAttributes(account.getEncryptedDeviceName(),
+                        null,
+                        account.getLocalRegistrationId(),
+                        true,
+                        null,
+                        account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(),
+                        account.getSelfUnidentifiedAccessKey(),
+                        account.isUnrestrictedUnidentifiedAccess(),
+                        capabilities,
+                        account.isDiscoverableByPhoneNumber());
+                account.setRegistered(true);
+                logger.info("Reactivated existing account, verify is not necessary.");
+                if (newManagerListener != null) {
+                    final var m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
+                    account = null;
+                    newManagerListener.accept(m);
+                }
+                return;
+            } catch (IOException e) {
+                logger.debug("Failed to reactivate account");
+            }
+        }
+        final ServiceResponse<RequestVerificationCodeResponse> response;
+        if (voiceVerification) {
+            response = accountManager.requestVoiceVerificationCode(Utils.getDefaultLocale(),
+                    Optional.fromNullable(captcha),
+                    Optional.absent(),
+                    Optional.absent());
+        } else {
+            response = accountManager.requestSmsVerificationCode(false,
+                    Optional.fromNullable(captcha),
+                    Optional.absent(),
+                    Optional.absent());
+        }
+        try {
+            handleResponseException(response);
+        } catch (org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException e) {
+            throw new CaptchaRequiredException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void verifyAccount(
+            String verificationCode, String pin
+    ) throws IOException, PinLockedException, IncorrectPinException {
+        verificationCode = verificationCode.replace("-", "");
+        VerifyAccountResponse response;
+        MasterKey masterKey;
+        try {
+            response = verifyAccountWithCode(verificationCode, null);
+
+            masterKey = null;
+            pin = null;
+        } catch (LockedException e) {
+            if (pin == null) {
+                throw new PinLockedException(e.getTimeRemaining());
+            }
+
+            KbsPinData registrationLockData;
+            try {
+                registrationLockData = pinHelper.getRegistrationLockData(pin, e);
+            } catch (KeyBackupSystemNoDataException ex) {
+                throw new IOException(e);
+            } catch (KeyBackupServicePinException ex) {
+                throw new IncorrectPinException(ex.getTriesRemaining());
+            }
+            if (registrationLockData == null) {
+                throw e;
+            }
+
+            var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock();
+            try {
+                response = verifyAccountWithCode(verificationCode, registrationLock);
+            } catch (LockedException _e) {
+                throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!");
+            }
+            masterKey = registrationLockData.getMasterKey();
+        }
+
+        //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
+        account.finishRegistration(ACI.parseOrNull(response.getUuid()), masterKey, pin);
+
+        ManagerImpl m = null;
+        try {
+            m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
+            account = null;
+
+            m.refreshPreKeys();
+            if (response.isStorageCapable()) {
+                m.retrieveRemoteStorage();
+            }
+            // Set an initial empty profile so user can be added to groups
+            try {
+                m.setProfile(null, null, null, null, null);
+            } catch (NoClassDefFoundError e) {
+                logger.warn("Failed to set default profile: {}", e.getMessage());
+            }
+
+            if (newManagerListener != null) {
+                newManagerListener.accept(m);
+                m = null;
+            }
+        } finally {
+            if (m != null) {
+                m.close();
+            }
+        }
+    }
+
+    private VerifyAccountResponse verifyAccountWithCode(
+            final String verificationCode, final String registrationLock
+    ) throws IOException {
+        final ServiceResponse<VerifyAccountResponse> response;
+        if (registrationLock == null) {
+            response = accountManager.verifyAccount(verificationCode,
+                    account.getLocalRegistrationId(),
+                    true,
+                    account.getSelfUnidentifiedAccessKey(),
+                    account.isUnrestrictedUnidentifiedAccess(),
+                    ServiceConfig.capabilities,
+                    account.isDiscoverableByPhoneNumber());
+        } else {
+            response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode,
+                    account.getLocalRegistrationId(),
+                    true,
+                    registrationLock,
+                    account.getSelfUnidentifiedAccessKey(),
+                    account.isUnrestrictedUnidentifiedAccess(),
+                    ServiceConfig.capabilities,
+                    account.isDiscoverableByPhoneNumber());
+        }
+        handleResponseException(response);
+        return response.getResult().get();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (account != null) {
+            account.close();
+            account = null;
+        }
+    }
+
+    private void handleResponseException(final ServiceResponse<?> response) throws IOException {
+        final var throwableOptional = response.getExecutionError().or(response.getApplicationError());
+        if (throwableOptional.isPresent()) {
+            if (throwableOptional.get() instanceof IOException) {
+                throw (IOException) throwableOptional.get();
+            } else {
+                throw new IOException(throwableOptional.get());
+            }
+        }
+    }
+}