From: AsamK Date: Mon, 23 Aug 2021 13:50:03 +0000 (+0200) Subject: Add new --trust-new-identities global parameter X-Git-Tag: v0.9.0~50 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/6c3106db5df80a23514864405829d20beb04955f Add new --trust-new-identities global parameter Closes #360 --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af9e358..e2ba6843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - 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) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 80c5fbbb..60ee4071 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -43,6 +43,7 @@ import org.asamk.signal.manager.storage.groups.GroupInfo; 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; @@ -270,7 +271,11 @@ public class Manager implements Closeable { } 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); @@ -278,7 +283,7 @@ public class Manager implements Closeable { 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(); diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 7801e999..80c214f7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -20,6 +20,7 @@ import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.util.KeyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,7 +122,8 @@ public class ProvisioningManager { deviceId, ret.getIdentity(), registrationId, - profileKey); + profileKey, + TrustNewIdentity.ON_FIRST_USE); Manager m = null; try { @@ -161,7 +163,7 @@ public class ProvisioningManager { 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; diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 653f9cb4..95d43fd6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.util.KeyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -102,12 +103,13 @@ public class RegistrationManager implements Closeable { 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); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 9b61fbb7..477e02dc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; 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; @@ -106,12 +107,14 @@ public class SignalAccount implements Closeable { 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())) { @@ -128,7 +131,12 @@ public class SignalAccount implements Closeable { } 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); @@ -142,7 +150,7 @@ public class SignalAccount implements Closeable { 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); @@ -157,7 +165,10 @@ public class SignalAccount implements Closeable { } 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); @@ -167,7 +178,8 @@ public class SignalAccount implements Closeable { identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username), recipientStore::resolveRecipient, identityKey, - registrationId); + registrationId, + trustNewIdentity); signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, @@ -186,7 +198,8 @@ public class SignalAccount implements Closeable { int deviceId, IdentityKeyPair identityKey, int registrationId, - ProfileKey profileKey + ProfileKey profileKey, + final TrustNewIdentity trustNewIdentity ) throws IOException { IOUtils.createPrivateDirectories(dataPath); var fileName = getFileName(dataPath, username); @@ -199,10 +212,11 @@ public class SignalAccount implements Closeable { 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(); @@ -227,7 +241,8 @@ public class SignalAccount implements Closeable { int deviceId, IdentityKeyPair identityKey, int registrationId, - ProfileKey profileKey + ProfileKey profileKey, + final TrustNewIdentity trustNewIdentity ) throws IOException { var fileName = getFileName(dataPath, username); IOUtils.createPrivateFile(fileName); @@ -237,7 +252,7 @@ public class SignalAccount implements Closeable { 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); @@ -339,7 +354,9 @@ public class SignalAccount implements Closeable { 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); @@ -428,7 +445,7 @@ public class SignalAccount implements Closeable { migratedLegacyConfig = true; } - initStores(dataPath, identityKeyPair, registrationId); + initStores(dataPath, identityKeyPair, registrationId, trustNewIdentity); migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index d1cfceda..0cbdf347 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -42,17 +42,20 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden 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 @@ -80,7 +83,10 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden 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; @@ -108,13 +114,17 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden @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 diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java new file mode 100644 index 00000000..b2db73a3 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java @@ -0,0 +1,7 @@ +package org.asamk.signal.manager.storage.identities; + +public enum TrustNewIdentity { + ALWAYS, + ON_FIRST_USE, + NEVER +} diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index de554c02..b8251eb1 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -58,6 +58,13 @@ Make request via system dbus. *-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 diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 6b31e2bc..1ff1a909 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -24,6 +24,7 @@ import org.asamk.signal.manager.ProvisioningManager; 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; @@ -74,6 +75,11 @@ public class App { .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) -> { @@ -125,11 +131,6 @@ public class App { dataPath = getDefaultDataPath(); } - final var serviceEnvironmentCli = ns.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"); @@ -139,6 +140,16 @@ public class App { throw new UserErrorException("Missing required native library dependency: libsignal-client"); } + final var serviceEnvironmentCli = ns.get("service-environment"); + final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE + ? ServiceEnvironment.LIVE + : ServiceEnvironment.SANDBOX; + + final var trustNewIdentityCli = ns.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"); @@ -156,7 +167,8 @@ public class App { dataPath, serviceEnvironment, usernames, - outputWriter); + outputWriter, + trustNewIdentity); return; } @@ -181,7 +193,12 @@ public class App { 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( @@ -222,9 +239,10 @@ public class App { 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); @@ -236,12 +254,13 @@ public class App { final File dataPath, final ServiceEnvironment serviceEnvironment, final List usernames, - final OutputWriter outputWriter + final OutputWriter outputWriter, + final TrustNewIdentity trustNewIdentity ) throws CommandException { final var managers = new ArrayList(); 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()); } @@ -269,11 +288,14 @@ public class App { } 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) { diff --git a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java new file mode 100644 index 00000000..5cc36bbd --- /dev/null +++ b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java @@ -0,0 +1,22 @@ +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"; + } + }, +}