-
Notifications
You must be signed in to change notification settings - Fork 25k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce a Hashing Processor #31087
Merged
Merged
Changes from 1 commit
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
0e77233
Initial commit for HashProcessor
talevy c6f9ff6
Merge branch 'master' into hash-processor
talevy 80ca1ee
add rest test, and add two params for salt & ignore_missing
talevy 514664e
Merge branch 'master' into hash-processor
talevy 1676aa9
fix key resolution
talevy cad0dc5
make salt required
talevy a0b411c
Merge branch 'master' into hash-processor
talevy 38f9141
Fix merge master test
talevy 8e44e06
Merge branch 'master' into hash-processor
talevy 278f235
remove extra lines
talevy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
.../plugin/security/src/main/java/org/elasticsearch/xpack/security/ingest/HashProcessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
/* | ||
* 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.ingest; | ||
|
||
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.common.Nullable; | ||
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.common.collect.Tuple; | ||
import org.elasticsearch.common.settings.SecureSetting; | ||
import org.elasticsearch.common.settings.SecureString; | ||
import org.elasticsearch.common.settings.Setting; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.ingest.AbstractProcessor; | ||
import org.elasticsearch.ingest.ConfigurationUtils; | ||
import org.elasticsearch.ingest.IngestDocument; | ||
import org.elasticsearch.ingest.Processor; | ||
import org.elasticsearch.xpack.core.security.SecurityField; | ||
import org.elasticsearch.xpack.core.security.authc.support.CharArrays; | ||
import org.elasticsearch.xpack.core.security.authc.support.Hasher; | ||
|
||
import javax.crypto.Mac; | ||
import javax.crypto.SecretKeyFactory; | ||
import javax.crypto.spec.PBEKeySpec; | ||
import javax.crypto.spec.SecretKeySpec; | ||
import java.nio.charset.StandardCharsets; | ||
import java.security.InvalidKeyException; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.spec.InvalidKeySpecException; | ||
import java.util.Arrays; | ||
import java.util.Base64; | ||
import java.util.List; | ||
import java.util.Locale; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.elasticsearch.ingest.ConfigurationUtils.newConfigurationException; | ||
|
||
/** | ||
* A processor that hashes the contents of a field (or fields) using various hashing algorithms | ||
*/ | ||
public final class HashProcessor extends AbstractProcessor { | ||
enum Method { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: maybe move this enum below the factory class? |
||
SHA1("HmacSHA1"), | ||
SHA256("HmacSHA256"), | ||
SHA384("HmacSHA384"), | ||
SHA512("HmacSHA512"); | ||
|
||
private final String algorithm; | ||
|
||
Method(String algorithm) { | ||
this.algorithm = algorithm; | ||
} | ||
|
||
public String getAlgorithm() { | ||
return algorithm; | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return name().toLowerCase(Locale.ROOT); | ||
} | ||
|
||
public String hash(Mac mac, byte[] salt, String input) { | ||
try { | ||
byte[] encrypted = mac.doFinal(input.getBytes(StandardCharsets.UTF_8)); | ||
byte[] messageWithSalt = new byte[salt.length + encrypted.length]; | ||
System.arraycopy(salt, 0, messageWithSalt, 0, salt.length); | ||
System.arraycopy(encrypted, 0, messageWithSalt, salt.length, encrypted.length); | ||
return Base64.getEncoder().encodeToString(messageWithSalt); | ||
} catch (IllegalStateException e) { | ||
throw new ElasticsearchException("error hashing data", e); | ||
} | ||
} | ||
|
||
public static Method fromString(String processorTag, String propertyName, String type) { | ||
try { | ||
return Method.valueOf(type.toUpperCase(Locale.ROOT)); | ||
} catch(IllegalArgumentException e) { | ||
throw newConfigurationException(TYPE, processorTag, propertyName, "type [" + type + | ||
"] not supported, cannot convert field. Valid hash methods: " + Arrays.toString(Method.values())); | ||
} | ||
} | ||
} | ||
|
||
public static final String TYPE = "hash"; | ||
public static final Setting.AffixSetting<SecureString> HMAC_KEY_SETTING = SecureSetting | ||
.affixKeySetting(SecurityField.setting("ingest." + TYPE) + ".", "key", | ||
(key) -> SecureSetting.secureString(key, null)); | ||
|
||
private final List<String> fields; | ||
private final String targetField; | ||
private final Method method; | ||
private final Mac mac; | ||
private final byte[] salt; | ||
|
||
HashProcessor(String tag, List<String> fields, String targetField, byte[] salt, Method method, @Nullable Mac mac) { | ||
super(tag); | ||
this.fields = fields; | ||
this.targetField = targetField; | ||
this.method = method; | ||
this.mac = mac; | ||
this.salt = salt; | ||
} | ||
|
||
List<String> getFields() { | ||
return fields; | ||
} | ||
|
||
String getTargetField() { | ||
return targetField; | ||
} | ||
|
||
byte[] getSalt() { | ||
return salt; | ||
} | ||
|
||
@Override | ||
public void execute(IngestDocument document) { | ||
Map<String, String> hashedFieldValues = fields.stream().map(f -> { | ||
try { | ||
String value = document.getFieldValue(f, String.class); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should there be missing field support? |
||
return new Tuple<>(f, method.hash(mac, salt, value)); | ||
} catch (Exception e) { | ||
throw new IllegalArgumentException("field[" + f + "] could not be hashed", e); | ||
} | ||
}).collect(Collectors.toMap(Tuple::v1, Tuple::v2)); | ||
if (hashedFieldValues.size() == 1) { | ||
document.setFieldValue(targetField, hashedFieldValues.values().iterator().next()); | ||
} else { | ||
document.setFieldValue(targetField, hashedFieldValues); | ||
} | ||
} | ||
|
||
@Override | ||
public String getType() { | ||
return TYPE; | ||
} | ||
|
||
public static final class Factory implements Processor.Factory { | ||
|
||
private final Settings settings; | ||
|
||
public Factory(Settings settings) { | ||
this.settings = settings; | ||
} | ||
|
||
private static Mac createMac(Method method, SecureString password, byte[] salt, int iterations) { | ||
try { | ||
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2With" + method.getAlgorithm()); | ||
PBEKeySpec keySpec = new PBEKeySpec(password.getChars(), salt, iterations, 128); | ||
byte[] pbkdf2 = secretKeyFactory.generateSecret(keySpec).getEncoded(); | ||
Mac mac = Mac.getInstance(method.getAlgorithm()); | ||
mac.init(new SecretKeySpec(pbkdf2, method.getAlgorithm())); | ||
return mac; | ||
} catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) { | ||
throw new IllegalArgumentException("invalid settings", e); | ||
} | ||
} | ||
|
||
@Override | ||
public HashProcessor create(Map<String, Processor.Factory> registry, String processorTag, Map<String, Object> config) { | ||
List<String> fields = ConfigurationUtils.readList(TYPE, processorTag, config, "fields"); | ||
if (fields.isEmpty()) { | ||
throw ConfigurationUtils.newConfigurationException(TYPE, processorTag, "fields", "must specify at least one field"); | ||
} else if (fields.stream().anyMatch(Strings::isNullOrEmpty)) { | ||
throw ConfigurationUtils.newConfigurationException(TYPE, processorTag, "fields", | ||
"a field-name entry is either empty or null"); | ||
} | ||
String targetField = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "target_field"); | ||
String keySettingName = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "key_setting"); | ||
SecureString key = HMAC_KEY_SETTING.getConcreteSetting(keySettingName).get(settings); | ||
byte[] salt = CharArrays.toUtf8Bytes(Hasher.SaltProvider.salt(8)); | ||
String methodProperty = ConfigurationUtils.readStringProperty(TYPE, processorTag, config, "method", "SHA256"); | ||
Method method = Method.fromString(processorTag, "method", methodProperty); | ||
int iterations = ConfigurationUtils.readIntProperty(TYPE, processorTag, config, "iterations", 5); | ||
Mac mac = createMac(method, key, salt, iterations); | ||
return new HashProcessor(processorTag, fields, targetField, salt, method, mac); | ||
} | ||
} | ||
} |
114 changes: 114 additions & 0 deletions
114
...rity/src/test/java/org/elasticsearch/xpack/security/ingest/HashProcessorFactoryTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* 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.ingest; | ||
|
||
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.common.settings.MockSecureSettings; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.test.ESTestCase; | ||
|
||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
import static org.hamcrest.Matchers.equalTo; | ||
|
||
public class HashProcessorFactoryTests extends ESTestCase { | ||
|
||
public void testProcessor() { | ||
MockSecureSettings mockSecureSettings = new MockSecureSettings(); | ||
mockSecureSettings.setString("xpack.security.ingest.hash.processor.key", "my_key"); | ||
Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); | ||
HashProcessor.Factory factory = new HashProcessor.Factory(settings); | ||
Map<String, Object> config = new HashMap<>(); | ||
config.put("fields", Collections.singletonList("_field")); | ||
config.put("target_field", "_target"); | ||
config.put("key_setting", "xpack.security.ingest.hash.processor.key"); | ||
for (HashProcessor.Method method : HashProcessor.Method.values()) { | ||
config.put("method", method.toString()); | ||
HashProcessor processor = factory.create(null, "_tag", new HashMap<>(config)); | ||
assertThat(processor.getFields(), equalTo(Collections.singletonList("_field"))); | ||
assertThat(processor.getTargetField(), equalTo("_target")); | ||
} | ||
} | ||
|
||
public void testProcessorNoFields() { | ||
MockSecureSettings mockSecureSettings = new MockSecureSettings(); | ||
mockSecureSettings.setString("xpack.security.ingest.hash.processor.key", "my_key"); | ||
Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); | ||
HashProcessor.Factory factory = new HashProcessor.Factory(settings); | ||
Map<String, Object> config = new HashMap<>(); | ||
config.put("salt", "_salt"); | ||
config.put("target_field", "_target"); | ||
config.put("key_setting", "xpack.security.ingest.hash.processor.key"); | ||
config.put("method", HashProcessor.Method.SHA1.toString()); | ||
ElasticsearchException e = expectThrows(ElasticsearchException.class, | ||
() -> factory.create(null, "_tag", config)); | ||
assertThat(e.getMessage(), equalTo("[fields] required property is missing")); | ||
} | ||
|
||
public void testProcessorNoTargetField() { | ||
MockSecureSettings mockSecureSettings = new MockSecureSettings(); | ||
mockSecureSettings.setString("xpack.security.ingest.hash.processor.key", "my_key"); | ||
Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); | ||
HashProcessor.Factory factory = new HashProcessor.Factory(settings); | ||
Map<String, Object> config = new HashMap<>(); | ||
config.put("fields", Collections.singletonList("_field")); | ||
config.put("key_setting", "xpack.security.ingest.hash.processor.key"); | ||
config.put("method", HashProcessor.Method.SHA1.toString()); | ||
ElasticsearchException e = expectThrows(ElasticsearchException.class, | ||
() -> factory.create(null, "_tag", config)); | ||
assertThat(e.getMessage(), equalTo("[target_field] required property is missing")); | ||
} | ||
|
||
public void testProcessorFieldsIsEmpty() { | ||
MockSecureSettings mockSecureSettings = new MockSecureSettings(); | ||
mockSecureSettings.setString("xpack.security.ingest.hash.processor.key", "my_key"); | ||
Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); | ||
HashProcessor.Factory factory = new HashProcessor.Factory(settings); | ||
Map<String, Object> config = new HashMap<>(); | ||
config.put("fields", Collections.singletonList(randomBoolean() ? "" : null)); | ||
config.put("target_field", "_target"); | ||
config.put("key_setting", "xpack.security.ingest.hash.processor.key"); | ||
config.put("method", HashProcessor.Method.SHA1.toString()); | ||
ElasticsearchException e = expectThrows(ElasticsearchException.class, | ||
() -> factory.create(null, "_tag", config)); | ||
assertThat(e.getMessage(), equalTo("[fields] a field-name entry is either empty or null")); | ||
} | ||
|
||
public void testProcessorInvalidMethod() { | ||
MockSecureSettings mockSecureSettings = new MockSecureSettings(); | ||
mockSecureSettings.setString("xpack.security.ingest.hash.processor.key", "my_key"); | ||
Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); | ||
HashProcessor.Factory factory = new HashProcessor.Factory(settings); | ||
Map<String, Object> config = new HashMap<>(); | ||
config.put("fields", Collections.singletonList("_field")); | ||
config.put("target_field", "_target"); | ||
config.put("key_setting", "xpack.security.ingest.hash.processor.key"); | ||
config.put("method", "invalid"); | ||
ElasticsearchException e = expectThrows(ElasticsearchException.class, | ||
() -> factory.create(null, "_tag", config)); | ||
assertThat(e.getMessage(), equalTo("[method] type [invalid] not supported, cannot convert field. " + | ||
"Valid hash methods: [sha1, sha256, sha384, sha512]")); | ||
} | ||
|
||
public void testProcessorInvalidOrMissingKeySetting() { | ||
Settings settings = Settings.builder().setSecureSettings(new MockSecureSettings()).build(); | ||
HashProcessor.Factory factory = new HashProcessor.Factory(settings); | ||
Map<String, Object> config = new HashMap<>(); | ||
config.put("fields", Collections.singletonList("_field")); | ||
config.put("target_field", "_target"); | ||
config.put("key_setting", "invalid"); | ||
config.put("method", HashProcessor.Method.SHA1.toString()); | ||
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, | ||
() -> factory.create(null, "_tag", new HashMap<>(config))); | ||
assertThat(e.getMessage(), equalTo("key [invalid] must match [xpack.security.ingest.hash.*.key] but didn't.")); | ||
config.remove("key_setting"); | ||
ElasticsearchException ex = expectThrows(ElasticsearchException.class, | ||
() -> factory.create(null, "_tag", config)); | ||
assertThat(ex.getMessage(), equalTo("[key_setting] required property is missing")); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We removed SaltProvider in soon to be merged #31234 and opted for generating a random byte array from SecureRandom and then Base64 encoding that to a string, so probably you need to do something similar here too. https://github.com/elastic/elasticsearch/pull/31234/files#diff-ebc23bc2cb194fa926b2cdafaedef9d4R565
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah, it is also private! I see, thanks for the heads up. How soon will that be merged? One of us will have to change their code pre-pushing 😄