- Removed deprecated fallback data paths, only `$XDG_DATA_HOME/signal-cli` is used now
For those still using the old paths (`$HOME/.config/signal`, `$HOME/.config/textsecure`) you need to move those to the new location.
+### Added
+- New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification
+
## [0.8.5] - 2021-08-07
### Added
- Source name is included in JSON receive output (Thanks @technillogue)
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.identities.IdentityInfo;
+import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.storage.messageCache.CachedMessage;
import org.asamk.signal.manager.storage.recipients.Contact;
import org.asamk.signal.manager.storage.recipients.Profile;
}
public static Manager init(
- String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent
+ String username,
+ File settingsPath,
+ ServiceEnvironment serviceEnvironment,
+ String userAgent,
+ final TrustNewIdentity trustNewIdentity
) throws IOException, NotRegisteredException {
var pathConfig = PathConfig.createDefault(settingsPath);
throw new NotRegisteredException();
}
- var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
+ var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity);
if (!account.isRegistered()) {
throw new NotRegisteredException();
import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
deviceId,
ret.getIdentity(),
registrationId,
- profileKey);
+ profileKey,
+ TrustNewIdentity.ON_FIRST_USE);
Manager m = null;
try {
private boolean canRelinkExistingAccount(final String number) throws IOException {
final SignalAccount signalAccount;
try {
- signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false);
+ signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false, TrustNewIdentity.ON_FIRST_USE);
} catch (IOException e) {
logger.debug("Account in use or failed to load.", e);
return false;
import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.storage.SignalAccount;
+import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
username,
identityKey,
registrationId,
- profileKey);
+ profileKey,
+ TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
- var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
+ var account = SignalAccount.load(pathConfig.getDataPath(), username, true, TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent);
}
import org.asamk.signal.manager.storage.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
+import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.manager.storage.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
this.lock = lock;
}
- public static SignalAccount load(File dataPath, String username, boolean waitForLock) throws IOException {
+ public static SignalAccount load(
+ File dataPath, String username, boolean waitForLock, final TrustNewIdentity trustNewIdentity
+ ) throws IOException {
final var fileName = getFileName(dataPath, username);
final var pair = openFileChannel(fileName, waitForLock);
try {
var account = new SignalAccount(pair.first(), pair.second());
- account.load(dataPath);
+ account.load(dataPath, trustNewIdentity);
account.migrateLegacyConfigs();
if (!username.equals(account.getUsername())) {
}
public static SignalAccount create(
- File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
+ File dataPath,
+ String username,
+ IdentityKeyPair identityKey,
+ int registrationId,
+ ProfileKey profileKey,
+ final TrustNewIdentity trustNewIdentity
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
var fileName = getFileName(dataPath, username);
account.username = username;
account.profileKey = profileKey;
- account.initStores(dataPath, identityKey, registrationId);
+ account.initStores(dataPath, identityKey, registrationId, trustNewIdentity);
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient,
account::saveGroupStore);
}
private void initStores(
- final File dataPath, final IdentityKeyPair identityKey, final int registrationId
+ final File dataPath,
+ final IdentityKeyPair identityKey,
+ final int registrationId,
+ final TrustNewIdentity trustNewIdentity
) throws IOException {
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
recipientStore::resolveRecipient,
identityKey,
- registrationId);
+ registrationId,
+ trustNewIdentity);
signalProtocolStore = new SignalProtocolStore(preKeyStore,
signedPreKeyStore,
sessionStore,
int deviceId,
IdentityKeyPair identityKey,
int registrationId,
- ProfileKey profileKey
+ ProfileKey profileKey,
+ final TrustNewIdentity trustNewIdentity
) throws IOException {
IOUtils.createPrivateDirectories(dataPath);
var fileName = getFileName(dataPath, username);
deviceId,
identityKey,
registrationId,
- profileKey);
+ profileKey,
+ trustNewIdentity);
}
- final var account = load(dataPath, username, true);
+ final var account = load(dataPath, username, true, trustNewIdentity);
account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.sessionStore.archiveAllSessions();
int deviceId,
IdentityKeyPair identityKey,
int registrationId,
- ProfileKey profileKey
+ ProfileKey profileKey,
+ final TrustNewIdentity trustNewIdentity
) throws IOException {
var fileName = getFileName(dataPath, username);
IOUtils.createPrivateFile(fileName);
account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
- account.initStores(dataPath, identityKey, registrationId);
+ account.initStores(dataPath, identityKey, registrationId, trustNewIdentity);
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient,
account::saveGroupStore);
return !(!f.exists() || f.isDirectory());
}
- private void load(File dataPath) throws IOException {
+ private void load(
+ File dataPath, final TrustNewIdentity trustNewIdentity
+ ) throws IOException {
JsonNode rootNode;
synchronized (fileChannel) {
fileChannel.position(0);
migratedLegacyConfig = true;
}
- initStores(dataPath, identityKeyPair, registrationId);
+ initStores(dataPath, identityKeyPair, registrationId, trustNewIdentity);
migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig;
private final RecipientResolver resolver;
private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId;
+ private final TrustNewIdentity trustNewIdentity;
public IdentityKeyStore(
final File identitiesPath,
final RecipientResolver resolver,
final IdentityKeyPair identityKeyPair,
- final int localRegistrationId
+ final int localRegistrationId,
+ final TrustNewIdentity trustNewIdentity
) {
this.identitiesPath = identitiesPath;
this.resolver = resolver;
this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId;
+ this.trustNewIdentity = trustNewIdentity;
}
@Override
return false;
}
- final var trustLevel = identityInfo == null ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
+ final var trustLevel = trustNewIdentity == TrustNewIdentity.ALWAYS || (
+ trustNewIdentity == TrustNewIdentity.ON_FIRST_USE && identityInfo == null
+ ) ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
+ logger.debug("Storing new identity for recipient {} with trust {}", recipientId, trustLevel);
final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, added);
storeIdentityLocked(recipientId, newIdentityInfo);
return true;
@Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
+ if (trustNewIdentity == TrustNewIdentity.ALWAYS) {
+ return true;
+ }
+
var recipientId = resolveRecipient(address.getName());
synchronized (cachedIdentities) {
final var identityInfo = loadIdentityLocked(recipientId);
if (identityInfo == null) {
// Identity not found
- return true;
+ return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE;
}
// TODO implement possibility for different handling of incoming/outgoing trust decisions
--- /dev/null
+package org.asamk.signal.manager.storage.identities;
+
+public enum TrustNewIdentity {
+ ALWAYS,
+ ON_FIRST_USE,
+ NEVER
+}
*-o* OUTPUT-MODE, *--output* OUTPUT-MODE::
Specify if you want commands to output in either "plain-text" mode or in "json". Defaults to "plain-text"
+*--trust-new-identities* TRUST-MODE::
+Choose when to trust new identities:
+- `on-first-use` (default): Trust the first seen identity key from new users,
+ changed keys must be verified manually
+- `always`: Trust any new identity key without verification
+- `never`: Don't trust any unknown identity key, every key must be verified manually
+
== Commands
=== register
import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment;
+import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
.type(Arguments.enumStringType(ServiceEnvironmentCli.class))
.setDefault(ServiceEnvironmentCli.LIVE);
+ parser.addArgument("--trust-new-identities")
+ .help("Choose when to trust new identities.")
+ .type(Arguments.enumStringType(TrustNewIdentityCli.class))
+ .setDefault(TrustNewIdentityCli.ON_FIRST_USE);
+
var subparsers = parser.addSubparsers().title("subcommands").dest("command");
Commands.getCommandSubparserAttachers().forEach((key, value) -> {
dataPath = getDefaultDataPath();
}
- final var serviceEnvironmentCli = ns.<ServiceEnvironmentCli>get("service-environment");
- final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE
- ? ServiceEnvironment.LIVE
- : ServiceEnvironment.SANDBOX;
-
if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup");
throw new UserErrorException("Missing required native library dependency: libsignal-client");
}
+ final var serviceEnvironmentCli = ns.<ServiceEnvironmentCli>get("service-environment");
+ final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE
+ ? ServiceEnvironment.LIVE
+ : ServiceEnvironment.SANDBOX;
+
+ final var trustNewIdentityCli = ns.<TrustNewIdentityCli>get("trust-new-identities");
+ final var trustNewIdentity = trustNewIdentityCli == TrustNewIdentityCli.ON_FIRST_USE
+ ? TrustNewIdentity.ON_FIRST_USE
+ : trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER;
+
if (command instanceof ProvisioningCommand) {
if (username != null) {
throw new UserErrorException("You cannot specify a username (phone number) when linking");
dataPath,
serviceEnvironment,
usernames,
- outputWriter);
+ outputWriter,
+ trustNewIdentity);
return;
}
throw new UserErrorException("Command only works via dbus");
}
- handleLocalCommand((LocalCommand) command, username, dataPath, serviceEnvironment, outputWriter);
+ handleLocalCommand((LocalCommand) command,
+ username,
+ dataPath,
+ serviceEnvironment,
+ outputWriter,
+ trustNewIdentity);
}
private void handleProvisioningCommand(
final String username,
final File dataPath,
final ServiceEnvironment serviceEnvironment,
- final OutputWriter outputWriter
+ final OutputWriter outputWriter,
+ final TrustNewIdentity trustNewIdentity
) throws CommandException {
- try (var m = loadManager(username, dataPath, serviceEnvironment)) {
+ try (var m = loadManager(username, dataPath, serviceEnvironment, trustNewIdentity)) {
command.handleCommand(ns, m, outputWriter);
} catch (IOException e) {
logger.warn("Cleanup failed", e);
final File dataPath,
final ServiceEnvironment serviceEnvironment,
final List<String> usernames,
- final OutputWriter outputWriter
+ final OutputWriter outputWriter,
+ final TrustNewIdentity trustNewIdentity
) throws CommandException {
final var managers = new ArrayList<Manager>();
for (String u : usernames) {
try {
- managers.add(loadManager(u, dataPath, serviceEnvironment));
+ managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity));
} catch (CommandException e) {
logger.warn("Ignoring {}: {}", u, e.getMessage());
}
}
private Manager loadManager(
- final String username, final File dataPath, final ServiceEnvironment serviceEnvironment
+ final String username,
+ final File dataPath,
+ final ServiceEnvironment serviceEnvironment,
+ final TrustNewIdentity trustNewIdentity
) throws CommandException {
Manager manager;
try {
- manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT);
+ manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity);
} catch (NotRegisteredException e) {
throw new UserErrorException("User " + username + " is not registered.");
} catch (Throwable e) {
--- /dev/null
+package org.asamk.signal;
+
+public enum TrustNewIdentityCli {
+ ALWAYS {
+ @Override
+ public String toString() {
+ return "always";
+ }
+ },
+ ON_FIRST_USE {
+ @Override
+ public String toString() {
+ return "on-first-use";
+ }
+ },
+ NEVER {
+ @Override
+ public String toString() {
+ return "never";
+ }
+ },
+}