private final static Logger logger = LoggerFactory.getLogger("LibSignal");
- public static void initLogger() {
+ static void initLogger() {
SignalProtocolLoggerProvider.setProvider(new LibSignalLogger());
}
package org.asamk.signal.manager;
-import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.AttachmentInvalidException;
import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device;
import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.NotMasterDeviceException;
-import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults;
import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup;
-import org.asamk.signal.manager.config.ServiceConfig;
-import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
-import org.asamk.signal.manager.storage.SignalAccount;
-import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress;
public interface Manager extends Closeable {
- static Manager init(
- String number,
- File settingsPath,
- ServiceEnvironment serviceEnvironment,
- String userAgent,
- TrustNewIdentity trustNewIdentity
- ) throws IOException, NotRegisteredException, AccountCheckException {
- var pathConfig = PathConfig.createDefault(settingsPath);
-
- if (!SignalAccount.userExists(pathConfig.dataPath(), number)) {
- throw new NotRegisteredException();
- }
-
- var account = SignalAccount.load(pathConfig.dataPath(), number, true, trustNewIdentity);
-
- if (!account.isRegistered()) {
- account.close();
- throw new NotRegisteredException();
- }
-
- account.initDatabase();
- final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
-
- final var manager = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
-
- try {
- manager.checkAccountState();
- } catch (IOException e) {
- manager.close();
- throw new AccountCheckException("Error while checking account " + account + ": " + e.getMessage(), e);
- }
-
- return manager;
- }
-
- static void initLogger() {
- LibSignalLogger.initLogger();
- }
-
static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
}
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
+import org.asamk.signal.manager.helper.AccountFileUpdater;
import org.asamk.signal.manager.helper.Context;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo;
private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
private SignalAccount account;
+ private final AccountFileUpdater accountFileUpdater;
private final SignalDependencies dependencies;
private final Context context;
ManagerImpl(
SignalAccount account,
PathConfig pathConfig,
+ AccountFileUpdater accountFileUpdater,
ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent
) {
this.account = account;
+ this.accountFileUpdater = accountFileUpdater;
final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(),
- account.getAccount(),
+ account.getNumber(),
account.getPassword(),
account.getDeviceId());
final var sessionLock = new SignalSessionLock() {
final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath());
- this.context = new Context(account, dependencies, avatarStore, attachmentStore, stickerPackStore);
+ this.context = new Context(account,
+ accountFileUpdater,
+ dependencies,
+ avatarStore,
+ attachmentStore,
+ stickerPackStore);
this.context.getAccountHelper().setUnregisteredListener(this::close);
this.context.getReceiveHelper().setAuthenticationFailureListener(this::close);
this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> {
@Override
public String getSelfNumber() {
- return account.getAccount();
+ return account.getNumber();
}
void checkAccountState() throws IOException {
public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) throws IOException {
final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
try {
- final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount());
+ final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getNumber());
if (!canonicalizedNumber.equals(n)) {
logger.debug("Normalized number {} to {}.", n, canonicalizedNumber);
}
--- /dev/null
+package org.asamk.signal.manager;
+
+public class ManagerLogger {
+
+ public static void initLogger() {
+ LibSignalLogger.initLogger();
+ }
+}
package org.asamk.signal.manager;
-import org.asamk.signal.manager.api.AccountCheckException;
-import org.asamk.signal.manager.api.NotRegisteredException;
-import org.asamk.signal.manager.config.ServiceEnvironment;
-import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
-import org.slf4j.LoggerFactory;
-import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
-
-import java.io.File;
import java.io.IOException;
import java.net.URI;
-import java.util.Arrays;
import java.util.List;
-import java.util.Objects;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
public interface MultiAccountManager extends AutoCloseable {
- static MultiAccountManager init(
- final File settingsPath,
- final ServiceEnvironment serviceEnvironment,
- final String userAgent,
- final TrustNewIdentity trustNewIdentity
- ) {
- final var logger = LoggerFactory.getLogger(MultiAccountManager.class);
- final var managers = getAllLocalAccountNumbers(settingsPath).stream().map(a -> {
- try {
- return Manager.init(a, settingsPath, serviceEnvironment, userAgent, trustNewIdentity);
- } catch (NotRegisteredException | IOException | AccountCheckException e) {
- logger.warn("Ignoring {}: {} ({})", a, e.getMessage(), e.getClass().getSimpleName());
- return null;
- }
- }).filter(Objects::nonNull).toList();
-
- return new MultiAccountManagerImpl(managers, settingsPath, serviceEnvironment, userAgent);
- }
-
- static List<String> getAllLocalAccountNumbers(File settingsPath) {
- var pathConfig = PathConfig.createDefault(settingsPath);
- final var dataPath = pathConfig.dataPath();
- final var files = dataPath.listFiles();
-
- if (files == null) {
- return List.of();
- }
-
- return Arrays.stream(files)
- .filter(File::isFile)
- .map(File::getName)
- .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
- .toList();
- }
-
List<String> getAccountNumbers();
List<Manager> getManagers();
package org.asamk.signal.manager;
-import org.asamk.signal.manager.config.ServiceEnvironment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
private final Set<Consumer<Manager>> onManagerRemovedHandlers = new HashSet<>();
private final Set<Manager> managers = new HashSet<>();
private final Map<URI, ProvisioningManager> provisioningManagers = new HashMap<>();
- private final File settingsPath;
- private final ServiceEnvironment serviceEnvironment;
- private final String userAgent;
-
- public MultiAccountManagerImpl(
- final Collection<Manager> managers,
- final File settingsPath,
- final ServiceEnvironment serviceEnvironment,
- final String userAgent
- ) {
+ private final SignalAccountFiles signalAccountFiles;
+
+ public MultiAccountManagerImpl(final Collection<Manager> managers, final SignalAccountFiles signalAccountFiles) {
+ this.signalAccountFiles = signalAccountFiles;
this.managers.addAll(managers);
managers.forEach(m -> m.addClosedListener(() -> this.removeManager(m)));
- this.settingsPath = settingsPath;
- this.serviceEnvironment = serviceEnvironment;
- this.userAgent = userAgent;
}
@Override
}
@Override
- public Manager getManager(final String account) {
+ public Manager getManager(final String number) {
synchronized (managers) {
- return managers.stream().filter(m -> m.getSelfNumber().equals(account)).findFirst().orElse(null);
+ return managers.stream().filter(m -> m.getSelfNumber().equals(number)).findFirst().orElse(null);
}
}
}
private ProvisioningManager getNewProvisioningManager() {
- return ProvisioningManager.init(settingsPath, serviceEnvironment, userAgent, this::addManager);
+ return signalAccountFiles.initProvisioningManager(this::addManager);
}
@Override
- public RegistrationManager getNewRegistrationManager(String account) throws IOException {
- return RegistrationManager.init(account, settingsPath, serviceEnvironment, userAgent, this::addManager);
+ public RegistrationManager getNewRegistrationManager(String number) throws IOException {
+ return signalAccountFiles.initRegistrationManager(number, this::addManager);
}
@Override
package org.asamk.signal.manager;
import org.asamk.signal.manager.api.UserAlreadyExistsException;
-import org.asamk.signal.manager.config.ServiceConfig;
-import org.asamk.signal.manager.config.ServiceEnvironment;
-import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.concurrent.TimeoutException;
-import java.util.function.Consumer;
public interface ProvisioningManager {
- static ProvisioningManager init(
- File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
- ) {
- return init(settingsPath, serviceEnvironment, userAgent, null);
- }
-
- static ProvisioningManager init(
- File settingsPath,
- ServiceEnvironment serviceEnvironment,
- String userAgent,
- Consumer<Manager> newManagerListener
- ) {
- var pathConfig = PathConfig.createDefault(settingsPath);
-
- final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
-
- return new ProvisioningManagerImpl(pathConfig, serviceConfiguration, userAgent, newManagerListener);
- }
-
URI getDeviceLinkUri() throws TimeoutException, IOException;
String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException;
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.accounts.AccountsStore;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger;
private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent;
private final Consumer<Manager> newManagerListener;
+ private final AccountsStore accountsStore;
private final SignalServiceAccountManager accountManager;
private final IdentityKeyPair tempIdentityKey;
PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent,
- final Consumer<Manager> newManagerListener
+ final Consumer<Manager> newManagerListener,
+ final AccountsStore accountsStore
) {
this.pathConfig = pathConfig;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
this.newManagerListener = newManagerListener;
+ this.accountsStore = accountsStore;
tempIdentityKey = KeyUtils.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false);
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber();
+ var aci = ret.getAci();
logger.info("Received link information from {}, linking in progress ...", number);
- if (SignalAccount.userExists(pathConfig.dataPath(), number) && !canRelinkExistingAccount(number)) {
- throw new UserAlreadyExistsException(number, SignalAccount.getFileName(pathConfig.dataPath(), number));
+ var accountPath = accountsStore.getPathByAci(aci);
+ if (accountPath == null) {
+ accountPath = accountsStore.getPathByNumber(number);
+ }
+ if (accountPath != null
+ && SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)
+ && !canRelinkExistingAccount(accountPath)) {
+ throw new UserAlreadyExistsException(number, SignalAccount.getFileName(pathConfig.dataPath(), accountPath));
+ }
+ if (accountPath == null) {
+ accountPath = accountsStore.addAccount(number, aci);
+ } else {
+ accountsStore.updateAccount(accountPath, number, aci);
}
var encryptedDeviceName = deviceName == null
SignalAccount account = null;
try {
account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
+ accountPath,
number,
- ret.getAci(),
+ aci,
password,
encryptedDeviceName,
deviceId,
ManagerImpl m = null;
try {
- m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ final var accountPathFinal = accountPath;
+ m = new ManagerImpl(account,
+ pathConfig,
+ (newNumber, newAci) -> accountsStore.updateAccount(accountPathFinal, newNumber, newAci),
+ serviceEnvironmentConfig,
+ userAgent);
account = null;
logger.debug("Refreshing pre keys");
}
}
- private boolean canRelinkExistingAccount(final String number) throws IOException {
+ private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
final SignalAccount signalAccount;
try {
- signalAccount = SignalAccount.load(pathConfig.dataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
+ signalAccount = SignalAccount.load(pathConfig.dataPath(),
+ accountPath,
+ false,
+ TrustNewIdentity.ON_FIRST_USE);
} catch (IOException e) {
logger.debug("Account in use or failed to load.", e);
return false;
return false;
}
- final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent);
+ final var m = new ManagerImpl(signalAccount,
+ pathConfig,
+ (newNumber, newAci) -> accountsStore.updateAccount(accountPath, newNumber, newAci),
+ serviceEnvironmentConfig,
+ userAgent);
try (m) {
m.checkAccountState();
} catch (AuthorizationFailedException ignored) {
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.ServiceEnvironment;
-import org.asamk.signal.manager.storage.SignalAccount;
-import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
-import org.asamk.signal.manager.util.KeyUtils;
-import org.whispersystems.libsignal.util.KeyHelper;
import java.io.Closeable;
-import java.io.File;
import java.io.IOException;
-import java.util.function.Consumer;
public interface RegistrationManager extends Closeable {
- static RegistrationManager init(
- String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
- ) throws IOException {
- return init(number, settingsPath, serviceEnvironment, userAgent, null);
- }
-
- static RegistrationManager init(
- String number,
- File settingsPath,
- ServiceEnvironment serviceEnvironment,
- String userAgent,
- Consumer<Manager> newManagerListener
- ) throws IOException {
- var pathConfig = PathConfig.createDefault(settingsPath);
-
- final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
- if (!SignalAccount.userExists(pathConfig.dataPath(), number)) {
- var identityKey = KeyUtils.generateIdentityKeyPair();
- var registrationId = KeyHelper.generateRegistrationId(false);
-
- var profileKey = KeyUtils.createProfileKey();
- var account = SignalAccount.create(pathConfig.dataPath(),
- number,
- identityKey,
- registrationId,
- profileKey,
- TrustNewIdentity.ON_FIRST_USE);
-
- return new RegistrationManagerImpl(account,
- pathConfig,
- serviceConfiguration,
- userAgent,
- newManagerListener);
- }
-
- var account = SignalAccount.load(pathConfig.dataPath(), number, true, TrustNewIdentity.ON_FIRST_USE);
-
- return new RegistrationManagerImpl(account, pathConfig, serviceConfiguration, userAgent, newManagerListener);
- }
-
void register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException;
void verifyAccount(
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.AccountFileUpdater;
import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.Utils;
private final SignalServiceAccountManager accountManager;
private final PinHelper pinHelper;
+ private final AccountFileUpdater accountFileUpdater;
RegistrationManagerImpl(
SignalAccount account,
PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent,
- Consumer<Manager> newManagerListener
+ Consumer<Manager> newManagerListener,
+ AccountFileUpdater accountFileUpdater
) {
this.account = account;
this.pathConfig = pathConfig;
+ this.accountFileUpdater = accountFileUpdater;
this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent;
this.newManagerListener = newManagerListener;
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),
+ null, account.getNumber(), account.getPassword(), SignalServiceAddress.DEFAULT_DEVICE_ID),
userAgent,
groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY);
try {
final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider(account.getAci(),
- account.getAccount(),
+ account.getNumber(),
account.getPassword(),
account.getDeviceId()),
userAgent,
account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) {
- final var m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ final var m = new ManagerImpl(account,
+ pathConfig,
+ accountFileUpdater,
+ serviceEnvironmentConfig,
+ userAgent);
account = null;
newManagerListener.accept(m);
}
}
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
- account.finishRegistration(ACI.parseOrNull(response.getUuid()), masterKey, pin);
+ final var aci = ACI.parseOrNull(response.getUuid());
+ account.finishRegistration(aci, masterKey, pin);
+ accountFileUpdater.updateAccountIdentifiers(account.getNumber(), aci);
ManagerImpl m = null;
try {
- m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent);
+ m = new ManagerImpl(account, pathConfig, accountFileUpdater, serviceEnvironmentConfig, userAgent);
account = null;
m.refreshPreKeys();
--- /dev/null
+package org.asamk.signal.manager;
+
+import org.asamk.signal.manager.api.AccountCheckException;
+import org.asamk.signal.manager.api.NotRegisteredException;
+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.accounts.AccountsStore;
+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.util.KeyHelper;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class SignalAccountFiles {
+
+ private static final Logger logger = LoggerFactory.getLogger(MultiAccountManager.class);
+
+ private final PathConfig pathConfig;
+ private final ServiceEnvironmentConfig serviceEnvironmentConfig;
+ private final String userAgent;
+ private final TrustNewIdentity trustNewIdentity;
+ private final AccountsStore accountsStore;
+
+ public SignalAccountFiles(
+ final File settingsPath,
+ final ServiceEnvironment serviceEnvironment,
+ final String userAgent,
+ final TrustNewIdentity trustNewIdentity
+ ) {
+ this.pathConfig = PathConfig.createDefault(settingsPath);
+ this.serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent);
+ this.userAgent = userAgent;
+ this.trustNewIdentity = trustNewIdentity;
+ this.accountsStore = new AccountsStore(pathConfig.dataPath());
+ }
+
+ public Set<String> getAllLocalAccountNumbers() {
+ return accountsStore.getAllNumbers();
+ }
+
+ public MultiAccountManager initMultiAccountManager() {
+ final var managers = getAllLocalAccountNumbers().stream().map(a -> {
+ try {
+ return initManager(a);
+ } catch (NotRegisteredException | IOException | AccountCheckException e) {
+ logger.warn("Ignoring {}: {} ({})", a, e.getMessage(), e.getClass().getSimpleName());
+ return null;
+ }
+ }).filter(Objects::nonNull).toList();
+
+ return new MultiAccountManagerImpl(managers, this);
+ }
+
+ public Manager initManager(String number) throws IOException, NotRegisteredException, AccountCheckException {
+ final var accountPath = accountsStore.getPathByNumber(number);
+ if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
+ throw new NotRegisteredException();
+ }
+
+ var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, trustNewIdentity);
+ if (!number.equals(account.getNumber())) {
+ account.close();
+ throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
+ }
+
+ if (!account.isRegistered()) {
+ account.close();
+ throw new NotRegisteredException();
+ }
+
+ account.initDatabase();
+
+ final var manager = new ManagerImpl(account,
+ pathConfig,
+ (newNumber, newAci) -> accountsStore.updateAccount(accountPath, newNumber, newAci),
+ serviceEnvironmentConfig,
+ userAgent);
+
+ try {
+ manager.checkAccountState();
+ } catch (IOException e) {
+ manager.close();
+ throw new AccountCheckException("Error while checking account " + number + ": " + e.getMessage(), e);
+ }
+
+ return manager;
+ }
+
+ public ProvisioningManager initProvisioningManager() {
+ return initProvisioningManager(null);
+ }
+
+ public ProvisioningManager initProvisioningManager(Consumer<Manager> newManagerListener) {
+ return new ProvisioningManagerImpl(pathConfig,
+ serviceEnvironmentConfig,
+ userAgent,
+ newManagerListener,
+ accountsStore);
+ }
+
+ public RegistrationManager initRegistrationManager(String number) throws IOException {
+ return initRegistrationManager(number, null);
+ }
+
+ public RegistrationManager initRegistrationManager(
+ String number, Consumer<Manager> newManagerListener
+ ) throws IOException {
+ final var accountPath = accountsStore.getPathByNumber(number);
+ if (accountPath == null || !SignalAccount.accountFileExists(pathConfig.dataPath(), accountPath)) {
+ final var newAccountPath = accountPath == null ? accountsStore.addAccount(number, null) : accountPath;
+ var identityKey = KeyUtils.generateIdentityKeyPair();
+ var registrationId = KeyHelper.generateRegistrationId(false);
+
+ var profileKey = KeyUtils.createProfileKey();
+ var account = SignalAccount.create(pathConfig.dataPath(),
+ newAccountPath,
+ number,
+ identityKey,
+ registrationId,
+ profileKey,
+ trustNewIdentity);
+
+ return new RegistrationManagerImpl(account,
+ pathConfig,
+ serviceEnvironmentConfig,
+ userAgent,
+ newManagerListener,
+ (newNumber, newAci) -> accountsStore.updateAccount(newAccountPath, newNumber, newAci));
+ }
+
+ var account = SignalAccount.load(pathConfig.dataPath(), accountPath, true, trustNewIdentity);
+ if (!number.equals(account.getNumber())) {
+ account.close();
+ throw new IOException("Number in account file doesn't match expected number: " + account.getNumber());
+ }
+
+ return new RegistrationManagerImpl(account,
+ pathConfig,
+ serviceEnvironmentConfig,
+ userAgent,
+ newManagerListener,
+ (newNumber, newAci) -> accountsStore.updateAccount(accountPath, newNumber, newAci));
+ }
+}
--- /dev/null
+package org.asamk.signal.manager.helper;
+
+import org.whispersystems.signalservice.api.push.ACI;
+
+public interface AccountFileUpdater {
+
+ void updateAccountIdentifiers(String number, ACI aci);
+}
try {
context.getPreKeyHelper().refreshPreKeysIfNecessary();
if (account.getAci() == null) {
- account.setAci(ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci()));
+ final var aci = ACI.parseOrNull(dependencies.getAccountManager().getWhoAmI().getAci());
+ account.setAci(aci);
+ context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), aci);
}
updateAccountAttributes();
} catch (AuthorizationFailedException e) {
import org.asamk.signal.manager.AvatarStore;
import org.asamk.signal.manager.JobExecutor;
import org.asamk.signal.manager.SignalDependencies;
-import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore;
import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore;
import java.util.function.Supplier;
private final Object LOCK = new Object();
private final SignalAccount account;
+ private final AccountFileUpdater accountFileUpdater;
private final SignalDependencies dependencies;
private final AvatarStore avatarStore;
private final StickerPackStore stickerPackStore;
public Context(
final SignalAccount account,
+ final AccountFileUpdater accountFileUpdater,
final SignalDependencies dependencies,
final AvatarStore avatarStore,
final AttachmentStore attachmentStore,
final StickerPackStore stickerPackStore
) {
this.account = account;
+ this.accountFileUpdater = accountFileUpdater;
this.dependencies = dependencies;
this.avatarStore = avatarStore;
this.stickerPackStore = stickerPackStore;
return account;
}
+ public AccountFileUpdater getAccountFileUpdater() {
+ return accountFileUpdater;
+ }
+
public SignalDependencies getDependencies() {
return dependencies;
}
package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.SignalDependencies;
-import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.api.PhoneNumberSharingMode;
+import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.Contact;
return;
}
- if (!accountRecord.getE164().equals(account.getAccount())) {
+ if (!accountRecord.getE164().equals(account.getNumber())) {
// TODO implement changed number handling
}
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.api.Pair;
+import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
import org.asamk.signal.manager.storage.contacts.ContactsStore;
private int previousStorageVersion;
private File dataPath;
- private String account;
+ private String accountPath;
+ private String number;
private ACI aci;
private String encryptedDeviceName;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
}
public static SignalAccount load(
- File dataPath, String account, boolean waitForLock, final TrustNewIdentity trustNewIdentity
+ File dataPath, String accountPath, boolean waitForLock, final TrustNewIdentity trustNewIdentity
) throws IOException {
logger.trace("Opening account file");
- final var fileName = getFileName(dataPath, account);
+ final var fileName = getFileName(dataPath, accountPath);
final var pair = openFileChannel(fileName, waitForLock);
try {
var signalAccount = new SignalAccount(pair.first(), pair.second());
logger.trace("Loading account file");
- signalAccount.load(dataPath, trustNewIdentity);
+ signalAccount.load(dataPath, accountPath, trustNewIdentity);
logger.trace("Migrating legacy parts of account file");
signalAccount.migrateLegacyConfigs();
- if (!account.equals(signalAccount.getAccount())) {
- throw new IOException("Number in account file doesn't match expected number: "
- + signalAccount.getAccount());
- }
-
return signalAccount;
} catch (Throwable e) {
pair.second().close();
public static SignalAccount create(
File dataPath,
- String account,
+ String accountPath,
+ String number,
IdentityKeyPair identityKey,
int registrationId,
ProfileKey profileKey,
final TrustNewIdentity trustNewIdentity
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
- var fileName = getFileName(dataPath, account);
+ var fileName = getFileName(dataPath, accountPath);
if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName);
}
final var pair = openFileChannel(fileName, true);
var signalAccount = new SignalAccount(pair.first(), pair.second());
- signalAccount.account = account;
+ signalAccount.accountPath = accountPath;
+ signalAccount.number = number;
signalAccount.profileKey = profileKey;
signalAccount.dataPath = dataPath;
signalAccount.identityKeyPair = identityKey;
signalAccount.localRegistrationId = registrationId;
signalAccount.trustNewIdentity = trustNewIdentity;
- signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, account),
+ signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
signalAccount.getRecipientStore(),
signalAccount::saveGroupStore);
signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
public static SignalAccount createOrUpdateLinkedAccount(
File dataPath,
- String account,
+ String accountPath,
+ String number,
ACI aci,
String password,
String encryptedDeviceName,
final TrustNewIdentity trustNewIdentity
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
- var fileName = getFileName(dataPath, account);
+ var fileName = getFileName(dataPath, accountPath);
if (!fileName.exists()) {
return createLinkedAccount(dataPath,
- account,
+ accountPath,
+ number,
aci,
password,
encryptedDeviceName,
trustNewIdentity);
}
- final var signalAccount = load(dataPath, account, true, trustNewIdentity);
- signalAccount.setProvisioningData(account, aci, password, encryptedDeviceName, deviceId, profileKey);
+ final var signalAccount = load(dataPath, accountPath, true, trustNewIdentity);
+ signalAccount.setProvisioningData(number, aci, password, encryptedDeviceName, deviceId, profileKey);
signalAccount.getRecipientStore().resolveRecipientTrusted(signalAccount.getSelfAddress());
signalAccount.getSessionStore().archiveAllSessions();
signalAccount.getSenderKeyStore().deleteAll();
private static SignalAccount createLinkedAccount(
File dataPath,
- String account,
+ String accountPath,
+ String number,
ACI aci,
String password,
String encryptedDeviceName,
ProfileKey profileKey,
final TrustNewIdentity trustNewIdentity
) throws IOException {
- var fileName = getFileName(dataPath, account);
+ var fileName = getFileName(dataPath, accountPath);
IOUtils.createPrivateFile(fileName);
final var pair = openFileChannel(fileName, true);
var signalAccount = new SignalAccount(pair.first(), pair.second());
- signalAccount.setProvisioningData(account, aci, password, encryptedDeviceName, deviceId, profileKey);
+ signalAccount.setProvisioningData(number, aci, password, encryptedDeviceName, deviceId, profileKey);
signalAccount.dataPath = dataPath;
+ signalAccount.accountPath = accountPath;
signalAccount.identityKeyPair = identityKey;
signalAccount.localRegistrationId = registrationId;
signalAccount.trustNewIdentity = trustNewIdentity;
- signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, account),
+ signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
signalAccount.getRecipientStore(),
signalAccount::saveGroupStore);
signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
}
private void setProvisioningData(
- final String account,
+ final String number,
final ACI aci,
final String password,
final String encryptedDeviceName,
final int deviceId,
final ProfileKey profileKey
) {
- this.account = account;
+ this.number = number;
this.aci = aci;
this.password = password;
this.profileKey = profileKey;
return new File(getUserPath(dataPath, account), "account.db");
}
- public static boolean userExists(File dataPath, String account) {
+ public static boolean accountFileExists(File dataPath, String account) {
if (account == null) {
return false;
}
}
private void load(
- File dataPath, final TrustNewIdentity trustNewIdentity
+ File dataPath, String accountPath, final TrustNewIdentity trustNewIdentity
) throws IOException {
- JsonNode rootNode;
+ this.dataPath = dataPath;
+ this.accountPath = accountPath;
+ final JsonNode rootNode;
synchronized (fileChannel) {
fileChannel.position(0);
rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
previousStorageVersion = accountVersion;
}
- account = Utils.getNotNullNode(rootNode, "username").asText();
+ number = Utils.getNotNullNode(rootNode, "username").asText();
if (rootNode.hasNonNull("password")) {
password = rootNode.get("password").asText();
}
migratedLegacyConfig = true;
}
- this.dataPath = dataPath;
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = registrationId;
this.trustNewIdentity = trustNewIdentity;
if (rootNode.hasNonNull("groupStore")) {
groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
groupStore = GroupStore.fromStorage(groupStoreStorage,
- getGroupCachePath(dataPath, account),
+ getGroupCachePath(dataPath, accountPath),
getRecipientStore(),
this::saveGroupStore);
} else {
- groupStore = new GroupStore(getGroupCachePath(dataPath, account),
+ groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
getRecipientStore(),
this::saveGroupStore);
}
synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode();
rootNode.put("version", CURRENT_STORAGE_VERSION)
- .put("username", account)
+ .put("username", number)
.put("uuid", aci == null ? null : aci.toString())
.put("deviceName", encryptedDeviceName)
.put("deviceId", deviceId)
}
private PreKeyStore getPreKeyStore() {
- return getOrCreate(() -> preKeyStore, () -> preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, account)));
+ return getOrCreate(() -> preKeyStore,
+ () -> preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, accountPath)));
}
private SignedPreKeyStore getSignedPreKeyStore() {
return getOrCreate(() -> signedPreKeyStore,
- () -> signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, account)));
+ () -> signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, accountPath)));
}
public SessionStore getSessionStore() {
return getOrCreate(() -> sessionStore,
- () -> sessionStore = new SessionStore(getSessionsPath(dataPath, account), getRecipientStore()));
+ () -> sessionStore = new SessionStore(getSessionsPath(dataPath, accountPath), getRecipientStore()));
}
public IdentityKeyStore getIdentityKeyStore() {
return getOrCreate(() -> identityKeyStore,
- () -> identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, account),
+ () -> identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, accountPath),
getRecipientStore(),
identityKeyPair,
localRegistrationId,
public RecipientStore getRecipientStore() {
return getOrCreate(() -> recipientStore,
- () -> recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, account),
+ () -> recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, accountPath),
this::mergeRecipients));
}
public SenderKeyStore getSenderKeyStore() {
return getOrCreate(() -> senderKeyStore,
- () -> senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, account),
- getSenderKeysPath(dataPath, account),
+ () -> senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, accountPath),
+ getSenderKeysPath(dataPath, accountPath),
getRecipientStore()::resolveRecipientAddress,
getRecipientStore()));
}
public MessageCache getMessageCache() {
return getOrCreate(() -> messageCache,
- () -> messageCache = new MessageCache(getMessageCachePath(dataPath, account)));
+ () -> messageCache = new MessageCache(getMessageCachePath(dataPath, accountPath)));
}
public AccountDatabase getAccountDatabase() {
return getOrCreate(() -> accountDatabase, () -> {
try {
- accountDatabase = AccountDatabase.init(getDatabaseFile(dataPath, account));
+ accountDatabase = AccountDatabase.init(getDatabaseFile(dataPath, accountPath));
} catch (SQLException e) {
throw new RuntimeException(e);
}
() -> messageSendLogStore = new MessageSendLogStore(getRecipientStore(), getAccountDatabase()));
}
- public String getAccount() {
- return account;
+ public String getNumber() {
+ return number;
}
public ACI getAci() {
}
public SignalServiceAddress getSelfAddress() {
- return new SignalServiceAddress(aci, account);
+ return new SignalServiceAddress(aci, number);
}
public RecipientAddress getSelfRecipientAddress() {
- return new RecipientAddress(aci == null ? null : aci.uuid(), account);
+ return new RecipientAddress(aci == null ? null : aci.uuid(), number);
}
public RecipientId getSelfRecipientId() {
--- /dev/null
+package org.asamk.signal.manager.storage.accounts;
+
+import org.whispersystems.signalservice.api.push.ACI;
+import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class AccountsStore {
+
+ private final File dataPath;
+
+ public AccountsStore(final File dataPath) {
+ this.dataPath = dataPath;
+ }
+
+ public Set<String> getAllNumbers() {
+ final var files = dataPath.listFiles();
+
+ if (files == null) {
+ return Set.of();
+ }
+
+ return Arrays.stream(files)
+ .filter(File::isFile)
+ .map(File::getName)
+ .filter(file -> PhoneNumberFormatter.isValidNumber(file, null))
+ .collect(Collectors.toSet());
+ }
+
+ public String getPathByNumber(String number) {
+ return number;
+ }
+
+ public String getPathByAci(ACI aci) {
+ return null;
+ }
+
+ public void updateAccount(String path, String number, ACI aci) {
+ // TODO remove number and uuid from all other accounts
+ if (!path.equals(number)) {
+ throw new UnsupportedOperationException("Updating number not supported yet");
+ }
+ }
+
+ public String addAccount(String number, ACI aci) {
+ // TODO remove number and uuid from all other accounts
+ return number;
+ }
+}
import org.asamk.signal.dbus.DbusProvisioningManagerImpl;
import org.asamk.signal.dbus.DbusRegistrationManagerImpl;
import org.asamk.signal.manager.Manager;
-import org.asamk.signal.manager.MultiAccountManager;
-import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager;
+import org.asamk.signal.manager.SignalAccountFiles;
import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.config.ServiceConfig;
? TrustNewIdentity.ON_FIRST_USE
: trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
+ final SignalAccountFiles signalAccountFiles = new SignalAccountFiles(configPath,
+ serviceEnvironment,
+ BaseConfig.USER_AGENT,
+ trustNewIdentity);
+
if (command instanceof ProvisioningCommand provisioningCommand) {
if (account != null) {
throw new UserErrorException("You cannot specify a account (phone number) when linking");
}
- handleProvisioningCommand(provisioningCommand, configPath, serviceEnvironment, outputWriter);
+ handleProvisioningCommand(provisioningCommand, signalAccountFiles, outputWriter);
return;
}
if (account == null) {
if (command instanceof MultiLocalCommand multiLocalCommand) {
- handleMultiLocalCommand(multiLocalCommand,
- configPath,
- serviceEnvironment,
- outputWriter,
- trustNewIdentity);
+ handleMultiLocalCommand(multiLocalCommand, signalAccountFiles, outputWriter);
return;
}
- var accounts = MultiAccountManager.getAllLocalAccountNumbers(configPath);
+ var accounts = signalAccountFiles.getAllLocalAccountNumbers();
if (accounts.size() == 0) {
throw new UserErrorException("No local users found, you first need to register or link an account");
} else if (accounts.size() > 1) {
"Multiple users found, you need to specify an account (phone number) with -a");
}
- account = accounts.get(0);
+ account = accounts.stream().findFirst().get();
} else if (!Manager.isValidNumber(account, null)) {
throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
}
if (command instanceof RegistrationCommand registrationCommand) {
- handleRegistrationCommand(registrationCommand, account, configPath, serviceEnvironment);
+ handleRegistrationCommand(registrationCommand, account, signalAccountFiles);
return;
}
throw new UserErrorException("Command only works in multi-account mode");
}
- handleLocalCommand((LocalCommand) command,
- account,
- configPath,
- serviceEnvironment,
- outputWriter,
- trustNewIdentity);
+ handleLocalCommand((LocalCommand) command, account, signalAccountFiles, outputWriter);
}
private void handleProvisioningCommand(
final ProvisioningCommand command,
- final File configPath,
- final ServiceEnvironment serviceEnvironment,
+ final SignalAccountFiles signalAccountFiles,
final OutputWriter outputWriter
) throws CommandException {
- var pm = ProvisioningManager.init(configPath, serviceEnvironment, BaseConfig.USER_AGENT);
+ var pm = signalAccountFiles.initProvisioningManager();
command.handleCommand(ns, pm, outputWriter);
}
}
private void handleRegistrationCommand(
- final RegistrationCommand command,
- final String account,
- final File configPath,
- final ServiceEnvironment serviceEnvironment
+ final RegistrationCommand command, final String account, final SignalAccountFiles signalAccountFiles
) throws CommandException {
- final RegistrationManager manager;
- try {
- manager = RegistrationManager.init(account, configPath, serviceEnvironment, BaseConfig.USER_AGENT);
- } catch (Throwable e) {
- throw new UnexpectedErrorException("Error loading or creating state file: "
- + e.getMessage()
- + " ("
- + e.getClass().getSimpleName()
- + ")", e);
- }
- try (manager) {
+ try (final var manager = loadRegistrationManager(account, signalAccountFiles)) {
command.handleCommand(ns, manager);
} catch (IOException e) {
logger.warn("Cleanup failed", e);
private void handleLocalCommand(
final LocalCommand command,
final String account,
- final File configPath,
- final ServiceEnvironment serviceEnvironment,
- final OutputWriter outputWriter,
- final TrustNewIdentity trustNewIdentity
+ final SignalAccountFiles signalAccountFiles,
+ final OutputWriter outputWriter
) throws CommandException {
- try (var m = loadManager(account, configPath, serviceEnvironment, trustNewIdentity)) {
+ try (var m = loadManager(account, signalAccountFiles)) {
command.handleCommand(ns, m, outputWriter);
} catch (IOException e) {
logger.warn("Cleanup failed", e);
private void handleMultiLocalCommand(
final MultiLocalCommand command,
- final File configPath,
- final ServiceEnvironment serviceEnvironment,
- final OutputWriter outputWriter,
- final TrustNewIdentity trustNewIdentity
+ final SignalAccountFiles signalAccountFiles,
+ final OutputWriter outputWriter
) throws CommandException {
- try (var multiAccountManager = MultiAccountManager.init(configPath,
- serviceEnvironment,
- BaseConfig.USER_AGENT,
- trustNewIdentity)) {
+ try (var multiAccountManager = signalAccountFiles.initMultiAccountManager()) {
command.handleCommand(ns, multiAccountManager, outputWriter);
}
}
}
}
+ private RegistrationManager loadRegistrationManager(
+ final String account, final SignalAccountFiles signalAccountFiles
+ ) throws UnexpectedErrorException {
+ try {
+ return signalAccountFiles.initRegistrationManager(account);
+ } catch (Throwable e) {
+ throw new UnexpectedErrorException("Error loading or creating state file: "
+ + e.getMessage()
+ + " ("
+ + e.getClass().getSimpleName()
+ + ")", e);
+ }
+ }
+
private Manager loadManager(
- final String account,
- final File configPath,
- final ServiceEnvironment serviceEnvironment,
- final TrustNewIdentity trustNewIdentity
+ final String account, final SignalAccountFiles signalAccountFiles
) throws CommandException {
logger.trace("Loading account file for {}", account);
try {
- return Manager.init(account, configPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity);
+ return signalAccountFiles.initManager(account);
} catch (NotRegisteredException e) {
throw new UserErrorException("User " + account + " is not registered.");
} catch (AccountCheckException ace) {
import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException;
-import org.asamk.signal.manager.Manager;
+import org.asamk.signal.manager.ManagerLogger;
import org.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.bridge.SLF4JBridgeHandler;
if (verboseLevel > 0) {
java.util.logging.Logger.getLogger("")
.setLevel(verboseLevel > 2 ? java.util.logging.Level.FINEST : java.util.logging.Level.INFO);
- Manager.initLogger();
+ ManagerLogger.initLogger();
}
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();