Add SignalAccountFiles as a central entry point

This commit is contained in:
AsamK 2022-02-09 19:07:23 +01:00
parent 1db7f8d76e
commit ff6b733cd0
19 changed files with 398 additions and 304 deletions

View file

@ -9,7 +9,7 @@ public class LibSignalLogger implements SignalProtocolLogger {
private final static Logger logger = LoggerFactory.getLogger("LibSignal"); private final static Logger logger = LoggerFactory.getLogger("LibSignal");
public static void initLogger() { static void initLogger() {
SignalProtocolLoggerProvider.setProvider(new LibSignalLogger()); SignalProtocolLoggerProvider.setProvider(new LibSignalLogger());
} }

View file

@ -1,6 +1,5 @@
package org.asamk.signal.manager; 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.AttachmentInvalidException;
import org.asamk.signal.manager.api.Configuration; import org.asamk.signal.manager.api.Configuration;
import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Device;
@ -12,7 +11,6 @@ import org.asamk.signal.manager.api.InvalidStickerException;
import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.Message;
import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.NotMasterDeviceException; 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.Pair;
import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.RecipientIdentifier;
import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendGroupMessageResults;
@ -23,16 +21,12 @@ import org.asamk.signal.manager.api.StickerPackUrl;
import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.api.TypingAction;
import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.api.UpdateGroup; 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.GroupId;
import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupInviteLinkUrl;
import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; 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.Contact;
import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.Profile;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
@ -51,45 +45,6 @@ import java.util.UUID;
public interface Manager extends Closeable { 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) { static boolean isValidNumber(final String e164Number, final String countryCode) {
return PhoneNumberFormatter.isValidNumber(e164Number, countryCode); return PhoneNumberFormatter.isValidNumber(e164Number, countryCode);
} }

View file

@ -45,6 +45,7 @@ import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException;
import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.LastGroupAdminException;
import org.asamk.signal.manager.groups.NotAGroupMemberException; 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.helper.Context;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfo;
@ -99,6 +100,7 @@ class ManagerImpl implements Manager {
private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class);
private SignalAccount account; private SignalAccount account;
private final AccountFileUpdater accountFileUpdater;
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
private final Context context; private final Context context;
@ -114,13 +116,15 @@ class ManagerImpl implements Manager {
ManagerImpl( ManagerImpl(
SignalAccount account, SignalAccount account,
PathConfig pathConfig, PathConfig pathConfig,
AccountFileUpdater accountFileUpdater,
ServiceEnvironmentConfig serviceEnvironmentConfig, ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent String userAgent
) { ) {
this.account = account; this.account = account;
this.accountFileUpdater = accountFileUpdater;
final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(), final var credentialsProvider = new DynamicCredentialsProvider(account.getAci(),
account.getAccount(), account.getNumber(),
account.getPassword(), account.getPassword(),
account.getDeviceId()); account.getDeviceId());
final var sessionLock = new SignalSessionLock() { final var sessionLock = new SignalSessionLock() {
@ -142,7 +146,12 @@ class ManagerImpl implements Manager {
final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath()); final var attachmentStore = new AttachmentStore(pathConfig.attachmentsPath());
final var stickerPackStore = new StickerPackStore(pathConfig.stickerPacksPath()); 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.getAccountHelper().setUnregisteredListener(this::close);
this.context.getReceiveHelper().setAuthenticationFailureListener(this::close); this.context.getReceiveHelper().setAuthenticationFailureListener(this::close);
this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> { this.context.getReceiveHelper().setCaughtUpWithOldMessagesListener(() -> {
@ -168,7 +177,7 @@ class ManagerImpl implements Manager {
@Override @Override
public String getSelfNumber() { public String getSelfNumber() {
return account.getAccount(); return account.getNumber();
} }
void checkAccountState() throws IOException { void checkAccountState() throws IOException {
@ -179,7 +188,7 @@ class ManagerImpl implements Manager {
public Map<String, Pair<String, UUID>> areUsersRegistered(Set<String> numbers) 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 -> { final var canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> {
try { try {
final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getAccount()); final var canonicalizedNumber = PhoneNumberFormatter.formatNumber(n, account.getNumber());
if (!canonicalizedNumber.equals(n)) { if (!canonicalizedNumber.equals(n)) {
logger.debug("Normalized number {} to {}.", n, canonicalizedNumber); logger.debug("Normalized number {} to {}.", n, canonicalizedNumber);
} }

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager;
public class ManagerLogger {
public static void initLogger() {
LibSignalLogger.initLogger();
}
}

View file

@ -1,58 +1,13 @@
package org.asamk.signal.manager; 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.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.Consumer; import java.util.function.Consumer;
public interface MultiAccountManager extends AutoCloseable { 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<String> getAccountNumbers();
List<Manager> getManagers(); List<Manager> getManagers();

View file

@ -1,10 +1,8 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
@ -25,21 +23,12 @@ class MultiAccountManagerImpl implements MultiAccountManager {
private final Set<Consumer<Manager>> onManagerRemovedHandlers = new HashSet<>(); private final Set<Consumer<Manager>> onManagerRemovedHandlers = new HashSet<>();
private final Set<Manager> managers = new HashSet<>(); private final Set<Manager> managers = new HashSet<>();
private final Map<URI, ProvisioningManager> provisioningManagers = new HashMap<>(); private final Map<URI, ProvisioningManager> provisioningManagers = new HashMap<>();
private final File settingsPath; private final SignalAccountFiles signalAccountFiles;
private final ServiceEnvironment serviceEnvironment;
private final String userAgent;
public MultiAccountManagerImpl( public MultiAccountManagerImpl(final Collection<Manager> managers, final SignalAccountFiles signalAccountFiles) {
final Collection<Manager> managers, this.signalAccountFiles = signalAccountFiles;
final File settingsPath,
final ServiceEnvironment serviceEnvironment,
final String userAgent
) {
this.managers.addAll(managers); this.managers.addAll(managers);
managers.forEach(m -> m.addClosedListener(() -> this.removeManager(m))); managers.forEach(m -> m.addClosedListener(() -> this.removeManager(m)));
this.settingsPath = settingsPath;
this.serviceEnvironment = serviceEnvironment;
this.userAgent = userAgent;
} }
@Override @Override
@ -99,9 +88,9 @@ class MultiAccountManagerImpl implements MultiAccountManager {
} }
@Override @Override
public Manager getManager(final String account) { public Manager getManager(final String number) {
synchronized (managers) { 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);
} }
} }
@ -119,12 +108,12 @@ class MultiAccountManagerImpl implements MultiAccountManager {
} }
private ProvisioningManager getNewProvisioningManager() { private ProvisioningManager getNewProvisioningManager() {
return ProvisioningManager.init(settingsPath, serviceEnvironment, userAgent, this::addManager); return signalAccountFiles.initProvisioningManager(this::addManager);
} }
@Override @Override
public RegistrationManager getNewRegistrationManager(String account) throws IOException { public RegistrationManager getNewRegistrationManager(String number) throws IOException {
return RegistrationManager.init(account, settingsPath, serviceEnvironment, userAgent, this::addManager); return signalAccountFiles.initRegistrationManager(number, this::addManager);
} }
@Override @Override

View file

@ -1,36 +1,13 @@
package org.asamk.signal.manager; package org.asamk.signal.manager;
import org.asamk.signal.manager.api.UserAlreadyExistsException; 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.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
public interface ProvisioningManager { 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; URI getDeviceLinkUri() throws TimeoutException, IOException;
String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException; String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException;

View file

@ -20,6 +20,7 @@ import org.asamk.signal.manager.api.UserAlreadyExistsException;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.storage.SignalAccount; 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.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -47,6 +48,7 @@ class ProvisioningManagerImpl implements ProvisioningManager {
private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final ServiceEnvironmentConfig serviceEnvironmentConfig;
private final String userAgent; private final String userAgent;
private final Consumer<Manager> newManagerListener; private final Consumer<Manager> newManagerListener;
private final AccountsStore accountsStore;
private final SignalServiceAccountManager accountManager; private final SignalServiceAccountManager accountManager;
private final IdentityKeyPair tempIdentityKey; private final IdentityKeyPair tempIdentityKey;
@ -57,12 +59,14 @@ class ProvisioningManagerImpl implements ProvisioningManager {
PathConfig pathConfig, PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig, ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent, String userAgent,
final Consumer<Manager> newManagerListener final Consumer<Manager> newManagerListener,
final AccountsStore accountsStore
) { ) {
this.pathConfig = pathConfig; this.pathConfig = pathConfig;
this.serviceEnvironmentConfig = serviceEnvironmentConfig; this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent; this.userAgent = userAgent;
this.newManagerListener = newManagerListener; this.newManagerListener = newManagerListener;
this.accountsStore = accountsStore;
tempIdentityKey = KeyUtils.generateIdentityKeyPair(); tempIdentityKey = KeyUtils.generateIdentityKeyPair();
registrationId = KeyHelper.generateRegistrationId(false); registrationId = KeyHelper.generateRegistrationId(false);
@ -91,11 +95,23 @@ class ProvisioningManagerImpl implements ProvisioningManager {
public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException { public String finishDeviceLink(String deviceName) throws IOException, TimeoutException, UserAlreadyExistsException {
var ret = accountManager.getNewDeviceRegistration(tempIdentityKey); var ret = accountManager.getNewDeviceRegistration(tempIdentityKey);
var number = ret.getNumber(); var number = ret.getNumber();
var aci = ret.getAci();
logger.info("Received link information from {}, linking in progress ...", number); logger.info("Received link information from {}, linking in progress ...", number);
if (SignalAccount.userExists(pathConfig.dataPath(), number) && !canRelinkExistingAccount(number)) { var accountPath = accountsStore.getPathByAci(aci);
throw new UserAlreadyExistsException(number, SignalAccount.getFileName(pathConfig.dataPath(), number)); 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 var encryptedDeviceName = deviceName == null
@ -115,8 +131,9 @@ class ProvisioningManagerImpl implements ProvisioningManager {
SignalAccount account = null; SignalAccount account = null;
try { try {
account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(), account = SignalAccount.createOrUpdateLinkedAccount(pathConfig.dataPath(),
accountPath,
number, number,
ret.getAci(), aci,
password, password,
encryptedDeviceName, encryptedDeviceName,
deviceId, deviceId,
@ -127,7 +144,12 @@ class ProvisioningManagerImpl implements ProvisioningManager {
ManagerImpl m = null; ManagerImpl m = null;
try { 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; account = null;
logger.debug("Refreshing pre keys"); logger.debug("Refreshing pre keys");
@ -162,10 +184,13 @@ class ProvisioningManagerImpl implements ProvisioningManager {
} }
} }
private boolean canRelinkExistingAccount(final String number) throws IOException { private boolean canRelinkExistingAccount(final String accountPath) throws IOException {
final SignalAccount signalAccount; final SignalAccount signalAccount;
try { 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) { } catch (IOException e) {
logger.debug("Account in use or failed to load.", e); logger.debug("Account in use or failed to load.", e);
return false; return false;
@ -177,7 +202,11 @@ class ProvisioningManagerImpl implements ProvisioningManager {
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) { try (m) {
m.checkAccountState(); m.checkAccountState();
} catch (AuthorizationFailedException ignored) { } catch (AuthorizationFailedException ignored) {

View file

@ -3,60 +3,12 @@ package org.asamk.signal.manager;
import org.asamk.signal.manager.api.CaptchaRequiredException; import org.asamk.signal.manager.api.CaptchaRequiredException;
import org.asamk.signal.manager.api.IncorrectPinException; import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.PinLockedException; 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.Closeable;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.function.Consumer;
public interface RegistrationManager extends Closeable { 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 register(boolean voiceVerification, String captcha) throws IOException, CaptchaRequiredException;
void verifyAccount( void verifyAccount(

View file

@ -21,6 +21,7 @@ import org.asamk.signal.manager.api.IncorrectPinException;
import org.asamk.signal.manager.api.PinLockedException; import org.asamk.signal.manager.api.PinLockedException;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig; 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.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
@ -59,16 +60,19 @@ class RegistrationManagerImpl implements RegistrationManager {
private final SignalServiceAccountManager accountManager; private final SignalServiceAccountManager accountManager;
private final PinHelper pinHelper; private final PinHelper pinHelper;
private final AccountFileUpdater accountFileUpdater;
RegistrationManagerImpl( RegistrationManagerImpl(
SignalAccount account, SignalAccount account,
PathConfig pathConfig, PathConfig pathConfig,
ServiceEnvironmentConfig serviceEnvironmentConfig, ServiceEnvironmentConfig serviceEnvironmentConfig,
String userAgent, String userAgent,
Consumer<Manager> newManagerListener Consumer<Manager> newManagerListener,
AccountFileUpdater accountFileUpdater
) { ) {
this.account = account; this.account = account;
this.pathConfig = pathConfig; this.pathConfig = pathConfig;
this.accountFileUpdater = accountFileUpdater;
this.serviceEnvironmentConfig = serviceEnvironmentConfig; this.serviceEnvironmentConfig = serviceEnvironmentConfig;
this.userAgent = userAgent; this.userAgent = userAgent;
this.newManagerListener = newManagerListener; this.newManagerListener = newManagerListener;
@ -82,7 +86,7 @@ class RegistrationManagerImpl implements RegistrationManager {
this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider( new DynamicCredentialsProvider(
// Using empty UUID, because registering doesn't work otherwise // 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, userAgent,
groupsV2Operations, groupsV2Operations,
ServiceConfig.AUTOMATIC_NETWORK_RETRY); ServiceConfig.AUTOMATIC_NETWORK_RETRY);
@ -101,7 +105,7 @@ class RegistrationManagerImpl implements RegistrationManager {
try { try {
final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), final var accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(),
new DynamicCredentialsProvider(account.getAci(), new DynamicCredentialsProvider(account.getAci(),
account.getAccount(), account.getNumber(),
account.getPassword(), account.getPassword(),
account.getDeviceId()), account.getDeviceId()),
userAgent, userAgent,
@ -120,7 +124,11 @@ class RegistrationManagerImpl implements RegistrationManager {
account.setRegistered(true); account.setRegistered(true);
logger.info("Reactivated existing account, verify is not necessary."); logger.info("Reactivated existing account, verify is not necessary.");
if (newManagerListener != null) { if (newManagerListener != null) {
final var m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); final var m = new ManagerImpl(account,
pathConfig,
accountFileUpdater,
serviceEnvironmentConfig,
userAgent);
account = null; account = null;
newManagerListener.accept(m); newManagerListener.accept(m);
} }
@ -187,11 +195,13 @@ class RegistrationManagerImpl implements RegistrationManager {
} }
//accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); //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; ManagerImpl m = null;
try { try {
m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); m = new ManagerImpl(account, pathConfig, accountFileUpdater, serviceEnvironmentConfig, userAgent);
account = null; account = null;
m.refreshPreKeys(); m.refreshPreKeys();

View file

@ -0,0 +1,152 @@
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));
}
}

View file

@ -0,0 +1,8 @@
package org.asamk.signal.manager.helper;
import org.whispersystems.signalservice.api.push.ACI;
public interface AccountFileUpdater {
void updateAccountIdentifiers(String number, ACI aci);
}

View file

@ -52,7 +52,9 @@ public class AccountHelper {
try { try {
context.getPreKeyHelper().refreshPreKeysIfNecessary(); context.getPreKeyHelper().refreshPreKeysIfNecessary();
if (account.getAci() == null) { 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(); updateAccountAttributes();
} catch (AuthorizationFailedException e) { } catch (AuthorizationFailedException e) {

View file

@ -4,8 +4,8 @@ import org.asamk.signal.manager.AttachmentStore;
import org.asamk.signal.manager.AvatarStore; import org.asamk.signal.manager.AvatarStore;
import org.asamk.signal.manager.JobExecutor; import org.asamk.signal.manager.JobExecutor;
import org.asamk.signal.manager.SignalDependencies; 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.SignalAccount;
import org.asamk.signal.manager.storage.stickerPacks.StickerPackStore;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -14,6 +14,7 @@ public class Context {
private final Object LOCK = new Object(); private final Object LOCK = new Object();
private final SignalAccount account; private final SignalAccount account;
private final AccountFileUpdater accountFileUpdater;
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
private final AvatarStore avatarStore; private final AvatarStore avatarStore;
private final StickerPackStore stickerPackStore; private final StickerPackStore stickerPackStore;
@ -40,12 +41,14 @@ public class Context {
public Context( public Context(
final SignalAccount account, final SignalAccount account,
final AccountFileUpdater accountFileUpdater,
final SignalDependencies dependencies, final SignalDependencies dependencies,
final AvatarStore avatarStore, final AvatarStore avatarStore,
final AttachmentStore attachmentStore, final AttachmentStore attachmentStore,
final StickerPackStore stickerPackStore final StickerPackStore stickerPackStore
) { ) {
this.account = account; this.account = account;
this.accountFileUpdater = accountFileUpdater;
this.dependencies = dependencies; this.dependencies = dependencies;
this.avatarStore = avatarStore; this.avatarStore = avatarStore;
this.stickerPackStore = stickerPackStore; this.stickerPackStore = stickerPackStore;
@ -57,6 +60,10 @@ public class Context {
return account; return account;
} }
public AccountFileUpdater getAccountFileUpdater() {
return accountFileUpdater;
}
public SignalDependencies getDependencies() { public SignalDependencies getDependencies() {
return dependencies; return dependencies;
} }

View file

@ -1,8 +1,8 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import org.asamk.signal.manager.SignalDependencies; 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.PhoneNumberSharingMode;
import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.SignalAccount;
import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Contact;
@ -188,7 +188,7 @@ public class StorageHelper {
return; return;
} }
if (!accountRecord.getE164().equals(account.getAccount())) { if (!accountRecord.getE164().equals(account.getNumber())) {
// TODO implement changed number handling // TODO implement changed number handling
} }

View file

@ -3,8 +3,8 @@ package org.asamk.signal.manager.storage;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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.Pair;
import org.asamk.signal.manager.api.TrustLevel;
import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupId;
import org.asamk.signal.manager.storage.configuration.ConfigurationStore; import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
import org.asamk.signal.manager.storage.contacts.ContactsStore; import org.asamk.signal.manager.storage.contacts.ContactsStore;
@ -87,7 +87,8 @@ public class SignalAccount implements Closeable {
private int previousStorageVersion; private int previousStorageVersion;
private File dataPath; private File dataPath;
private String account; private String accountPath;
private String number;
private ACI aci; private ACI aci;
private String encryptedDeviceName; private String encryptedDeviceName;
private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
@ -132,23 +133,18 @@ public class SignalAccount implements Closeable {
} }
public static SignalAccount load( public static SignalAccount load(
File dataPath, String account, boolean waitForLock, final TrustNewIdentity trustNewIdentity File dataPath, String accountPath, boolean waitForLock, final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
logger.trace("Opening account file"); logger.trace("Opening account file");
final var fileName = getFileName(dataPath, account); final var fileName = getFileName(dataPath, accountPath);
final var pair = openFileChannel(fileName, waitForLock); final var pair = openFileChannel(fileName, waitForLock);
try { try {
var signalAccount = new SignalAccount(pair.first(), pair.second()); var signalAccount = new SignalAccount(pair.first(), pair.second());
logger.trace("Loading account file"); logger.trace("Loading account file");
signalAccount.load(dataPath, trustNewIdentity); signalAccount.load(dataPath, accountPath, trustNewIdentity);
logger.trace("Migrating legacy parts of account file"); logger.trace("Migrating legacy parts of account file");
signalAccount.migrateLegacyConfigs(); signalAccount.migrateLegacyConfigs();
if (!account.equals(signalAccount.getAccount())) {
throw new IOException("Number in account file doesn't match expected number: "
+ signalAccount.getAccount());
}
return signalAccount; return signalAccount;
} catch (Throwable e) { } catch (Throwable e) {
pair.second().close(); pair.second().close();
@ -159,14 +155,15 @@ public class SignalAccount implements Closeable {
public static SignalAccount create( public static SignalAccount create(
File dataPath, File dataPath,
String account, String accountPath,
String number,
IdentityKeyPair identityKey, IdentityKeyPair identityKey,
int registrationId, int registrationId,
ProfileKey profileKey, ProfileKey profileKey,
final TrustNewIdentity trustNewIdentity final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
IOUtils.createPrivateDirectories(dataPath); IOUtils.createPrivateDirectories(dataPath);
var fileName = getFileName(dataPath, account); var fileName = getFileName(dataPath, accountPath);
if (!fileName.exists()) { if (!fileName.exists()) {
IOUtils.createPrivateFile(fileName); IOUtils.createPrivateFile(fileName);
} }
@ -174,14 +171,15 @@ public class SignalAccount implements Closeable {
final var pair = openFileChannel(fileName, true); final var pair = openFileChannel(fileName, true);
var signalAccount = new SignalAccount(pair.first(), pair.second()); var signalAccount = new SignalAccount(pair.first(), pair.second());
signalAccount.account = account; signalAccount.accountPath = accountPath;
signalAccount.number = number;
signalAccount.profileKey = profileKey; signalAccount.profileKey = profileKey;
signalAccount.dataPath = dataPath; signalAccount.dataPath = dataPath;
signalAccount.identityKeyPair = identityKey; signalAccount.identityKeyPair = identityKey;
signalAccount.localRegistrationId = registrationId; signalAccount.localRegistrationId = registrationId;
signalAccount.trustNewIdentity = trustNewIdentity; signalAccount.trustNewIdentity = trustNewIdentity;
signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, account), signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
signalAccount.getRecipientStore(), signalAccount.getRecipientStore(),
signalAccount::saveGroupStore); signalAccount::saveGroupStore);
signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore); signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
@ -198,7 +196,8 @@ public class SignalAccount implements Closeable {
public static SignalAccount createOrUpdateLinkedAccount( public static SignalAccount createOrUpdateLinkedAccount(
File dataPath, File dataPath,
String account, String accountPath,
String number,
ACI aci, ACI aci,
String password, String password,
String encryptedDeviceName, String encryptedDeviceName,
@ -209,10 +208,11 @@ public class SignalAccount implements Closeable {
final TrustNewIdentity trustNewIdentity final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
IOUtils.createPrivateDirectories(dataPath); IOUtils.createPrivateDirectories(dataPath);
var fileName = getFileName(dataPath, account); var fileName = getFileName(dataPath, accountPath);
if (!fileName.exists()) { if (!fileName.exists()) {
return createLinkedAccount(dataPath, return createLinkedAccount(dataPath,
account, accountPath,
number,
aci, aci,
password, password,
encryptedDeviceName, encryptedDeviceName,
@ -223,8 +223,8 @@ public class SignalAccount implements Closeable {
trustNewIdentity); trustNewIdentity);
} }
final var signalAccount = load(dataPath, account, true, trustNewIdentity); final var signalAccount = load(dataPath, accountPath, true, trustNewIdentity);
signalAccount.setProvisioningData(account, aci, password, encryptedDeviceName, deviceId, profileKey); signalAccount.setProvisioningData(number, aci, password, encryptedDeviceName, deviceId, profileKey);
signalAccount.getRecipientStore().resolveRecipientTrusted(signalAccount.getSelfAddress()); signalAccount.getRecipientStore().resolveRecipientTrusted(signalAccount.getSelfAddress());
signalAccount.getSessionStore().archiveAllSessions(); signalAccount.getSessionStore().archiveAllSessions();
signalAccount.getSenderKeyStore().deleteAll(); signalAccount.getSenderKeyStore().deleteAll();
@ -246,7 +246,8 @@ public class SignalAccount implements Closeable {
private static SignalAccount createLinkedAccount( private static SignalAccount createLinkedAccount(
File dataPath, File dataPath,
String account, String accountPath,
String number,
ACI aci, ACI aci,
String password, String password,
String encryptedDeviceName, String encryptedDeviceName,
@ -256,19 +257,20 @@ public class SignalAccount implements Closeable {
ProfileKey profileKey, ProfileKey profileKey,
final TrustNewIdentity trustNewIdentity final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
var fileName = getFileName(dataPath, account); var fileName = getFileName(dataPath, accountPath);
IOUtils.createPrivateFile(fileName); IOUtils.createPrivateFile(fileName);
final var pair = openFileChannel(fileName, true); final var pair = openFileChannel(fileName, true);
var signalAccount = new SignalAccount(pair.first(), pair.second()); 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.dataPath = dataPath;
signalAccount.accountPath = accountPath;
signalAccount.identityKeyPair = identityKey; signalAccount.identityKeyPair = identityKey;
signalAccount.localRegistrationId = registrationId; signalAccount.localRegistrationId = registrationId;
signalAccount.trustNewIdentity = trustNewIdentity; signalAccount.trustNewIdentity = trustNewIdentity;
signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, account), signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
signalAccount.getRecipientStore(), signalAccount.getRecipientStore(),
signalAccount::saveGroupStore); signalAccount::saveGroupStore);
signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore); signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
@ -283,14 +285,14 @@ public class SignalAccount implements Closeable {
} }
private void setProvisioningData( private void setProvisioningData(
final String account, final String number,
final ACI aci, final ACI aci,
final String password, final String password,
final String encryptedDeviceName, final String encryptedDeviceName,
final int deviceId, final int deviceId,
final ProfileKey profileKey final ProfileKey profileKey
) { ) {
this.account = account; this.number = number;
this.aci = aci; this.aci = aci;
this.password = password; this.password = password;
this.profileKey = profileKey; this.profileKey = profileKey;
@ -396,7 +398,7 @@ public class SignalAccount implements Closeable {
return new File(getUserPath(dataPath, account), "account.db"); 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) { if (account == null) {
return false; return false;
} }
@ -405,9 +407,11 @@ public class SignalAccount implements Closeable {
} }
private void load( private void load(
File dataPath, final TrustNewIdentity trustNewIdentity File dataPath, String accountPath, final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
JsonNode rootNode; this.dataPath = dataPath;
this.accountPath = accountPath;
final JsonNode rootNode;
synchronized (fileChannel) { synchronized (fileChannel) {
fileChannel.position(0); fileChannel.position(0);
rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
@ -423,7 +427,7 @@ public class SignalAccount implements Closeable {
previousStorageVersion = accountVersion; previousStorageVersion = accountVersion;
} }
account = Utils.getNotNullNode(rootNode, "username").asText(); number = Utils.getNotNullNode(rootNode, "username").asText();
if (rootNode.hasNonNull("password")) { if (rootNode.hasNonNull("password")) {
password = rootNode.get("password").asText(); password = rootNode.get("password").asText();
} }
@ -501,7 +505,6 @@ public class SignalAccount implements Closeable {
migratedLegacyConfig = true; migratedLegacyConfig = true;
} }
this.dataPath = dataPath;
this.identityKeyPair = identityKeyPair; this.identityKeyPair = identityKeyPair;
this.localRegistrationId = registrationId; this.localRegistrationId = registrationId;
this.trustNewIdentity = trustNewIdentity; this.trustNewIdentity = trustNewIdentity;
@ -511,11 +514,11 @@ public class SignalAccount implements Closeable {
if (rootNode.hasNonNull("groupStore")) { if (rootNode.hasNonNull("groupStore")) {
groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class); groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
groupStore = GroupStore.fromStorage(groupStoreStorage, groupStore = GroupStore.fromStorage(groupStoreStorage,
getGroupCachePath(dataPath, account), getGroupCachePath(dataPath, accountPath),
getRecipientStore(), getRecipientStore(),
this::saveGroupStore); this::saveGroupStore);
} else { } else {
groupStore = new GroupStore(getGroupCachePath(dataPath, account), groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
getRecipientStore(), getRecipientStore(),
this::saveGroupStore); this::saveGroupStore);
} }
@ -732,7 +735,7 @@ public class SignalAccount implements Closeable {
synchronized (fileChannel) { synchronized (fileChannel) {
var rootNode = jsonProcessor.createObjectNode(); var rootNode = jsonProcessor.createObjectNode();
rootNode.put("version", CURRENT_STORAGE_VERSION) rootNode.put("version", CURRENT_STORAGE_VERSION)
.put("username", account) .put("username", number)
.put("uuid", aci == null ? null : aci.toString()) .put("uuid", aci == null ? null : aci.toString())
.put("deviceName", encryptedDeviceName) .put("deviceName", encryptedDeviceName)
.put("deviceId", deviceId) .put("deviceId", deviceId)
@ -821,22 +824,23 @@ public class SignalAccount implements Closeable {
} }
private PreKeyStore getPreKeyStore() { private PreKeyStore getPreKeyStore() {
return getOrCreate(() -> preKeyStore, () -> preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, account))); return getOrCreate(() -> preKeyStore,
() -> preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, accountPath)));
} }
private SignedPreKeyStore getSignedPreKeyStore() { private SignedPreKeyStore getSignedPreKeyStore() {
return getOrCreate(() -> signedPreKeyStore, return getOrCreate(() -> signedPreKeyStore,
() -> signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, account))); () -> signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, accountPath)));
} }
public SessionStore getSessionStore() { public SessionStore getSessionStore() {
return getOrCreate(() -> sessionStore, return getOrCreate(() -> sessionStore,
() -> sessionStore = new SessionStore(getSessionsPath(dataPath, account), getRecipientStore())); () -> sessionStore = new SessionStore(getSessionsPath(dataPath, accountPath), getRecipientStore()));
} }
public IdentityKeyStore getIdentityKeyStore() { public IdentityKeyStore getIdentityKeyStore() {
return getOrCreate(() -> identityKeyStore, return getOrCreate(() -> identityKeyStore,
() -> identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, account), () -> identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, accountPath),
getRecipientStore(), getRecipientStore(),
identityKeyPair, identityKeyPair,
localRegistrationId, localRegistrationId,
@ -853,7 +857,7 @@ public class SignalAccount implements Closeable {
public RecipientStore getRecipientStore() { public RecipientStore getRecipientStore() {
return getOrCreate(() -> recipientStore, return getOrCreate(() -> recipientStore,
() -> recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, account), () -> recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, accountPath),
this::mergeRecipients)); this::mergeRecipients));
} }
@ -867,8 +871,8 @@ public class SignalAccount implements Closeable {
public SenderKeyStore getSenderKeyStore() { public SenderKeyStore getSenderKeyStore() {
return getOrCreate(() -> senderKeyStore, return getOrCreate(() -> senderKeyStore,
() -> senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, account), () -> senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, accountPath),
getSenderKeysPath(dataPath, account), getSenderKeysPath(dataPath, accountPath),
getRecipientStore()::resolveRecipientAddress, getRecipientStore()::resolveRecipientAddress,
getRecipientStore())); getRecipientStore()));
} }
@ -879,13 +883,13 @@ public class SignalAccount implements Closeable {
public MessageCache getMessageCache() { public MessageCache getMessageCache() {
return getOrCreate(() -> messageCache, return getOrCreate(() -> messageCache,
() -> messageCache = new MessageCache(getMessageCachePath(dataPath, account))); () -> messageCache = new MessageCache(getMessageCachePath(dataPath, accountPath)));
} }
public AccountDatabase getAccountDatabase() { public AccountDatabase getAccountDatabase() {
return getOrCreate(() -> accountDatabase, () -> { return getOrCreate(() -> accountDatabase, () -> {
try { try {
accountDatabase = AccountDatabase.init(getDatabaseFile(dataPath, account)); accountDatabase = AccountDatabase.init(getDatabaseFile(dataPath, accountPath));
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -897,8 +901,8 @@ public class SignalAccount implements Closeable {
() -> messageSendLogStore = new MessageSendLogStore(getRecipientStore(), getAccountDatabase())); () -> messageSendLogStore = new MessageSendLogStore(getRecipientStore(), getAccountDatabase()));
} }
public String getAccount() { public String getNumber() {
return account; return number;
} }
public ACI getAci() { public ACI getAci() {
@ -911,11 +915,11 @@ public class SignalAccount implements Closeable {
} }
public SignalServiceAddress getSelfAddress() { public SignalServiceAddress getSelfAddress() {
return new SignalServiceAddress(aci, account); return new SignalServiceAddress(aci, number);
} }
public RecipientAddress getSelfRecipientAddress() { public RecipientAddress getSelfRecipientAddress() {
return new RecipientAddress(aci == null ? null : aci.uuid(), account); return new RecipientAddress(aci == null ? null : aci.uuid(), number);
} }
public RecipientId getSelfRecipientId() { public RecipientId getSelfRecipientId() {

View file

@ -0,0 +1,52 @@
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;
}
}

View file

@ -22,9 +22,8 @@ import org.asamk.signal.dbus.DbusMultiAccountManagerImpl;
import org.asamk.signal.dbus.DbusProvisioningManagerImpl; import org.asamk.signal.dbus.DbusProvisioningManagerImpl;
import org.asamk.signal.dbus.DbusRegistrationManagerImpl; import org.asamk.signal.dbus.DbusRegistrationManagerImpl;
import org.asamk.signal.manager.Manager; 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.RegistrationManager;
import org.asamk.signal.manager.SignalAccountFiles;
import org.asamk.signal.manager.api.AccountCheckException; import org.asamk.signal.manager.api.AccountCheckException;
import org.asamk.signal.manager.api.NotRegisteredException; import org.asamk.signal.manager.api.NotRegisteredException;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
@ -164,26 +163,27 @@ public class App {
? TrustNewIdentity.ON_FIRST_USE ? TrustNewIdentity.ON_FIRST_USE
: trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER; : trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
final SignalAccountFiles signalAccountFiles = new SignalAccountFiles(configPath,
serviceEnvironment,
BaseConfig.USER_AGENT,
trustNewIdentity);
if (command instanceof ProvisioningCommand provisioningCommand) { if (command instanceof ProvisioningCommand provisioningCommand) {
if (account != null) { if (account != null) {
throw new UserErrorException("You cannot specify a account (phone number) when linking"); throw new UserErrorException("You cannot specify a account (phone number) when linking");
} }
handleProvisioningCommand(provisioningCommand, configPath, serviceEnvironment, outputWriter); handleProvisioningCommand(provisioningCommand, signalAccountFiles, outputWriter);
return; return;
} }
if (account == null) { if (account == null) {
if (command instanceof MultiLocalCommand multiLocalCommand) { if (command instanceof MultiLocalCommand multiLocalCommand) {
handleMultiLocalCommand(multiLocalCommand, handleMultiLocalCommand(multiLocalCommand, signalAccountFiles, outputWriter);
configPath,
serviceEnvironment,
outputWriter,
trustNewIdentity);
return; return;
} }
var accounts = MultiAccountManager.getAllLocalAccountNumbers(configPath); var accounts = signalAccountFiles.getAllLocalAccountNumbers();
if (accounts.size() == 0) { if (accounts.size() == 0) {
throw new UserErrorException("No local users found, you first need to register or link an account"); throw new UserErrorException("No local users found, you first need to register or link an account");
} else if (accounts.size() > 1) { } else if (accounts.size() > 1) {
@ -191,13 +191,13 @@ public class App {
"Multiple users found, you need to specify an account (phone number) with -a"); "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)) { } else if (!Manager.isValidNumber(account, null)) {
throw new UserErrorException("Invalid account (phone number), make sure you include the country code."); throw new UserErrorException("Invalid account (phone number), make sure you include the country code.");
} }
if (command instanceof RegistrationCommand registrationCommand) { if (command instanceof RegistrationCommand registrationCommand) {
handleRegistrationCommand(registrationCommand, account, configPath, serviceEnvironment); handleRegistrationCommand(registrationCommand, account, signalAccountFiles);
return; return;
} }
@ -205,21 +205,15 @@ public class App {
throw new UserErrorException("Command only works in multi-account mode"); throw new UserErrorException("Command only works in multi-account mode");
} }
handleLocalCommand((LocalCommand) command, handleLocalCommand((LocalCommand) command, account, signalAccountFiles, outputWriter);
account,
configPath,
serviceEnvironment,
outputWriter,
trustNewIdentity);
} }
private void handleProvisioningCommand( private void handleProvisioningCommand(
final ProvisioningCommand command, final ProvisioningCommand command,
final File configPath, final SignalAccountFiles signalAccountFiles,
final ServiceEnvironment serviceEnvironment,
final OutputWriter outputWriter final OutputWriter outputWriter
) throws CommandException { ) throws CommandException {
var pm = ProvisioningManager.init(configPath, serviceEnvironment, BaseConfig.USER_AGENT); var pm = signalAccountFiles.initProvisioningManager();
command.handleCommand(ns, pm, outputWriter); command.handleCommand(ns, pm, outputWriter);
} }
@ -240,22 +234,9 @@ public class App {
} }
private void handleRegistrationCommand( private void handleRegistrationCommand(
final RegistrationCommand command, final RegistrationCommand command, final String account, final SignalAccountFiles signalAccountFiles
final String account,
final File configPath,
final ServiceEnvironment serviceEnvironment
) throws CommandException { ) throws CommandException {
final RegistrationManager manager; try (final var manager = loadRegistrationManager(account, signalAccountFiles)) {
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) {
command.handleCommand(ns, manager); command.handleCommand(ns, manager);
} catch (IOException e) { } catch (IOException e) {
logger.warn("Cleanup failed", e); logger.warn("Cleanup failed", e);
@ -280,12 +261,10 @@ public class App {
private void handleLocalCommand( private void handleLocalCommand(
final LocalCommand command, final LocalCommand command,
final String account, final String account,
final File configPath, final SignalAccountFiles signalAccountFiles,
final ServiceEnvironment serviceEnvironment, final OutputWriter outputWriter
final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
try (var m = loadManager(account, configPath, serviceEnvironment, trustNewIdentity)) { try (var m = loadManager(account, signalAccountFiles)) {
command.handleCommand(ns, m, outputWriter); command.handleCommand(ns, m, outputWriter);
} catch (IOException e) { } catch (IOException e) {
logger.warn("Cleanup failed", e); logger.warn("Cleanup failed", e);
@ -310,15 +289,10 @@ public class App {
private void handleMultiLocalCommand( private void handleMultiLocalCommand(
final MultiLocalCommand command, final MultiLocalCommand command,
final File configPath, final SignalAccountFiles signalAccountFiles,
final ServiceEnvironment serviceEnvironment, final OutputWriter outputWriter
final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
try (var multiAccountManager = MultiAccountManager.init(configPath, try (var multiAccountManager = signalAccountFiles.initMultiAccountManager()) {
serviceEnvironment,
BaseConfig.USER_AGENT,
trustNewIdentity)) {
command.handleCommand(ns, multiAccountManager, outputWriter); command.handleCommand(ns, multiAccountManager, outputWriter);
} }
} }
@ -336,15 +310,26 @@ public class App {
} }
} }
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( private Manager loadManager(
final String account, final String account, final SignalAccountFiles signalAccountFiles
final File configPath,
final ServiceEnvironment serviceEnvironment,
final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
logger.trace("Loading account file for {}", account); logger.trace("Loading account file for {}", account);
try { try {
return Manager.init(account, configPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity); return signalAccountFiles.initManager(account);
} catch (NotRegisteredException e) { } catch (NotRegisteredException e) {
throw new UserErrorException("User " + account + " is not registered."); throw new UserErrorException("User " + account + " is not registered.");
} catch (AccountCheckException ace) { } catch (AccountCheckException ace) {

View file

@ -27,7 +27,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException;
import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException;
import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException;
import org.asamk.signal.commands.exceptions.UserErrorException; 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.asamk.signal.util.SecurityProvider;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.bridge.SLF4JBridgeHandler; import org.slf4j.bridge.SLF4JBridgeHandler;
@ -97,7 +97,7 @@ public class Main {
if (verboseLevel > 0) { if (verboseLevel > 0) {
java.util.logging.Logger.getLogger("") java.util.logging.Logger.getLogger("")
.setLevel(verboseLevel > 2 ? java.util.logging.Level.FINEST : java.util.logging.Level.INFO); .setLevel(verboseLevel > 2 ? java.util.logging.Level.FINEST : java.util.logging.Level.INFO);
Manager.initLogger(); ManagerLogger.initLogger();
} }
SLF4JBridgeHandler.removeHandlersForRootLogger(); SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install(); SLF4JBridgeHandler.install();