Internal: Create CryptoService without guice

This change creates the CryptoService during xpack plugin construction,
and also collapses the only implementation of CryptoService into a
class instead of an interface.

Original commit: elastic/x-pack-elasticsearch@7f00a5d6ef
This commit is contained in:
Ryan Ernst 2016-07-11 12:18:39 -07:00
parent 84a60d2548
commit 7438177313
21 changed files with 557 additions and 611 deletions

View File

@ -34,6 +34,7 @@ import org.elasticsearch.xpack.monitoring.MonitoringSettings;
import org.elasticsearch.xpack.monitoring.test.MonitoringIntegTestCase;
import org.junit.Before;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -220,7 +221,7 @@ public abstract class AbstractCollectorTestCase extends MonitoringIntegTestCase
public static class InternalXPackPlugin extends XPackPlugin {
public InternalXPackPlugin(Settings settings) {
public InternalXPackPlugin(Settings settings) throws IOException {
super(settings);
licensing = new InternalLicensing();
}

View File

@ -26,6 +26,7 @@ import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.monitoring.MonitoringLicensee;
import org.elasticsearch.xpack.monitoring.test.MonitoringIntegTestCase;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -161,7 +162,7 @@ public class LicenseIntegrationTests extends MonitoringIntegTestCase {
}
public static class InternalXPackPlugin extends XPackPlugin {
public InternalXPackPlugin(Settings settings) {
public InternalXPackPlugin(Settings settings) throws IOException {
super(settings);
licensing = new MockLicensing();
}

View File

@ -37,7 +37,7 @@ import org.elasticsearch.xpack.security.authc.file.FileRealm;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.authc.support.SecuredString;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.watcher.Watcher;
import org.hamcrest.Matcher;
import org.jboss.netty.util.internal.SystemPropertyUtil;
@ -537,7 +537,7 @@ public abstract class MonitoringIntegTestCase extends ESIntegTestCase {
.put("xpack.security.authc.realms.esusers.files.users", writeFile(folder, "users", USERS))
.put("xpack.security.authc.realms.esusers.files.users_roles", writeFile(folder, "users_roles", USER_ROLES))
.put(FileRolesStore.ROLES_FILE_SETTING.getKey(), writeFile(folder, "roles.yml", ROLES))
.put(InternalCryptoService.FILE_SETTING.getKey(), writeFile(folder, "system_key.yml", systemKey))
.put(CryptoService.FILE_SETTING.getKey(), writeFile(folder, "system_key.yml", systemKey))
.put("xpack.security.authc.sign_user_header", false)
.put("xpack.security.audit.enabled", auditLogsEnabled);
} catch (IOException ex) {
@ -547,7 +547,7 @@ public abstract class MonitoringIntegTestCase extends ESIntegTestCase {
static byte[] generateKey() {
try {
return InternalCryptoService.generateKey();
return CryptoService.generateKey();
} catch (Exception e) {
throw new RuntimeException(e);
}

View File

@ -5,6 +5,16 @@
*/
package org.elasticsearch.xpack.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.support.ActionFilter;
@ -12,6 +22,7 @@ import org.elasticsearch.common.Booleans;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.LifecycleComponent;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.LoggerMessageFormat;
import org.elasticsearch.common.logging.Loggers;
@ -21,6 +32,7 @@ import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.index.IndexModule;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.rest.RestHandler;
@ -64,8 +76,7 @@ import org.elasticsearch.xpack.security.authz.accesscontrol.OptOutQueryCache;
import org.elasticsearch.xpack.security.authz.accesscontrol.SecurityIndexSearcherWrapper;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.authz.store.NativeRolesStore;
import org.elasticsearch.xpack.security.crypto.CryptoModule;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.rest.SecurityRestModule;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
import org.elasticsearch.xpack.security.rest.action.realm.RestClearRealmCacheAction;
@ -90,15 +101,6 @@ import org.elasticsearch.xpack.security.user.AnonymousUser;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
@ -117,13 +119,17 @@ public class Security implements ActionPlugin {
private final boolean enabled;
private final boolean transportClientMode;
private SecurityLicenseState securityLicenseState;
private final CryptoService cryptoService;
public Security(Settings settings) {
public Security(Settings settings, Environment env) throws IOException {
this.settings = settings;
this.transportClientMode = XPackPlugin.transportClientMode(settings);
this.enabled = XPackPlugin.featureEnabled(settings, NAME, true);
if (enabled && !transportClientMode) {
if (enabled && transportClientMode == false) {
validateAutoCreateIndex(settings);
cryptoService = new CryptoService(settings, env);
} else {
cryptoService = null;
}
}
@ -143,8 +149,8 @@ public class Security implements ActionPlugin {
modules.add(new AuthenticationModule(settings));
modules.add(new AuthorizationModule(settings));
if (enabled == false) {
modules.add(b -> b.bind(CryptoService.class).toProvider(Providers.of(null)));
modules.add(new SecurityModule(settings, securityLicenseState));
modules.add(new CryptoModule(settings));
modules.add(new AuditTrailModule(settings));
modules.add(new SecurityTransportModule(settings));
return modules;
@ -154,8 +160,8 @@ public class Security implements ActionPlugin {
// which might not be the case during Plugin class instantiation. Once nodeModules are pulled
// everything should have been loaded
securityLicenseState = new SecurityLicenseState();
modules.add(b -> b.bind(CryptoService.class).toInstance(cryptoService));
modules.add(new SecurityModule(settings, securityLicenseState));
modules.add(new CryptoModule(settings));
modules.add(new AuditTrailModule(settings));
modules.add(new SecurityRestModule(settings));
modules.add(new SecurityActionModule(settings));
@ -185,13 +191,18 @@ public class Security implements ActionPlugin {
return Settings.EMPTY;
}
return additionalSettings(settings);
}
// pkg private for testing
static Settings additionalSettings(Settings settings) {
Settings.Builder settingsBuilder = Settings.builder();
settingsBuilder.put(NetworkModule.TRANSPORT_TYPE_KEY, Security.NAME);
settingsBuilder.put(NetworkModule.TRANSPORT_SERVICE_TYPE_KEY, Security.NAME);
settingsBuilder.put(NetworkModule.HTTP_TYPE_SETTING.getKey(), Security.NAME);
SecurityNettyHttpServerTransport.overrideSettings(settingsBuilder, settings);
addUserSettings(settingsBuilder);
addTribeSettings(settingsBuilder);
addUserSettings(settings, settingsBuilder);
addTribeSettings(settings, settingsBuilder);
return settingsBuilder.build();
}
@ -233,7 +244,7 @@ public class Security implements ActionPlugin {
SecurityNettyHttpServerTransport.addSettings(settingsList);
// encryption settings
InternalCryptoService.addSettings(settingsList);
CryptoService.addSettings(settingsList);
// hide settings
settingsList.add(Setting.listSetting(setting("hide_settings"), Collections.emptyList(), Function.identity(),
@ -345,7 +356,7 @@ public class Security implements ActionPlugin {
}
}
private void addUserSettings(Settings.Builder settingsBuilder) {
private static void addUserSettings(Settings settings, Settings.Builder settingsBuilder) {
String authHeaderSettingName = ThreadContext.PREFIX + "." + UsernamePasswordToken.BASIC_AUTH_HEADER;
if (settings.get(authHeaderSettingName) != null) {
return;
@ -373,7 +384,7 @@ public class Security implements ActionPlugin {
*
* - forcibly enabling it (that means it's not possible to disable security on the tribe clients)
*/
private void addTribeSettings(Settings.Builder settingsBuilder) {
private static void addTribeSettings(Settings settings, Settings.Builder settingsBuilder) {
Map<String, Settings> tribesSettings = settings.getGroups("tribe", true);
if (tribesSettings.isEmpty()) {
// it's not a tribe node

View File

@ -1,30 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.crypto;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.security.support.AbstractSecurityModule;
/**
*
*/
public class CryptoModule extends AbstractSecurityModule.Node {
public CryptoModule(Settings settings) {
super(settings);
}
@Override
protected void configureNode() {
if (securityEnabled == false) {
bind(CryptoService.class).toProvider(Providers.of(null));
return;
}
bind(InternalCryptoService.class).asEagerSingleton();
bind(CryptoService.class).to(InternalCryptoService.class).asEagerSingleton();
}
}

View File

@ -5,55 +5,497 @@
*/
package org.elasticsearch.xpack.security.crypto;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.security.authc.support.CharArrays;
import static org.elasticsearch.xpack.security.Security.setting;
import static org.elasticsearch.xpack.security.authc.support.SecuredString.constantTimeEquals;
/**
* Service that provides cryptographic methods based on a shared system key
*/
public interface CryptoService {
public class CryptoService extends AbstractComponent {
public static final String KEY_ALGO = "HmacSHA512";
public static final int KEY_SIZE = 1024;
static final String FILE_NAME = "system_key";
static final String HMAC_ALGO = "HmacSHA1";
static final String DEFAULT_ENCRYPTION_ALGORITHM = "AES/CTR/NoPadding";
static final String DEFAULT_KEY_ALGORITH = "AES";
static final String ENCRYPTED_TEXT_PREFIX = "::es_encrypted::";
static final byte[] ENCRYPTED_BYTE_PREFIX = ENCRYPTED_TEXT_PREFIX.getBytes(StandardCharsets.UTF_8);
static final int DEFAULT_KEY_LENGTH = 128;
static final int RANDOM_KEY_SIZE = 128;
private static final Pattern SIG_PATTERN = Pattern.compile("^\\$\\$[0-9]+\\$\\$[^\\$]*\\$\\$.+");
private static final byte[] HKDF_APP_INFO = "es-security-crypto-service".getBytes(StandardCharsets.UTF_8);
public static final Setting<String> FILE_SETTING = Setting.simpleString(setting("system_key.file"), Setting.Property.NodeScope);
public static final Setting<String> ENCRYPTION_ALGO_SETTING =
new Setting<>(setting("encryption.algorithm"), s -> DEFAULT_ENCRYPTION_ALGORITHM, s -> s, Setting.Property.NodeScope);
public static final Setting<Integer> ENCRYPTION_KEY_LENGTH_SETTING =
Setting.intSetting(setting("encryption_key.length"), DEFAULT_KEY_LENGTH, Setting.Property.NodeScope);
public static final Setting<String> ENCRYPTION_KEY_ALGO_SETTING =
new Setting<>(setting("encryption_key.algorithm"), DEFAULT_KEY_ALGORITH, s -> s, Setting.Property.NodeScope);
private final SecureRandom secureRandom = new SecureRandom();
private final String encryptionAlgorithm;
private final String keyAlgorithm;
private final int keyLength;
private final int ivLength;
private final Path keyFile;
private final SecretKey randomKey;
private final String randomKeyBase64;
private final SecretKey encryptionKey;
private final SecretKey systemKey;
private final SecretKey signingKey;
public CryptoService(Settings settings, Environment env) throws IOException {
super(settings);
this.encryptionAlgorithm = ENCRYPTION_ALGO_SETTING.get(settings);
this.keyLength = ENCRYPTION_KEY_LENGTH_SETTING.get(settings);
this.ivLength = keyLength / 8;
this.keyAlgorithm = ENCRYPTION_KEY_ALGO_SETTING.get(settings);
if (keyLength % 8 != 0) {
throw new IllegalArgumentException("invalid key length [" + keyLength + "]. value must be a multiple of 8");
}
keyFile = resolveSystemKey(settings, env);
systemKey = readSystemKey(keyFile);
randomKey = generateSecretKey(RANDOM_KEY_SIZE);
randomKeyBase64 = Base64.getUrlEncoder().encodeToString(randomKey.getEncoded());
signingKey = createSigningKey(systemKey, randomKey);
try {
encryptionKey = encryptionKey(systemKey, keyLength, keyAlgorithm);
} catch (NoSuchAlgorithmException nsae) {
throw new ElasticsearchException("failed to start crypto service. could not load encryption key", nsae);
}
logger.info("system key [{}] has been loaded", keyFile.toAbsolutePath());
}
public static byte[] generateKey() {
return generateSecretKey(KEY_SIZE).getEncoded();
}
static SecretKey generateSecretKey(int keyLength) {
try {
KeyGenerator generator = KeyGenerator.getInstance(KEY_ALGO);
generator.init(keyLength);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new ElasticsearchException("failed to generate key", e);
}
}
public static Path resolveSystemKey(Settings settings, Environment env) {
String location = FILE_SETTING.get(settings);
if (location.isEmpty()) {
return XPackPlugin.resolveConfigFile(env, FILE_NAME);
}
return XPackPlugin.resolveConfigFile(env, location);
}
static SecretKey createSigningKey(@Nullable SecretKey systemKey, SecretKey randomKey) {
assert randomKey != null;
if (systemKey != null) {
return systemKey;
} else {
// the random key is only 128 bits so we use HKDF to expand to 1024 bits with some application specific data mixed in
byte[] keyBytes = HmacSHA1HKDF.extractAndExpand(null, randomKey.getEncoded(), HKDF_APP_INFO, (KEY_SIZE / 8));
assert keyBytes.length * 8 == KEY_SIZE;
return new SecretKeySpec(keyBytes, KEY_ALGO);
}
}
private static SecretKey readSystemKey(Path file) {
if (!Files.exists(file)) {
return null;
}
try {
byte[] bytes = Files.readAllBytes(file);
return new SecretKeySpec(bytes, KEY_ALGO);
} catch (IOException e) {
throw new ElasticsearchException("could not read secret key", e);
}
}
/**
* Signs the given text and returns the signed text (original text + signature)
* @param text the string to sign
*/
String sign(String text) throws IOException;
public String sign(String text) throws IOException {
String sigStr = signInternal(text, signingKey);
return "$$" + sigStr.length() + "$$" + (systemKey == signingKey ? "" : randomKeyBase64) + "$$" + sigStr + text;
}
/**
* Unsigns the given signed text, verifies the original text with the attached signature and if valid returns
* the unsigned (original) text. If signature verification fails a {@link IllegalArgumentException} is thrown.
* @param text the string to unsign and verify
* @param signedText the string to unsign and verify
*/
String unsignAndVerify(String text);
public String unsignAndVerify(String signedText) {
if (!signedText.startsWith("$$") || signedText.length() < 2) {
throw new IllegalArgumentException("tampered signed text");
}
// $$34$$randomKeyBase64$$sigtext
String[] pieces = signedText.split("\\$\\$");
if (pieces.length != 4 || !pieces[0].equals("")) {
logger.debug("received signed text [{}] with [{}] parts", signedText, pieces.length);
throw new IllegalArgumentException("tampered signed text");
}
String text;
String base64RandomKey;
String receivedSignature;
try {
int length = Integer.parseInt(pieces[1]);
base64RandomKey = pieces[2];
receivedSignature = pieces[3].substring(0, length);
text = pieces[3].substring(length);
} catch (Exception e) {
logger.error("error occurred while parsing signed text", e);
throw new IllegalArgumentException("tampered signed text");
}
SecretKey signingKey;
// no random key, so we must have a system key
if (base64RandomKey.isEmpty()) {
if (systemKey == null) {
logger.debug("received signed text without random key information and no system key is present");
throw new IllegalArgumentException("tampered signed text");
}
signingKey = systemKey;
} else if (systemKey != null) {
// we have a system key and there is some random key data, this is an error
logger.debug("received signed text with random key information but a system key is present");
throw new IllegalArgumentException("tampered signed text");
} else {
byte[] randomKeyBytes;
try {
randomKeyBytes = Base64.getUrlDecoder().decode(base64RandomKey);
} catch (IllegalArgumentException e) {
logger.error("error occurred while decoding key data", e);
throw new IllegalStateException("error while verifying the signed text");
}
if (randomKeyBytes.length * 8 != RANDOM_KEY_SIZE) {
logger.debug("incorrect random key data length. received [{}] bytes", randomKeyBytes.length);
throw new IllegalArgumentException("tampered signed text");
}
SecretKey randomKey = new SecretKeySpec(randomKeyBytes, KEY_ALGO);
signingKey = createSigningKey(systemKey, randomKey);
}
try {
String sig = signInternal(text, signingKey);
if (constantTimeEquals(sig, receivedSignature)) {
return text;
}
} catch (Exception e) {
logger.error("error occurred while verifying signed text", e);
throw new IllegalStateException("error while verifying the signed text");
}
throw new IllegalArgumentException("tampered signed text");
}
/**
* Checks whether the given text is signed.
*/
boolean isSigned(String text);
public boolean isSigned(String text) {
return SIG_PATTERN.matcher(text).matches();
}
/**
* Encrypts the provided char array and returns the encrypted values in a char array
* @param chars the characters to encrypt
* @return character array representing the encrypted data
*/
char[] encrypt(char[] chars);
public char[] encrypt(char[] chars) {
SecretKey key = this.encryptionKey;
if (key == null) {
logger.warn("encrypt called without a key, returning plain text. run syskeygen and copy same key to all nodes to enable " +
"encryption");
return chars;
}
byte[] charBytes = CharArrays.toUtf8Bytes(chars);
String base64 = Base64.getEncoder().encodeToString(encryptInternal(charBytes, key));
return ENCRYPTED_TEXT_PREFIX.concat(base64).toCharArray();
}
/**
* Decrypts the provided char array and returns the plain-text chars
* @param chars the data to decrypt
* @return plaintext chars
*/
char[] decrypt(char[] chars);
public char[] decrypt(char[] chars) {
if (encryptionKey == null) {
return chars;
}
if (!isEncrypted(chars)) {
// Not encrypted
return chars;
}
String encrypted = new String(chars, ENCRYPTED_TEXT_PREFIX.length(), chars.length - ENCRYPTED_TEXT_PREFIX.length());
byte[] bytes;
try {
bytes = Base64.getDecoder().decode(encrypted);
} catch (IllegalArgumentException e) {
throw new ElasticsearchException("unable to decode encrypted data", e);
}
byte[] decrypted = decryptInternal(bytes, encryptionKey);
return CharArrays.utf8BytesToChars(decrypted);
}
/**
* Checks whether the given chars are encrypted
* @param chars the chars to check if they are encrypted
* @return true is data is encrypted
*/
boolean isEncrypted(char[] chars);
public boolean isEncrypted(char[] chars) {
return CharArrays.charsBeginsWith(ENCRYPTED_TEXT_PREFIX, chars);
}
/**
* Flag for callers to determine if values will actually be encrypted or returned plaintext
* @return true if values will be encrypted
*/
boolean isEncryptionEnabled();
public boolean isEncryptionEnabled() {
return this.encryptionKey != null;
}
private byte[] encryptInternal(byte[] bytes, SecretKey key) {
byte[] iv = new byte[ivLength];
secureRandom.nextBytes(iv);
Cipher cipher = cipher(Cipher.ENCRYPT_MODE, encryptionAlgorithm, key, iv);
try {
byte[] encrypted = cipher.doFinal(bytes);
byte[] output = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, output, 0, iv.length);
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
return output;
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new ElasticsearchException("error encrypting data", e);
}
}
private byte[] decryptInternal(byte[] bytes, SecretKey key) {
if (bytes.length < ivLength) {
logger.error("received data for decryption with size [{}] that is less than IV length [{}]", bytes.length, ivLength);
throw new IllegalArgumentException("invalid data to decrypt");
}
byte[] iv = new byte[ivLength];
System.arraycopy(bytes, 0, iv, 0, ivLength);
byte[] data = new byte[bytes.length - ivLength];
System.arraycopy(bytes, ivLength, data, 0, bytes.length - ivLength);
Cipher cipher = cipher(Cipher.DECRYPT_MODE, encryptionAlgorithm, key, iv);
try {
return cipher.doFinal(data);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new IllegalStateException("error decrypting data", e);
}
}
static Mac createMac(SecretKey key) {
try {
Mac mac = HmacSHA1Provider.hmacSHA1();
mac.init(key);
return mac;
} catch (Exception e) {
throw new ElasticsearchException("could not initialize mac", e);
}
}
private static String signInternal(String text, SecretKey key) throws IOException {
Mac mac = createMac(key);
byte[] sig = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().encodeToString(sig);
}
static Cipher cipher(int mode, String encryptionAlgorithm, SecretKey key, byte[] initializationVector) {
try {
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
cipher.init(mode, key, new IvParameterSpec(initializationVector));
return cipher;
} catch (Exception e) {
throw new ElasticsearchException("error creating cipher", e);
}
}
static SecretKey encryptionKey(SecretKey systemKey, int keyLength, String algorithm) throws NoSuchAlgorithmException {
if (systemKey == null) {
return null;
}
byte[] bytes = systemKey.getEncoded();
if ((bytes.length * 8) < keyLength) {
throw new IllegalArgumentException("at least " + keyLength + " bits should be provided as key data");
}
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] digest = messageDigest.digest(bytes);
assert digest.length == (256 / 8);
if ((digest.length * 8) < keyLength) {
throw new IllegalArgumentException("requested key length is too large");
}
byte[] truncatedDigest = Arrays.copyOfRange(digest, 0, (keyLength / 8));
return new SecretKeySpec(truncatedDigest, algorithm);
}
/**
* Provider class for the HmacSHA1 {@link Mac} that provides an optimization by using a thread local instead of calling
* Mac#getInstance and obtaining a lock (in the internals)
*/
private static class HmacSHA1Provider {
private static final ThreadLocal<Mac> MAC = ThreadLocal.withInitial(() -> {
try {
return Mac.getInstance(HMAC_ALGO);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("could not create Mac instance with algorithm [" + HMAC_ALGO + "]", e);
}
});
private static Mac hmacSHA1() {
Mac instance = MAC.get();
instance.reset();
return instance;
}
}
/**
* Simplified implementation of HKDF using the HmacSHA1 algortihm.
*
* @see <a href=https://tools.ietf.org/html/rfc5869>RFC 5869</a>
*/
private static class HmacSHA1HKDF {
private static final int HMAC_SHA1_BYTE_LENGTH = 20;
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
/**
* This method performs the <code>extract</code> and <code>expand</code> steps of HKDF in one call with the given
* data. The output of the extract step is used as the input to the expand step
*
* @param salt optional salt value (a non-secret random value); if not provided, it is set to a string of HashLen zeros.
* @param ikm the input keying material
* @param info optional context and application specific information; if not provided a zero length byte[] is used
* @param outputLength length of output keying material in octets (&lt;= 255*HashLen)
* @return the output keying material
*/
static byte[] extractAndExpand(@Nullable SecretKey salt, byte[] ikm, @Nullable byte[] info, int outputLength) {
// arg checking
Objects.requireNonNull(ikm, "the input keying material must not be null");
if (outputLength < 1) {
throw new IllegalArgumentException("output length must be positive int >= 1");
}
if (outputLength > 255 * HMAC_SHA1_BYTE_LENGTH) {
throw new IllegalArgumentException("output length must be <= 255*" + HMAC_SHA1_BYTE_LENGTH);
}
if (salt == null) {
salt = new SecretKeySpec(new byte[HMAC_SHA1_BYTE_LENGTH], HMAC_SHA1_ALGORITHM);
}
if (info == null) {
info = new byte[0];
}
// extract
Mac mac = createMac(salt);
byte[] keyBytes = mac.doFinal(ikm);
final SecretKey pseudoRandomKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM);
/*
* The output OKM is calculated as follows:
* N = ceil(L/HashLen)
* T = T(1) | T(2) | T(3) | ... | T(N)
* OKM = first L octets of T
*
* where:
* T(0) = empty string (zero length)
* T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
* T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
* T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
* ...
*
* (where the constant concatenated to the end of each T(n) is a single octet.)
*/
int n = (outputLength % HMAC_SHA1_BYTE_LENGTH == 0) ?
outputLength / HMAC_SHA1_BYTE_LENGTH :
(outputLength / HMAC_SHA1_BYTE_LENGTH) + 1;
byte[] hashRound = new byte[0];
ByteBuffer generatedBytes = ByteBuffer.allocate(Math.multiplyExact(n, HMAC_SHA1_BYTE_LENGTH));
try {
// initiliaze the mac with the new key
mac.init(pseudoRandomKey);
} catch (InvalidKeyException e) {
throw new ElasticsearchException("failed to initialize the mac", e);
}
for (int roundNum = 1; roundNum <= n; roundNum++) {
mac.reset();
mac.update(hashRound);
mac.update(info);
mac.update((byte) roundNum);
hashRound = mac.doFinal();
generatedBytes.put(hashRound);
}
byte[] result = new byte[outputLength];
generatedBytes.rewind();
generatedBytes.get(result, 0, outputLength);
return result;
}
}
public static void addSettings(List<Setting<?>> settings) {
settings.add(FILE_SETTING);
settings.add(ENCRYPTION_KEY_LENGTH_SETTING);
settings.add(ENCRYPTION_KEY_ALGO_SETTING);
settings.add(ENCRYPTION_ALGO_SETTING);
}
}

View File

@ -1,477 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.security.crypto;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.XPackPlugin;
import org.elasticsearch.xpack.security.authc.support.CharArrays;
import static org.elasticsearch.xpack.security.Security.setting;
import static org.elasticsearch.xpack.security.authc.support.SecuredString.constantTimeEquals;
public class InternalCryptoService extends AbstractComponent implements CryptoService {
public static final String KEY_ALGO = "HmacSHA512";
public static final int KEY_SIZE = 1024;
static final String FILE_NAME = "system_key";
static final String HMAC_ALGO = "HmacSHA1";
static final String DEFAULT_ENCRYPTION_ALGORITHM = "AES/CTR/NoPadding";
static final String DEFAULT_KEY_ALGORITH = "AES";
static final String ENCRYPTED_TEXT_PREFIX = "::es_encrypted::";
static final byte[] ENCRYPTED_BYTE_PREFIX = ENCRYPTED_TEXT_PREFIX.getBytes(StandardCharsets.UTF_8);
static final int DEFAULT_KEY_LENGTH = 128;
static final int RANDOM_KEY_SIZE = 128;
private static final Pattern SIG_PATTERN = Pattern.compile("^\\$\\$[0-9]+\\$\\$[^\\$]*\\$\\$.+");
private static final byte[] HKDF_APP_INFO = "es-security-crypto-service".getBytes(StandardCharsets.UTF_8);
public static final Setting<String> FILE_SETTING = Setting.simpleString(setting("system_key.file"), Property.NodeScope);
public static final Setting<String> ENCRYPTION_ALGO_SETTING =
new Setting<>(setting("encryption.algorithm"), s -> DEFAULT_ENCRYPTION_ALGORITHM, s -> s, Property.NodeScope);
public static final Setting<Integer> ENCRYPTION_KEY_LENGTH_SETTING =
Setting.intSetting(setting("encryption_key.length"), DEFAULT_KEY_LENGTH, Property.NodeScope);
public static final Setting<String> ENCRYPTION_KEY_ALGO_SETTING =
new Setting<>(setting("encryption_key.algorithm"), DEFAULT_KEY_ALGORITH, s -> s, Property.NodeScope);
private final SecureRandom secureRandom = new SecureRandom();
private final String encryptionAlgorithm;
private final String keyAlgorithm;
private final int keyLength;
private final int ivLength;
private final Path keyFile;
private final SecretKey randomKey;
private final String randomKeyBase64;
private final SecretKey encryptionKey;
private final SecretKey systemKey;
private final SecretKey signingKey;
@Inject
public InternalCryptoService(Settings settings, Environment env) throws Exception {
super(settings);
this.encryptionAlgorithm = ENCRYPTION_ALGO_SETTING.get(settings);
this.keyLength = ENCRYPTION_KEY_LENGTH_SETTING.get(settings);
this.ivLength = keyLength / 8;
this.keyAlgorithm = ENCRYPTION_KEY_ALGO_SETTING.get(settings);
if (keyLength % 8 != 0) {
throw new IllegalArgumentException("invalid key length [" + keyLength + "]. value must be a multiple of 8");
}
keyFile = resolveSystemKey(settings, env);
systemKey = readSystemKey(keyFile);
randomKey = generateSecretKey(RANDOM_KEY_SIZE);
randomKeyBase64 = Base64.getUrlEncoder().encodeToString(randomKey.getEncoded());
signingKey = createSigningKey(systemKey, randomKey);
try {
encryptionKey = encryptionKey(systemKey, keyLength, keyAlgorithm);
} catch (NoSuchAlgorithmException nsae) {
throw new ElasticsearchException("failed to start crypto service. could not load encryption key", nsae);
}
logger.info("system key [{}] has been loaded", keyFile.toAbsolutePath());
}
public static byte[] generateKey() {
return generateSecretKey(KEY_SIZE).getEncoded();
}
static SecretKey generateSecretKey(int keyLength) {
try {
KeyGenerator generator = KeyGenerator.getInstance(KEY_ALGO);
generator.init(keyLength);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new ElasticsearchException("failed to generate key", e);
}
}
public static Path resolveSystemKey(Settings settings, Environment env) {
String location = FILE_SETTING.get(settings);
if (location.isEmpty()) {
return XPackPlugin.resolveConfigFile(env, FILE_NAME);
}
return XPackPlugin.resolveConfigFile(env, location);
}
static SecretKey createSigningKey(@Nullable SecretKey systemKey, SecretKey randomKey) {
assert randomKey != null;
if (systemKey != null) {
return systemKey;
} else {
// the random key is only 128 bits so we use HKDF to expand to 1024 bits with some application specific data mixed in
byte[] keyBytes = HmacSHA1HKDF.extractAndExpand(null, randomKey.getEncoded(), HKDF_APP_INFO, (KEY_SIZE / 8));
assert keyBytes.length * 8 == KEY_SIZE;
return new SecretKeySpec(keyBytes, KEY_ALGO);
}
}
private static SecretKey readSystemKey(Path file) {
if (!Files.exists(file)) {
return null;
}
try {
byte[] bytes = Files.readAllBytes(file);
return new SecretKeySpec(bytes, KEY_ALGO);
} catch (IOException e) {
throw new ElasticsearchException("could not read secret key", e);
}
}
@Override
public String sign(String text) throws IOException {
String sigStr = signInternal(text, signingKey);
return "$$" + sigStr.length() + "$$" + (systemKey == signingKey ? "" : randomKeyBase64) + "$$" + sigStr + text;
}
@Override
public String unsignAndVerify(String signedText) {
if (!signedText.startsWith("$$") || signedText.length() < 2) {
throw new IllegalArgumentException("tampered signed text");
}
// $$34$$randomKeyBase64$$sigtext
String[] pieces = signedText.split("\\$\\$");
if (pieces.length != 4 || !pieces[0].equals("")) {
logger.debug("received signed text [{}] with [{}] parts", signedText, pieces.length);
throw new IllegalArgumentException("tampered signed text");
}
String text;
String base64RandomKey;
String receivedSignature;
try {
int length = Integer.parseInt(pieces[1]);
base64RandomKey = pieces[2];
receivedSignature = pieces[3].substring(0, length);
text = pieces[3].substring(length);
} catch (Exception e) {
logger.error("error occurred while parsing signed text", e);
throw new IllegalArgumentException("tampered signed text");
}
SecretKey signingKey;
// no random key, so we must have a system key
if (base64RandomKey.isEmpty()) {
if (systemKey == null) {
logger.debug("received signed text without random key information and no system key is present");
throw new IllegalArgumentException("tampered signed text");
}
signingKey = systemKey;
} else if (systemKey != null) {
// we have a system key and there is some random key data, this is an error
logger.debug("received signed text with random key information but a system key is present");
throw new IllegalArgumentException("tampered signed text");
} else {
byte[] randomKeyBytes;
try {
randomKeyBytes = Base64.getUrlDecoder().decode(base64RandomKey);
} catch (IllegalArgumentException e) {
logger.error("error occurred while decoding key data", e);
throw new IllegalStateException("error while verifying the signed text");
}
if (randomKeyBytes.length * 8 != RANDOM_KEY_SIZE) {
logger.debug("incorrect random key data length. received [{}] bytes", randomKeyBytes.length);
throw new IllegalArgumentException("tampered signed text");
}
SecretKey randomKey = new SecretKeySpec(randomKeyBytes, KEY_ALGO);
signingKey = createSigningKey(systemKey, randomKey);
}
try {
String sig = signInternal(text, signingKey);
if (constantTimeEquals(sig, receivedSignature)) {
return text;
}
} catch (Exception e) {
logger.error("error occurred while verifying signed text", e);
throw new IllegalStateException("error while verifying the signed text");
}
throw new IllegalArgumentException("tampered signed text");
}
@Override
public boolean isSigned(String text) {
return SIG_PATTERN.matcher(text).matches();
}
@Override
public char[] encrypt(char[] chars) {
SecretKey key = this.encryptionKey;
if (key == null) {
logger.warn("encrypt called without a key, returning plain text. run syskeygen and copy same key to all nodes to enable " +
"encryption");
return chars;
}
byte[] charBytes = CharArrays.toUtf8Bytes(chars);
String base64 = Base64.getEncoder().encodeToString(encryptInternal(charBytes, key));
return ENCRYPTED_TEXT_PREFIX.concat(base64).toCharArray();
}
@Override
public char[] decrypt(char[] chars) {
if (encryptionKey == null) {
return chars;
}
if (!isEncrypted(chars)) {
// Not encrypted
return chars;
}
String encrypted = new String(chars, ENCRYPTED_TEXT_PREFIX.length(), chars.length - ENCRYPTED_TEXT_PREFIX.length());
byte[] bytes;
try {
bytes = Base64.getDecoder().decode(encrypted);
} catch (IllegalArgumentException e) {
throw new ElasticsearchException("unable to decode encrypted data", e);
}
byte[] decrypted = decryptInternal(bytes, encryptionKey);
return CharArrays.utf8BytesToChars(decrypted);
}
@Override
public boolean isEncrypted(char[] chars) {
return CharArrays.charsBeginsWith(ENCRYPTED_TEXT_PREFIX, chars);
}
@Override
public boolean isEncryptionEnabled() {
return this.encryptionKey != null;
}
private byte[] encryptInternal(byte[] bytes, SecretKey key) {
byte[] iv = new byte[ivLength];
secureRandom.nextBytes(iv);
Cipher cipher = cipher(Cipher.ENCRYPT_MODE, encryptionAlgorithm, key, iv);
try {
byte[] encrypted = cipher.doFinal(bytes);
byte[] output = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, output, 0, iv.length);
System.arraycopy(encrypted, 0, output, iv.length, encrypted.length);
return output;
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new ElasticsearchException("error encrypting data", e);
}
}
private byte[] decryptInternal(byte[] bytes, SecretKey key) {
if (bytes.length < ivLength) {
logger.error("received data for decryption with size [{}] that is less than IV length [{}]", bytes.length, ivLength);
throw new IllegalArgumentException("invalid data to decrypt");
}
byte[] iv = new byte[ivLength];
System.arraycopy(bytes, 0, iv, 0, ivLength);
byte[] data = new byte[bytes.length - ivLength];
System.arraycopy(bytes, ivLength, data, 0, bytes.length - ivLength);
Cipher cipher = cipher(Cipher.DECRYPT_MODE, encryptionAlgorithm, key, iv);
try {
return cipher.doFinal(data);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new IllegalStateException("error decrypting data", e);
}
}
static Mac createMac(SecretKey key) {
try {
Mac mac = HmacSHA1Provider.hmacSHA1();
mac.init(key);
return mac;
} catch (Exception e) {
throw new ElasticsearchException("could not initialize mac", e);
}
}
private static String signInternal(String text, SecretKey key) throws IOException {
Mac mac = createMac(key);
byte[] sig = mac.doFinal(text.getBytes(StandardCharsets.UTF_8));
return Base64.getUrlEncoder().encodeToString(sig);
}
static Cipher cipher(int mode, String encryptionAlgorithm, SecretKey key, byte[] initializationVector) {
try {
Cipher cipher = Cipher.getInstance(encryptionAlgorithm);
cipher.init(mode, key, new IvParameterSpec(initializationVector));
return cipher;
} catch (Exception e) {
throw new ElasticsearchException("error creating cipher", e);
}
}
static SecretKey encryptionKey(SecretKey systemKey, int keyLength, String algorithm) throws NoSuchAlgorithmException {
if (systemKey == null) {
return null;
}
byte[] bytes = systemKey.getEncoded();
if ((bytes.length * 8) < keyLength) {
throw new IllegalArgumentException("at least " + keyLength + " bits should be provided as key data");
}
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] digest = messageDigest.digest(bytes);
assert digest.length == (256 / 8);
if ((digest.length * 8) < keyLength) {
throw new IllegalArgumentException("requested key length is too large");
}
byte[] truncatedDigest = Arrays.copyOfRange(digest, 0, (keyLength / 8));
return new SecretKeySpec(truncatedDigest, algorithm);
}
/**
* Provider class for the HmacSHA1 {@link Mac} that provides an optimization by using a thread local instead of calling
* Mac#getInstance and obtaining a lock (in the internals)
*/
private static class HmacSHA1Provider {
private static final ThreadLocal<Mac> MAC = ThreadLocal.withInitial(() -> {
try {
return Mac.getInstance(HMAC_ALGO);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("could not create Mac instance with algorithm [" + HMAC_ALGO + "]", e);
}
});
private static Mac hmacSHA1() {
Mac instance = MAC.get();
instance.reset();
return instance;
}
}
/**
* Simplified implementation of HKDF using the HmacSHA1 algortihm.
*
* @see <a href=https://tools.ietf.org/html/rfc5869>RFC 5869</a>
*/
private static class HmacSHA1HKDF {
private static final int HMAC_SHA1_BYTE_LENGTH = 20;
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
/**
* This method performs the <code>extract</code> and <code>expand</code> steps of HKDF in one call with the given
* data. The output of the extract step is used as the input to the expand step
*
* @param salt optional salt value (a non-secret random value); if not provided, it is set to a string of HashLen zeros.
* @param ikm the input keying material
* @param info optional context and application specific information; if not provided a zero length byte[] is used
* @param outputLength length of output keying material in octets (&lt;= 255*HashLen)
* @return the output keying material
*/
static byte[] extractAndExpand(@Nullable SecretKey salt, byte[] ikm, @Nullable byte[] info, int outputLength) {
// arg checking
Objects.requireNonNull(ikm, "the input keying material must not be null");
if (outputLength < 1) {
throw new IllegalArgumentException("output length must be positive int >= 1");
}
if (outputLength > 255 * HMAC_SHA1_BYTE_LENGTH) {
throw new IllegalArgumentException("output length must be <= 255*" + HMAC_SHA1_BYTE_LENGTH);
}
if (salt == null) {
salt = new SecretKeySpec(new byte[HMAC_SHA1_BYTE_LENGTH], HMAC_SHA1_ALGORITHM);
}
if (info == null) {
info = new byte[0];
}
// extract
Mac mac = createMac(salt);
byte[] keyBytes = mac.doFinal(ikm);
final SecretKey pseudoRandomKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM);
/*
* The output OKM is calculated as follows:
* N = ceil(L/HashLen)
* T = T(1) | T(2) | T(3) | ... | T(N)
* OKM = first L octets of T
*
* where:
* T(0) = empty string (zero length)
* T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
* T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
* T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
* ...
*
* (where the constant concatenated to the end of each T(n) is a single octet.)
*/
int n = (outputLength % HMAC_SHA1_BYTE_LENGTH == 0) ?
outputLength / HMAC_SHA1_BYTE_LENGTH :
(outputLength / HMAC_SHA1_BYTE_LENGTH) + 1;
byte[] hashRound = new byte[0];
ByteBuffer generatedBytes = ByteBuffer.allocate(Math.multiplyExact(n, HMAC_SHA1_BYTE_LENGTH));
try {
// initiliaze the mac with the new key
mac.init(pseudoRandomKey);
} catch (InvalidKeyException e) {
throw new ElasticsearchException("failed to initialize the mac", e);
}
for (int roundNum = 1; roundNum <= n; roundNum++) {
mac.reset();
mac.update(hashRound);
mac.update(info);
mac.update((byte) roundNum);
hashRound = mac.doFinal();
generatedBytes.put(hashRound);
}
byte[] result = new byte[outputLength];
generatedBytes.rewind();
generatedBytes.get(result, 0, outputLength);
return result;
}
}
public static void addSettings(List<Setting<?>> settings) {
settings.add(FILE_SETTING);
settings.add(ENCRYPTION_KEY_LENGTH_SETTING);
settings.add(ENCRYPTION_KEY_ALGO_SETTING);
settings.add(ENCRYPTION_ALGO_SETTING);
}
}

View File

@ -17,7 +17,7 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import java.nio.file.Files;
import java.nio.file.Path;
@ -66,12 +66,12 @@ public class SystemKeyTool extends SettingCommand {
}
keyPath = parsePath(args.get(0));
} else {
keyPath = InternalCryptoService.resolveSystemKey(env.settings(), env);
keyPath = CryptoService.resolveSystemKey(env.settings(), env);
}
// write the key
terminal.println(Terminal.Verbosity.VERBOSE, "generating...");
byte[] key = InternalCryptoService.generateKey();
byte[] key = CryptoService.generateKey();
terminal.println(String.format(Locale.ROOT, "Storing generated key in [%s]...", keyPath.toAbsolutePath()));
Files.write(keyPath, key, StandardOpenOption.CREATE_NEW);

View File

@ -42,6 +42,7 @@ import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.authc.support.UsernamePasswordToken;
import org.junit.After;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -265,7 +266,7 @@ public class LicensingTests extends SecurityIntegTestCase {
public static class InternalXPackPlugin extends XPackPlugin {
public InternalXPackPlugin(Settings settings) {
public InternalXPackPlugin(Settings settings) throws IOException {
super(settings);
licensing = new InternalLicensing();
}

View File

@ -11,7 +11,7 @@ import org.elasticsearch.action.index.IndexRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.test.SecurityIntegTestCase;
import java.util.Locale;
@ -106,7 +106,7 @@ public class ScrollIdSigningTests extends SecurityIntegTestCase {
}
private void assertSigned(String scrollId) {
CryptoService cryptoService = internalCluster().getDataNodeInstance(InternalCryptoService.class);
CryptoService cryptoService = internalCluster().getDataNodeInstance(CryptoService.class);
String message = String.format(Locale.ROOT, "Expected scrollId [%s] to be signed, but was not", scrollId);
assertThat(message, cryptoService.isSigned(scrollId), is(true));
}

View File

@ -21,7 +21,7 @@ import org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.authc.support.SecuredString;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.test.SecurityTestUtils;
import org.elasticsearch.xpack.security.transport.netty.SecurityNettyHttpServerTransport;
import org.elasticsearch.xpack.security.transport.netty.SecurityNettyTransport;
@ -131,7 +131,7 @@ public class SecuritySettingsSource extends ClusterDiscoveryConfiguration.Unicas
.put(LoggingAuditTrail.HOST_ADDRESS_SETTING.getKey(), randomBoolean())
.put(LoggingAuditTrail.HOST_NAME_SETTING.getKey(), randomBoolean())
.put(LoggingAuditTrail.NODE_NAME_SETTING.getKey(), randomBoolean())
.put(InternalCryptoService.FILE_SETTING.getKey(), writeFile(folder, "system_key", systemKey))
.put(CryptoService.FILE_SETTING.getKey(), writeFile(folder, "system_key", systemKey))
.put("xpack.security.authc.realms.file.type", FileRealm.TYPE)
.put("xpack.security.authc.realms.file.order", 0)
.put("xpack.security.authc.realms.file.files.users", writeFile(folder, "users", configUsers()))
@ -206,7 +206,7 @@ public class SecuritySettingsSource extends ClusterDiscoveryConfiguration.Unicas
private static byte[] generateKey() {
try {
return InternalCryptoService.generateKey();
return CryptoService.generateKey();
} catch (Exception e) {
throw new ElasticsearchException("exception while generating the system key", e);
}

View File

@ -15,6 +15,7 @@ import org.elasticsearch.xpack.security.ssl.SSLConfiguration;
import org.elasticsearch.xpack.XPackPlugin;
import org.hamcrest.Matcher;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -28,7 +29,7 @@ public class SettingsFilterTests extends ESTestCase {
private Settings.Builder configuredSettingsBuilder = Settings.builder();
private Map<String, Matcher> settingsMatcherMap = new HashMap<>();
public void testFiltering() {
public void testFiltering() throws IOException {
configureUnfilteredSetting("xpack.security.authc.realms.file.type", "file");
// ldap realm filtering

View File

@ -5,8 +5,11 @@
*/
package org.elasticsearch.xpack.security;
import java.io.IOException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.xpack.security.audit.AuditTrailModule;
import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail;
import org.elasticsearch.test.ESTestCase;
@ -25,13 +28,11 @@ public class SecuritySettingsTests extends ESTestCase {
private static final String TRIBE_T1_SECURITY_ENABLED = "tribe.t1." + Security.enabledSetting();
private static final String TRIBE_T2_SECURITY_ENABLED = "tribe.t2." + Security.enabledSetting();
public void testSecurityIsMandatoryOnTribes() {
public void testSecurityIsMandatoryOnTribes() throws IOException {
Settings settings = Settings.builder().put("tribe.t1.cluster.name", "non_existing")
.put("tribe.t2.cluster.name", "non_existing").build();
Security security = new Security(settings);
Settings additionalSettings = security.additionalSettings();
Settings additionalSettings = Security.additionalSettings(settings);
assertThat(additionalSettings.getAsArray("tribe.t1.plugin.mandatory", null), arrayContaining(XPackPlugin.NAME));
@ -42,11 +43,9 @@ public class SecuritySettingsTests extends ESTestCase {
Settings settings = Settings.builder().put("tribe.t1.cluster.name", "non_existing")
.putArray("tribe.t1.plugin.mandatory", "test_plugin").build();
Security security = new Security(settings);
//simulate what PluginsService#updatedSettings does to make sure we don't override existing mandatory plugins
try {
Settings.builder().put(settings).put(security.additionalSettings()).build();
Settings.builder().put(settings).put(Security.additionalSettings(settings)).build();
fail("security cannot change the value of a setting that is already defined, so a exception should be thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), containsString(XPackPlugin.NAME));
@ -58,10 +57,8 @@ public class SecuritySettingsTests extends ESTestCase {
Settings settings = Settings.builder().put("tribe.t1.cluster.name", "non_existing")
.putArray("tribe.t1.plugin.mandatory", "test_plugin", XPackPlugin.NAME).build();
Security security = new Security(settings);
//simulate what PluginsService#updatedSettings does to make sure we don't override existing mandatory plugins
Settings finalSettings = Settings.builder().put(settings).put(security.additionalSettings()).build();
Settings finalSettings = Settings.builder().put(settings).put(Security.additionalSettings(settings)).build();
String[] finalMandatoryPlugins = finalSettings.getAsArray("tribe.t1.plugin.mandatory", null);
assertThat(finalMandatoryPlugins, notNullValue());
@ -74,9 +71,7 @@ public class SecuritySettingsTests extends ESTestCase {
Settings settings = Settings.builder().put("tribe.t1.cluster.name", "non_existing")
.put("tribe.t2.cluster.name", "non_existing").build();
Security security = new Security(settings);
Settings additionalSettings = security.additionalSettings();
Settings additionalSettings = Security.additionalSettings(settings);
assertThat(additionalSettings.getAsBoolean(TRIBE_T1_SECURITY_ENABLED, null), equalTo(true));
assertThat(additionalSettings.getAsBoolean(TRIBE_T2_SECURITY_ENABLED, null), equalTo(true));
@ -87,10 +82,8 @@ public class SecuritySettingsTests extends ESTestCase {
.put(TRIBE_T1_SECURITY_ENABLED, false)
.put("tribe.t2.cluster.name", "non_existing").build();
Security security = new Security(settings);
try {
security.additionalSettings();
Security.additionalSettings(settings);
fail("security cannot change the value of a setting that is already defined, so a exception should be thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), containsString(TRIBE_T1_SECURITY_ENABLED));
@ -103,10 +96,8 @@ public class SecuritySettingsTests extends ESTestCase {
.put("tribe.t2.cluster.name", "non_existing")
.putArray("tribe.t1.plugin.mandatory", "test_plugin", XPackPlugin.NAME).build();
Security security = new Security(settings);
try {
security.additionalSettings();
Security.additionalSettings(settings);
fail("security cannot change the value of a setting that is already defined, so a exception should be thrown");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), containsString(TRIBE_T1_SECURITY_ENABLED));
@ -122,8 +113,7 @@ public class SecuritySettingsTests extends ESTestCase {
.putArray("xpack.security.something.else.here", new String[] { "foo", "bar" })
.build();
Security security = new Security(settings);
Settings additionalSettings = security.additionalSettings();
Settings additionalSettings = Security.additionalSettings(settings);
assertThat(additionalSettings.get("xpack.security.foo"), nullValue());
assertThat(additionalSettings.get("xpack.security.bar"), nullValue());

View File

@ -39,7 +39,7 @@ import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.audit.index.IndexAuditTrail.Message;
import org.elasticsearch.xpack.security.authc.AuthenticationToken;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.transport.filter.IPFilter;
import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule;
import org.elasticsearch.xpack.security.transport.netty.SecurityNettyTransport;
@ -99,7 +99,7 @@ public class IndexAuditTrailTests extends SecurityIntegTestCase {
@BeforeClass
public static void configureBeforeClass() {
remoteIndexing = randomBoolean();
systemKey = InternalCryptoService.generateKey();
systemKey = CryptoService.generateKey();
if (remoteIndexing == false) {
remoteSettings = Settings.EMPTY;
}

View File

@ -22,7 +22,7 @@ import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class InternalCryptoServiceTests extends ESTestCase {
public class CryptoServiceTests extends ESTestCase {
private Settings settings;
private Environment env;
private Path keyFile;
@ -30,9 +30,9 @@ public class InternalCryptoServiceTests extends ESTestCase {
@Before
public void init() throws Exception {
keyFile = createTempDir().resolve("system_key");
Files.write(keyFile, InternalCryptoService.generateKey());
Files.write(keyFile, CryptoService.generateKey());
settings = Settings.builder()
.put(InternalCryptoService.FILE_SETTING.getKey(), keyFile.toAbsolutePath())
.put(CryptoService.FILE_SETTING.getKey(), keyFile.toAbsolutePath())
.put("resource.reload.interval.high", "2s")
.put("path.home", createTempDir())
.build();
@ -42,14 +42,14 @@ public class InternalCryptoServiceTests extends ESTestCase {
public void testSigned() throws Exception {
// randomize whether to use a system key or not
Settings settings = randomBoolean() ? this.settings : Settings.EMPTY;
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
String text = randomAsciiOfLength(10);
String signed = service.sign(text);
assertThat(service.isSigned(signed), is(true));
}
public void testSignAndUnsign() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
String text = randomAsciiOfLength(10);
String signed = service.sign(text);
assertThat(text.equals(signed), is(false));
@ -58,7 +58,7 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testSignAndUnsignNoKeyFile() throws Exception {
InternalCryptoService service = new InternalCryptoService(Settings.EMPTY, env);
CryptoService service = new CryptoService(Settings.EMPTY, env);
final String text = randomAsciiOfLength(10);
String signed = service.sign(text);
// we always have some sort of key to sign with
@ -68,7 +68,7 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testTamperedSignature() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
String text = randomAsciiOfLength(10);
String signed = service.sign(text);
int i = signed.indexOf("$$", 2);
@ -85,7 +85,7 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testTamperedSignatureOneChar() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
String text = randomAsciiOfLength(10);
String signed = service.sign(text);
int i = signed.indexOf("$$", 2);
@ -104,7 +104,7 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testTamperedSignatureLength() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
String text = randomAsciiOfLength(10);
String signed = service.sign(text);
int i = signed.indexOf("$$", 2);
@ -131,7 +131,7 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testEncryptionAndDecryptionChars() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
assertThat(service.isEncryptionEnabled(), is(true));
final char[] chars = randomAsciiOfLengthBetween(0, 1000).toCharArray();
final char[] encrypted = service.encrypt(chars);
@ -143,7 +143,7 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testEncryptionAndDecryptionCharsWithoutKey() throws Exception {
InternalCryptoService service = new InternalCryptoService(Settings.EMPTY, env);
CryptoService service = new CryptoService(Settings.EMPTY, env);
assertThat(service.isEncryptionEnabled(), is(false));
final char[] chars = randomAsciiOfLengthBetween(0, 1000).toCharArray();
final char[] encryptedChars = service.encrypt(chars);
@ -153,34 +153,34 @@ public class InternalCryptoServiceTests extends ESTestCase {
}
public void testEncryptionEnabledWithKey() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
assertThat(service.isEncryptionEnabled(), is(true));
}
public void testEncryptionEnabledWithoutKey() throws Exception {
InternalCryptoService service = new InternalCryptoService(Settings.EMPTY, env);
CryptoService service = new CryptoService(Settings.EMPTY, env);
assertThat(service.isEncryptionEnabled(), is(false));
}
public void testEncryptedChar() throws Exception {
InternalCryptoService service = new InternalCryptoService(settings, env);
CryptoService service = new CryptoService(settings, env);
assertThat(service.isEncryptionEnabled(), is(true));
assertThat(service.isEncrypted((char[]) null), is(false));
assertThat(service.isEncrypted(new char[0]), is(false));
assertThat(service.isEncrypted(new char[InternalCryptoService.ENCRYPTED_TEXT_PREFIX.length()]), is(false));
assertThat(service.isEncrypted(InternalCryptoService.ENCRYPTED_TEXT_PREFIX.toCharArray()), is(true));
assertThat(service.isEncrypted(new char[CryptoService.ENCRYPTED_TEXT_PREFIX.length()]), is(false));
assertThat(service.isEncrypted(CryptoService.ENCRYPTED_TEXT_PREFIX.toCharArray()), is(true));
assertThat(service.isEncrypted(randomAsciiOfLengthBetween(0, 100).toCharArray()), is(false));
assertThat(service.isEncrypted(service.encrypt(randomAsciiOfLength(10).toCharArray())), is(true));
}
public void testSigningKeyCanBeRecomputedConsistently() {
final SecretKey systemKey = new SecretKeySpec(InternalCryptoService.generateKey(), InternalCryptoService.KEY_ALGO);
final SecretKey randomKey = InternalCryptoService.generateSecretKey(InternalCryptoService.RANDOM_KEY_SIZE);
final SecretKey systemKey = new SecretKeySpec(CryptoService.generateKey(), CryptoService.KEY_ALGO);
final SecretKey randomKey = CryptoService.generateSecretKey(CryptoService.RANDOM_KEY_SIZE);
int iterations = randomInt(100);
final SecretKey signingKey = InternalCryptoService.createSigningKey(systemKey, randomKey);
final SecretKey signingKey = CryptoService.createSigningKey(systemKey, randomKey);
for (int i = 0; i < iterations; i++) {
SecretKey regenerated = InternalCryptoService.createSigningKey(systemKey, randomKey);
SecretKey regenerated = CryptoService.createSigningKey(systemKey, randomKey);
assertThat(regenerated, equalTo(signingKey));
}
}

View File

@ -11,7 +11,7 @@ import org.apache.lucene.util.IOUtils;
import org.elasticsearch.cli.Command;
import org.elasticsearch.cli.CommandTestCase;
import org.elasticsearch.common.io.PathUtilsForTesting;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.junit.After;
import java.nio.file.FileSystem;
@ -52,7 +52,7 @@ public class SystemKeyToolTests extends CommandTestCase {
execute("-Epath.home=" + homeDir, path.toString());
byte[] bytes = Files.readAllBytes(path);
// TODO: maybe we should actually check the key is...i dunno...valid?
assertEquals(InternalCryptoService.KEY_SIZE / 8, bytes.length);
assertEquals(CryptoService.KEY_SIZE / 8, bytes.length);
Set<PosixFilePermission> perms = Files.getPosixFilePermissions(path);
assertTrue(perms.toString(), perms.contains(PosixFilePermission.OWNER_READ));
@ -67,7 +67,7 @@ public class SystemKeyToolTests extends CommandTestCase {
Files.createDirectories(path.getParent());
execute("-Epath.home=" + homeDir.toString(), "-Expack.security.system_key.file=" + path.toAbsolutePath().toString());
byte[] bytes = Files.readAllBytes(path);
assertEquals(InternalCryptoService.KEY_SIZE / 8, bytes.length);
assertEquals(CryptoService.KEY_SIZE / 8, bytes.length);
}
public void testGenerateDefaultPath() throws Exception {
@ -76,7 +76,7 @@ public class SystemKeyToolTests extends CommandTestCase {
Files.createDirectories(keyPath.getParent());
execute("-Epath.home=" + homeDir.toString());
byte[] bytes = Files.readAllBytes(keyPath);
assertEquals(InternalCryptoService.KEY_SIZE / 8, bytes.length);
assertEquals(CryptoService.KEY_SIZE / 8, bytes.length);
}
public void testThatSystemKeyMayOnlyBeReadByOwner() throws Exception {

View File

@ -17,7 +17,7 @@ import org.elasticsearch.node.Node;
import org.elasticsearch.xpack.security.authc.file.FileRealm;
import org.elasticsearch.xpack.security.Security;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.xpack.security.transport.netty.SecurityNettyTransport;
import org.elasticsearch.test.SecurityIntegTestCase;
import org.elasticsearch.transport.Transport;
@ -82,7 +82,7 @@ public class ServerTransportFilterIntegrationTests extends SecurityIntegTestCase
public void testThatConnectionToServerTypeConnectionWorks() throws IOException {
Settings dataNodeSettings = internalCluster().getDataNodeInstance(Settings.class);
String systemKeyFile = InternalCryptoService.FILE_SETTING.get(dataNodeSettings);
String systemKeyFile = CryptoService.FILE_SETTING.get(dataNodeSettings);
Transport transport = internalCluster().getDataNodeInstance(Transport.class);
TransportAddress transportAddress = transport.boundAddress().publishAddress();
@ -102,7 +102,7 @@ public class ServerTransportFilterIntegrationTests extends SecurityIntegTestCase
.put("xpack.security.audit.enabled", false)
.put("path.home", createTempDir())
.put(NetworkModule.HTTP_ENABLED.getKey(), false)
.put(InternalCryptoService.FILE_SETTING.getKey(), systemKeyFile)
.put(CryptoService.FILE_SETTING.getKey(), systemKeyFile)
.build();
try (Node node = new MockNode(nodeSettings, Collections.singletonList(XPackPlugin.class))) {
node.start();
@ -112,7 +112,7 @@ public class ServerTransportFilterIntegrationTests extends SecurityIntegTestCase
public void testThatConnectionToClientTypeConnectionIsRejected() throws IOException {
Settings dataNodeSettings = internalCluster().getDataNodeInstance(Settings.class);
String systemKeyFile = InternalCryptoService.FILE_SETTING.get(dataNodeSettings);
String systemKeyFile = CryptoService.FILE_SETTING.get(dataNodeSettings);
Path folder = createFolder(createTempDir(), getClass().getSimpleName() + "-" + randomAsciiOfLength(10));
@ -132,7 +132,7 @@ public class ServerTransportFilterIntegrationTests extends SecurityIntegTestCase
.put(SecurityNettyTransport.SSL_SETTING.getKey(), sslTransportEnabled())
.put("xpack.security.audit.enabled", false)
.put(NetworkModule.HTTP_ENABLED.getKey(), false)
.put(InternalCryptoService.FILE_SETTING.getKey(), systemKeyFile)
.put(CryptoService.FILE_SETTING.getKey(), systemKeyFile)
.put("discovery.initial_state_timeout", "2s")
.put("path.home", createTempDir())
.put(Node.NODE_MASTER_SETTING.getKey(), false)

View File

@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack;
import java.io.IOException;
import java.nio.file.Path;
import java.security.AccessController;
import java.security.PrivilegedAction;
@ -109,18 +110,19 @@ public class XPackPlugin extends Plugin implements ScriptPlugin, ActionPlugin {
protected Graph graph;
protected Notification notification;
public XPackPlugin(Settings settings) {
public XPackPlugin(Settings settings) throws IOException {
this.settings = settings;
this.transportClientMode = transportClientMode(settings);
final Environment env = transportClientMode ? null : new Environment(settings);
this.licensing = new Licensing(settings);
this.security = new Security(settings);
this.security = new Security(settings, env);
this.monitoring = new Monitoring(settings);
this.watcher = new Watcher(settings);
this.graph = new Graph(settings);
this.notification = new Notification(settings);
// Check if the node is a transport client.
if (transportClientMode == false) {
Environment env = new Environment(settings);
this.extensionsService = new XPackExtensionsService(settings, resolveXPackExtensionsFile(env), getExtensions());
} else {
this.extensionsService = null;

View File

@ -10,8 +10,11 @@ import org.elasticsearch.xpack.support.clock.Clock;
import org.elasticsearch.xpack.support.clock.ClockMock;
import org.elasticsearch.xpack.watcher.test.TimeWarpedWatcher;
import java.io.IOException;
public class TimeWarpedXPackPlugin extends XPackPlugin {
public TimeWarpedXPackPlugin(Settings settings) {
public TimeWarpedXPackPlugin(Settings settings) throws IOException {
super(settings);
watcher = new TimeWarpedWatcher(settings);
}

View File

@ -32,7 +32,7 @@ import org.elasticsearch.xpack.security.authc.file.FileRealm;
import org.elasticsearch.xpack.security.authc.support.Hasher;
import org.elasticsearch.xpack.security.authc.support.SecuredString;
import org.elasticsearch.xpack.security.authz.store.FileRolesStore;
import org.elasticsearch.xpack.security.crypto.InternalCryptoService;
import org.elasticsearch.xpack.security.crypto.CryptoService;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
import org.elasticsearch.test.TestCluster;
@ -700,7 +700,7 @@ public abstract class AbstractWatcherIntegrationTestCase extends ESIntegTestCase
.put("xpack.security.authc.realms.esusers.files.users", writeFile(folder, "users", USERS))
.put("xpack.security.authc.realms.esusers.files.users_roles", writeFile(folder, "users_roles", USER_ROLES))
.put(FileRolesStore.ROLES_FILE_SETTING.getKey(), writeFile(folder, "roles.yml", ROLES))
.put(InternalCryptoService.FILE_SETTING.getKey(), writeFile(folder, "system_key.yml", systemKey))
.put(CryptoService.FILE_SETTING.getKey(), writeFile(folder, "system_key.yml", systemKey))
.put("xpack.security.authc.sign_user_header", false)
.put("xpack.security.audit.enabled", auditLogsEnabled)
.build();
@ -711,7 +711,7 @@ public abstract class AbstractWatcherIntegrationTestCase extends ESIntegTestCase
static byte[] generateKey() {
try {
return InternalCryptoService.generateKey();
return CryptoService.generateKey();
} catch (Exception e) {
throw new RuntimeException(e);
}

View File

@ -22,6 +22,7 @@ import org.elasticsearch.xpack.watcher.trigger.ScheduleTriggerEngineMock;
import org.elasticsearch.xpack.watcher.trigger.TriggerModule;
import org.elasticsearch.xpack.XPackPlugin;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -204,7 +205,7 @@ public class WatcherExecutorServiceBenchmark {
public static final class XPackBenchmarkPlugin extends XPackPlugin {
public XPackBenchmarkPlugin(Settings settings) {
public XPackBenchmarkPlugin(Settings settings) throws IOException {
super(settings);
watcher = new BenchmarkWatcher(settings);
}