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:
parent
84a60d2548
commit
7438177313
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 (<= 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 (<= 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue