Add new --trust-new-identities global parameter

Closes #360
This commit is contained in:
AsamK 2021-08-23 15:50:03 +02:00
parent 6dd1a21606
commit 6c3106db5d
10 changed files with 132 additions and 35 deletions

View file

@ -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)

View file

@ -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();

View file

@ -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;

View file

@ -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);
}

View file

@ -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;

View file

@ -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

View file

@ -0,0 +1,7 @@
package org.asamk.signal.manager.storage.identities;
public enum TrustNewIdentity {
ALWAYS,
ON_FIRST_USE,
NEVER
}

View file

@ -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

View file

@ -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.<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");
@ -139,6 +140,16 @@ public class App {
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");
@ -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<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());
}
@ -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) {

View file

@ -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";
}
},
}