From 00cda598c849ac0dd5301a961ee4113093b533c8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 31 Dec 2021 16:39:33 +0100 Subject: [PATCH] Extract ProvisioningManager and RegistrationManager interfaces --- .../signal/manager/ProvisioningManager.java | 185 +----------- .../manager/ProvisioningManagerImpl.java | 190 +++++++++++++ .../signal/manager/RegistrationManager.java | 262 +---------------- .../manager/RegistrationManagerImpl.java | 263 ++++++++++++++++++ 4 files changed, 471 insertions(+), 429 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/ProvisioningManagerImpl.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index cfb4fe4b..3aa26f31 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -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 . - */ 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 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 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 index 00000000..466286b0 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManagerImpl.java @@ -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 . + */ +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 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 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; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 24f0d5ba..f094ed92 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -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 . - */ 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 newManagerListener; - - private final SignalServiceAccountManager accountManager; - private final PinHelper pinHelper; - - private RegistrationManager( - SignalAccount account, - PathConfig pathConfig, - ServiceEnvironmentConfig serviceEnvironmentConfig, - String userAgent, - Consumer 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 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 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 index 00000000..ddb884cd --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManagerImpl.java @@ -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 . + */ +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 newManagerListener; + + private final SignalServiceAccountManager accountManager; + private final PinHelper pinHelper; + + RegistrationManagerImpl( + SignalAccount account, + PathConfig pathConfig, + ServiceEnvironmentConfig serviceEnvironmentConfig, + String userAgent, + Consumer 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 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 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()); + } + } + } +} -- 2.50.1