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 - 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. 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 ## [0.8.5] - 2021-08-07
### Added ### Added
- Source name is included in JSON receive output (Thanks @technillogue) - 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.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.groups.GroupInfoV2;
import org.asamk.signal.manager.storage.identities.IdentityInfo; 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.messageCache.CachedMessage;
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;
@ -270,7 +271,11 @@ public class Manager implements Closeable {
} }
public static Manager init( 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 { ) throws IOException, NotRegisteredException {
var pathConfig = PathConfig.createDefault(settingsPath); var pathConfig = PathConfig.createDefault(settingsPath);
@ -278,7 +283,7 @@ public class Manager implements Closeable {
throw new NotRegisteredException(); throw new NotRegisteredException();
} }
var account = SignalAccount.load(pathConfig.getDataPath(), username, true); var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity);
if (!account.isRegistered()) { if (!account.isRegistered()) {
throw new NotRegisteredException(); 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.ServiceEnvironment;
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.identities.TrustNewIdentity;
import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.KeyUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -121,7 +122,8 @@ public class ProvisioningManager {
deviceId, deviceId,
ret.getIdentity(), ret.getIdentity(),
registrationId, registrationId,
profileKey); profileKey,
TrustNewIdentity.ON_FIRST_USE);
Manager m = null; Manager m = null;
try { try {
@ -161,7 +163,7 @@ public class ProvisioningManager {
private boolean canRelinkExistingAccount(final String number) throws IOException { private boolean canRelinkExistingAccount(final String number) throws IOException {
final SignalAccount signalAccount; final SignalAccount signalAccount;
try { try {
signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false); signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, 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;

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.config.ServiceEnvironmentConfig;
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.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;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -102,12 +103,13 @@ public class RegistrationManager implements Closeable {
username, username,
identityKey, identityKey,
registrationId, registrationId,
profileKey); profileKey,
TrustNewIdentity.ON_FIRST_USE);
return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); 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); 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.GroupInfoV1;
import org.asamk.signal.manager.storage.groups.GroupStore; import org.asamk.signal.manager.storage.groups.GroupStore;
import org.asamk.signal.manager.storage.identities.IdentityKeyStore; 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.messageCache.MessageCache;
import org.asamk.signal.manager.storage.prekeys.PreKeyStore; import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore; import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
@ -106,12 +107,14 @@ public class SignalAccount implements Closeable {
this.lock = lock; 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 fileName = getFileName(dataPath, username);
final var pair = openFileChannel(fileName, waitForLock); final var pair = openFileChannel(fileName, waitForLock);
try { try {
var account = new SignalAccount(pair.first(), pair.second()); var account = new SignalAccount(pair.first(), pair.second());
account.load(dataPath); account.load(dataPath, trustNewIdentity);
account.migrateLegacyConfigs(); account.migrateLegacyConfigs();
if (!username.equals(account.getUsername())) { if (!username.equals(account.getUsername())) {
@ -128,7 +131,12 @@ public class SignalAccount implements Closeable {
} }
public static SignalAccount create( 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 { ) throws IOException {
IOUtils.createPrivateDirectories(dataPath); IOUtils.createPrivateDirectories(dataPath);
var fileName = getFileName(dataPath, username); var fileName = getFileName(dataPath, username);
@ -142,7 +150,7 @@ public class SignalAccount implements Closeable {
account.username = username; account.username = username;
account.profileKey = profileKey; account.profileKey = profileKey;
account.initStores(dataPath, identityKey, registrationId); account.initStores(dataPath, identityKey, registrationId, trustNewIdentity);
account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient, account.recipientStore::resolveRecipient,
account::saveGroupStore); account::saveGroupStore);
@ -157,7 +165,10 @@ public class SignalAccount implements Closeable {
} }
private void initStores( 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 { ) throws IOException {
recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients); recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
@ -167,7 +178,8 @@ public class SignalAccount implements Closeable {
identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username), identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
recipientStore::resolveRecipient, recipientStore::resolveRecipient,
identityKey, identityKey,
registrationId); registrationId,
trustNewIdentity);
signalProtocolStore = new SignalProtocolStore(preKeyStore, signalProtocolStore = new SignalProtocolStore(preKeyStore,
signedPreKeyStore, signedPreKeyStore,
sessionStore, sessionStore,
@ -186,7 +198,8 @@ public class SignalAccount implements Closeable {
int deviceId, int deviceId,
IdentityKeyPair identityKey, IdentityKeyPair identityKey,
int registrationId, int registrationId,
ProfileKey profileKey ProfileKey profileKey,
final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
IOUtils.createPrivateDirectories(dataPath); IOUtils.createPrivateDirectories(dataPath);
var fileName = getFileName(dataPath, username); var fileName = getFileName(dataPath, username);
@ -199,10 +212,11 @@ public class SignalAccount implements Closeable {
deviceId, deviceId,
identityKey, identityKey,
registrationId, 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.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey);
account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
account.sessionStore.archiveAllSessions(); account.sessionStore.archiveAllSessions();
@ -227,7 +241,8 @@ public class SignalAccount implements Closeable {
int deviceId, int deviceId,
IdentityKeyPair identityKey, IdentityKeyPair identityKey,
int registrationId, int registrationId,
ProfileKey profileKey ProfileKey profileKey,
final TrustNewIdentity trustNewIdentity
) throws IOException { ) throws IOException {
var fileName = getFileName(dataPath, username); var fileName = getFileName(dataPath, username);
IOUtils.createPrivateFile(fileName); IOUtils.createPrivateFile(fileName);
@ -237,7 +252,7 @@ public class SignalAccount implements Closeable {
account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey); 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.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
account.recipientStore::resolveRecipient, account.recipientStore::resolveRecipient,
account::saveGroupStore); account::saveGroupStore);
@ -339,7 +354,9 @@ public class SignalAccount implements Closeable {
return !(!f.exists() || f.isDirectory()); return !(!f.exists() || f.isDirectory());
} }
private void load(File dataPath) throws IOException { private void load(
File dataPath, final TrustNewIdentity trustNewIdentity
) throws IOException {
JsonNode rootNode; JsonNode rootNode;
synchronized (fileChannel) { synchronized (fileChannel) {
fileChannel.position(0); fileChannel.position(0);
@ -428,7 +445,7 @@ public class SignalAccount implements Closeable {
migratedLegacyConfig = true; migratedLegacyConfig = true;
} }
initStores(dataPath, identityKeyPair, registrationId); initStores(dataPath, identityKeyPair, registrationId, trustNewIdentity);
migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig; 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 RecipientResolver resolver;
private final IdentityKeyPair identityKeyPair; private final IdentityKeyPair identityKeyPair;
private final int localRegistrationId; private final int localRegistrationId;
private final TrustNewIdentity trustNewIdentity;
public IdentityKeyStore( public IdentityKeyStore(
final File identitiesPath, final File identitiesPath,
final RecipientResolver resolver, final RecipientResolver resolver,
final IdentityKeyPair identityKeyPair, final IdentityKeyPair identityKeyPair,
final int localRegistrationId final int localRegistrationId,
final TrustNewIdentity trustNewIdentity
) { ) {
this.identitiesPath = identitiesPath; this.identitiesPath = identitiesPath;
this.resolver = resolver; this.resolver = resolver;
this.identityKeyPair = identityKeyPair; this.identityKeyPair = identityKeyPair;
this.localRegistrationId = localRegistrationId; this.localRegistrationId = localRegistrationId;
this.trustNewIdentity = trustNewIdentity;
} }
@Override @Override
@ -80,7 +83,10 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden
return false; 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); final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, added);
storeIdentityLocked(recipientId, newIdentityInfo); storeIdentityLocked(recipientId, newIdentityInfo);
return true; return true;
@ -108,13 +114,17 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden
@Override @Override
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
if (trustNewIdentity == TrustNewIdentity.ALWAYS) {
return true;
}
var recipientId = resolveRecipient(address.getName()); var recipientId = resolveRecipient(address.getName());
synchronized (cachedIdentities) { synchronized (cachedIdentities) {
final var identityInfo = loadIdentityLocked(recipientId); final var identityInfo = loadIdentityLocked(recipientId);
if (identityInfo == null) { if (identityInfo == null) {
// Identity not found // Identity not found
return true; return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE;
} }
// TODO implement possibility for different handling of incoming/outgoing trust decisions // 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:: *-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" 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 == Commands
=== register === register

View file

@ -24,6 +24,7 @@ import org.asamk.signal.manager.ProvisioningManager;
import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.RegistrationManager;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironment;
import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.IOUtils;
import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusException;
@ -74,6 +75,11 @@ public class App {
.type(Arguments.enumStringType(ServiceEnvironmentCli.class)) .type(Arguments.enumStringType(ServiceEnvironmentCli.class))
.setDefault(ServiceEnvironmentCli.LIVE); .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"); var subparsers = parser.addSubparsers().title("subcommands").dest("command");
Commands.getCommandSubparserAttachers().forEach((key, value) -> { Commands.getCommandSubparserAttachers().forEach((key, value) -> {
@ -125,11 +131,6 @@ public class App {
dataPath = getDefaultDataPath(); dataPath = getDefaultDataPath();
} }
final var serviceEnvironmentCli = ns.<ServiceEnvironmentCli>get("service-environment");
final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE
? ServiceEnvironment.LIVE
: ServiceEnvironment.SANDBOX;
if (!ServiceConfig.getCapabilities().isGv2()) { if (!ServiceConfig.getCapabilities().isGv2()) {
logger.warn("WARNING: Support for new group V2 is disabled," logger.warn("WARNING: Support for new group V2 is disabled,"
+ " because the required native library dependency is missing: libzkgroup"); + " 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"); 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 (command instanceof ProvisioningCommand) {
if (username != null) { if (username != null) {
throw new UserErrorException("You cannot specify a username (phone number) when linking"); throw new UserErrorException("You cannot specify a username (phone number) when linking");
@ -156,7 +167,8 @@ public class App {
dataPath, dataPath,
serviceEnvironment, serviceEnvironment,
usernames, usernames,
outputWriter); outputWriter,
trustNewIdentity);
return; return;
} }
@ -181,7 +193,12 @@ public class App {
throw new UserErrorException("Command only works via dbus"); 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( private void handleProvisioningCommand(
@ -222,9 +239,10 @@ public class App {
final String username, final String username,
final File dataPath, final File dataPath,
final ServiceEnvironment serviceEnvironment, final ServiceEnvironment serviceEnvironment,
final OutputWriter outputWriter final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
try (var m = loadManager(username, dataPath, serviceEnvironment)) { try (var m = loadManager(username, dataPath, serviceEnvironment, trustNewIdentity)) {
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);
@ -236,12 +254,13 @@ public class App {
final File dataPath, final File dataPath,
final ServiceEnvironment serviceEnvironment, final ServiceEnvironment serviceEnvironment,
final List<String> usernames, final List<String> usernames,
final OutputWriter outputWriter final OutputWriter outputWriter,
final TrustNewIdentity trustNewIdentity
) throws CommandException { ) throws CommandException {
final var managers = new ArrayList<Manager>(); final var managers = new ArrayList<Manager>();
for (String u : usernames) { for (String u : usernames) {
try { try {
managers.add(loadManager(u, dataPath, serviceEnvironment)); managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity));
} catch (CommandException e) { } catch (CommandException e) {
logger.warn("Ignoring {}: {}", u, e.getMessage()); logger.warn("Ignoring {}: {}", u, e.getMessage());
} }
@ -269,11 +288,14 @@ public class App {
} }
private Manager loadManager( 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 { ) throws CommandException {
Manager manager; Manager manager;
try { try {
manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity);
} catch (NotRegisteredException e) { } catch (NotRegisteredException e) {
throw new UserErrorException("User " + username + " is not registered."); throw new UserErrorException("User " + username + " is not registered.");
} catch (Throwable e) { } 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";
}
},
}