diff --git a/.java-version b/.java-version new file mode 100644 index 0000000000000..03b6389f32ad5 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +17.0 diff --git a/keystore-added.gradle b/keystore-added.gradle new file mode 100644 index 0000000000000..713dc8223406c --- /dev/null +++ b/keystore-added.gradle @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +rootProject { + if(project.name == 'elasticsearch') { + afterEvaluate { + testClusters.configureEach { + keystore 'xpack.ingestion.encryption_key', '_passwd' + } + } + } +} diff --git a/x-pack/plugin/enterprise-search/build.gradle b/x-pack/plugin/enterprise-search/build.gradle new file mode 100644 index 0000000000000..134b0eb00b4a7 --- /dev/null +++ b/x-pack/plugin/enterprise-search/build.gradle @@ -0,0 +1,23 @@ +apply plugin: 'elasticsearch.internal-es-plugin' + +esplugin { + name 'enterprise-search' + description 'A module for ingestion encryption' + classname 'org.elasticsearch.xpack.enterprisesearch.EnterpriseSearchPlugin' + extendedPlugins = ['x-pack-core'] +} +archivesBaseName = 'x-pack-enterprise-search' + +dependencies { + compileOnly project(":server") + compileOnly project(path: xpackModule('core')) +} + +tasks.named("dependencyLicenses").configure { + ignoreSha 'x-pack-core' +} + +//no tests +tasks.named("test").configure { + enabled = false +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/EnterpriseSearchPlugin.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/EnterpriseSearchPlugin.java new file mode 100644 index 0000000000000..8836e5e58407c --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/EnterpriseSearchPlugin.java @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.xpack.enterprisesearch; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.settings.SettingsFilter; +import org.elasticsearch.indices.SystemIndexDescriptor; +import org.elasticsearch.plugins.ActionPlugin; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SystemIndexPlugin; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestHandler; +import org.elasticsearch.xpack.enterprisesearch.action.decrypt.DecryptAction; +import org.elasticsearch.xpack.enterprisesearch.action.decrypt.DecryptRestHandler; +import org.elasticsearch.xpack.enterprisesearch.action.decrypt.DecryptTransportAction; +import org.elasticsearch.xpack.enterprisesearch.action.encrypt.EncryptAction; +import org.elasticsearch.xpack.enterprisesearch.action.encrypt.EncryptRestHandler; +import org.elasticsearch.xpack.enterprisesearch.action.encrypt.EncryptTransportAction; +import org.elasticsearch.xpack.enterprisesearch.index.ConnectorIndex; +import org.elasticsearch.xpack.enterprisesearch.index.SyncJobIndex; +import org.elasticsearch.xpack.enterprisesearch.setting.EntSearchField; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import static java.util.Collections.singletonList; + +public class EnterpriseSearchPlugin extends Plugin implements SystemIndexPlugin { + public static final String FEATURE_NAME = "Enterprise Search Connectors"; + public static final String DESCRIPTION = "The state and metadata surrounding registered connectors and their sync jobs"; + + @Override + public List getRestHandlers(final Settings settings, + final RestController restController, + final ClusterSettings clusterSettings, + final IndexScopedSettings indexScopedSettings, + final SettingsFilter settingsFilter, + final IndexNameExpressionResolver indexNameExpressionResolver, + final Supplier nodesInCluster) { + + return List.of( + new EncryptRestHandler(), + new DecryptRestHandler() + ); + } + + @Override + public List> getActions() { + return List.of( + new ActionHandler<>(EncryptAction.INSTANCE, EncryptTransportAction.class), + new ActionHandler<>(DecryptAction.INSTANCE, DecryptTransportAction.class) + ); + } + + @Override + public List> getSettings() { + return singletonList(EntSearchField.ENCRYPTION_KEY_SETTING); + } + + @Override + public String getFeatureName() { + return FEATURE_NAME; + } + + @Override + public String getFeatureDescription() { + return DESCRIPTION; + } + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + SystemIndexDescriptor connectorsIndex = SystemIndexDescriptor.builder() + .setIndexPattern(".elastic-connectors-v*") + .setDescription("State of individual connectors") + .setType(SystemIndexDescriptor.Type.INTERNAL_MANAGED) + .setPrimaryIndex(".elastic-connectors-v1") + .setAliasName(".elastic-connectors") + .setMappings(ConnectorIndex.MAPPING_JSON) + .setSettings(SyncJobIndex.SETTINGS) + .setVersionMetaKey("es-version") + .setOrigin(FEATURE_NAME) + .build(); + SystemIndexDescriptor syncJobsIndex = SystemIndexDescriptor.builder() + .setIndexPattern(".elastic-connectors-sync-jobs-v*") + .setDescription("History/log of connector sync jobs") + .setType(SystemIndexDescriptor.Type.INTERNAL_MANAGED) + .setPrimaryIndex(".elastic-connectors-sync-jobs-v1") + .setAliasName(".elastic-connectors-sync-jobs") + .setMappings(SyncJobIndex.MAPPING_JSON) + .setSettings(SyncJobIndex.SETTINGS) + .setVersionMetaKey("es-version") + .setOrigin(FEATURE_NAME) + .build(); + + return List.of(connectorsIndex, syncJobsIndex); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptAction.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptAction.java new file mode 100644 index 0000000000000..8e2b8955a18df --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptAction.java @@ -0,0 +1,22 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.decrypt; + +import org.elasticsearch.action.ActionType; + +public class DecryptAction extends ActionType { + + public static final DecryptAction INSTANCE = new DecryptAction(); + public static final String NAME = "indices:data/read/ent-search-decrypt"; + + public DecryptAction() { + super(NAME, DecryptResponse::new); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptRequest.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptRequest.java new file mode 100644 index 0000000000000..e33e353348cf5 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptRequest.java @@ -0,0 +1,105 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.decrypt; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; + +public class DecryptRequest extends ActionRequest implements IndicesRequest, ToXContentObject { + + private String index; + private String id; + private String field; + + public DecryptRequest(){ + super(); + } + + public DecryptRequest(StreamInput in) throws IOException { + super(in); + this.index = in.readString(); + this.id = in.readString(); + this.field = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(index); + out.writeString(id); + out.writeString(field); + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder + .startObject() + .field("index", params.param("index")) + .field("id", params.param("id")) + .field("field", params.param("field")) + .endObject(); + } + + @Override + public ActionRequestValidationException validate() { + return null; // TODO + } + + /** + * Returns the array of indices that the action relates to + */ + @Override + public String[] indices() { + String[] indices = new String[1]; + indices[0] = index; + return indices; + } + + /** + * Returns the indices options used to resolve indices. They tell for instance whether a single index is + * accepted, whether an empty array will be converted to _all, and how wildcards will be expanded if needed. + */ + @Override + public IndicesOptions indicesOptions() { + return IndicesOptions.STRICT_EXPAND_OPEN_CLOSED_HIDDEN; // TODO no idea what this is about + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptResponse.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptResponse.java new file mode 100644 index 0000000000000..5016715620039 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptResponse.java @@ -0,0 +1,70 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.decrypt; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.StatusToXContentObject; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; + +public class DecryptResponse extends ActionResponse implements StatusToXContentObject { + + String value; + + public DecryptResponse(){ + super(); + } + + public DecryptResponse(StreamInput in) throws IOException { + super(in); + this.value = in.readString(); + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field("value", value) + .endObject(); + } + + /** + * Write this into the {@linkplain StreamOutput}. + * + * @param out + */ + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(value); + } + + /** + * Returns the REST status to make sure it is returned correctly + */ + @Override + public RestStatus status() { + if (value != null) { + return RestStatus.OK; + } else { + return RestStatus.INTERNAL_SERVER_ERROR; + } + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptRestHandler.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptRestHandler.java new file mode 100644 index 0000000000000..e4acccb0f5d08 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptRestHandler.java @@ -0,0 +1,44 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.decrypt; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestStatusToXContentListener; + +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class DecryptRestHandler extends BaseRestHandler { + + @Override + public String getName() { + return "rest_ent_search_decrypt"; + } + + @Override + public List routes() { + return List.of( + new Route(GET, "_decrypt/{index}/{id}/{field}") + ); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + DecryptRequest decryptRequest = new DecryptRequest(); + decryptRequest.setIndex(request.param("index")); + decryptRequest.setId(request.param("id")); + decryptRequest.setField(request.param("field")); + + return channel -> client.execute(DecryptAction.INSTANCE, decryptRequest, new RestStatusToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptTransportAction.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptTransportAction.java new file mode 100644 index 0000000000000..fa104eecf9f34 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/decrypt/DecryptTransportAction.java @@ -0,0 +1,61 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.decrypt; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.search.fetch.subphase.FetchSourceContext; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.enterprisesearch.crypto.CryptoService; + +public class DecryptTransportAction extends HandledTransportAction { + + private final NodeClient client; + private final CryptoService cryptoService; + protected Logger logger = LogManager.getLogger(getClass()); + + @Inject + public DecryptTransportAction(TransportService transportService, ActionFilters actionFilters, NodeClient client) { + super(DecryptAction.NAME, transportService, actionFilters, DecryptRequest::new); + this.client = client; + this.cryptoService = new CryptoService(client.settings()); + } + + @Override + protected void doExecute(Task task, DecryptRequest request, ActionListener listener) { + String[] includes = {request.getField()}; + FetchSourceContext fetchSourceContext = FetchSourceContext.of(true, includes, null); + GetRequest getRequest = new GetRequest() + .index(request.getIndex()) + .id(request.getId()) + .fetchSourceContext(fetchSourceContext); + client.get(getRequest, listener.delegateFailure((l, getResponse) -> { + try { + DecryptResponse decryptResponse = new DecryptResponse(); + decryptResponse.setValue( + new String(cryptoService.decrypt(getResponse.getSource().get(request.getField()).toString().toCharArray())) + ); + l.onResponse(decryptResponse); + logger.info("Successfully responded to decrypt request"); + } catch (Throwable t) { + Exception e = new Exception("Decrypt task failed.", t); + logger.error("Failed to decrypt.", e); + l.onFailure(e); + } + })); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptAction.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptAction.java new file mode 100644 index 0000000000000..14ea418a0e584 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptAction.java @@ -0,0 +1,23 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.encrypt; + +import org.elasticsearch.action.ActionType; + +public class EncryptAction extends ActionType { + + + public static final EncryptAction INSTANCE = new EncryptAction(); + public static final String NAME = "indices:data/write/ent-search-encrypt"; + + private EncryptAction() { + super(NAME, EncryptResponse::new); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptRequest.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptRequest.java new file mode 100644 index 0000000000000..cefd7ad69a2a3 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptRequest.java @@ -0,0 +1,108 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.encrypt; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.action.IndicesRequest; +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.xcontent.ToXContentObject; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; + +public class EncryptRequest extends ActionRequest implements IndicesRequest, ToXContentObject { + private String index; + private String id; + private String field; + private String value; + + public EncryptRequest(){ + super(); + } + + public EncryptRequest(StreamInput in) throws IOException { + super(in); + this.index = in.readString(); + this.id = in.readString(); + this.field = in.readString(); + this.value = in.readString(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(this.index); + out.writeString(this.id); + out.writeString(this.field); + out.writeString(this.value); + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder + .startObject() + .field("id", params.param("id")) + .field("index", params.param("index")) + .field("field", params.param("field")) + .field("value", params.param("value")) + .endObject(); + } + + @Override + public ActionRequestValidationException validate() { + return null; // TODO + } + + @Override + public String[] indices() { + return Collections.singletonList(index).toArray(new String[1]); + } + + @Override + public IndicesOptions indicesOptions() { + return IndicesOptions.STRICT_EXPAND_OPEN_CLOSED_HIDDEN; // TODO no idea what this is about + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptResponse.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptResponse.java new file mode 100644 index 0000000000000..ecac39c6ac9fa --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptResponse.java @@ -0,0 +1,64 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.encrypt; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.update.UpdateResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.StatusToXContentObject; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xcontent.XContentBuilder; + +import java.io.IOException; + +public class EncryptResponse extends ActionResponse implements StatusToXContentObject { + + private UpdateResponse updateResponse = null; + + public EncryptResponse(){ + super(); + } + + public EncryptResponse(StreamInput in) throws IOException { + super(in); + this.updateResponse = in.readOptionalWriteable(UpdateResponse::new); + } + + public UpdateResponse getUpdateResponse() { + return updateResponse; + } + + public void setUpdateResponse(UpdateResponse updateResponse) { + this.updateResponse = updateResponse; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder + .startObject() + .field("result", updateResponse.getResult().toString()) + .endObject(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeOptionalWriteable(updateResponse); + } + + @Override + public RestStatus status() { + if (updateResponse != null) { + return updateResponse.status(); + } else { + return RestStatus.INTERNAL_SERVER_ERROR; + } + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptRestHandler.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptRestHandler.java new file mode 100644 index 0000000000000..8c4417a1f6a81 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptRestHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.encrypt; + +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.rest.BaseRestHandler; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestStatusToXContentListener; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class EncryptRestHandler extends BaseRestHandler { + + @Override + public String getName() { + return "rest_ent_search_encrypt"; + } + + @Override + public List routes() { + return List.of( + new Route(POST, "_encrypt/{index}/{id}") + ); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + + EncryptRequest encryptRequest = new EncryptRequest(); + encryptRequest.setIndex(request.param("index")); + encryptRequest.setId(request.param("id")); + Map body = request.contentParser().map(); + encryptRequest.setField((String) body.get("field")); + encryptRequest.setValue((String) body.get("value")); + + return channel -> client.execute(EncryptAction.INSTANCE, encryptRequest, new RestStatusToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptTransportAction.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptTransportAction.java new file mode 100644 index 0000000000000..549be47ec9ff2 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/action/encrypt/EncryptTransportAction.java @@ -0,0 +1,66 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.action.encrypt; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.action.update.UpdateRequest; +import org.elasticsearch.client.internal.node.NodeClient; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.enterprisesearch.crypto.CryptoService; + +import java.util.HashMap; +import java.util.Map; + + +public class EncryptTransportAction extends HandledTransportAction { + + protected Logger logger = LogManager.getLogger(getClass()); + private final NodeClient client; + private final CryptoService cryptoService; + + @Inject + public EncryptTransportAction(TransportService transportService, ActionFilters actionFilters, NodeClient client) { + super(EncryptAction.NAME, transportService, actionFilters, EncryptRequest::new); + this.client = client; + this.cryptoService = new CryptoService(client.settings()); + } + + @Override + protected void doExecute(Task task, EncryptRequest request, ActionListener listener) { + final EncryptResponse encryptResponse = new EncryptResponse(); + + // encrypt the value + String encryptedValue = new String(cryptoService.encrypt(request.getValue().toCharArray())); + + // update the document's field + Map jsonMap = new HashMap<>(); + jsonMap.put(request.getField(), encryptedValue); + + UpdateRequest updateRequest = new UpdateRequest(request.getIndex(), request.getId()).doc(jsonMap); + logger.info("Made the request"); + client.update(updateRequest, listener.delegateFailure((l, updateResponse) -> { + try { + encryptResponse.setUpdateResponse(updateResponse); + l.onResponse(encryptResponse); + logger.info("Successfully responded to encrypt request"); + } catch (Throwable t) { + Exception e = new Exception("Encrypt task failed.", t); + logger.error("Failed to encrypt.", e); + l.onFailure(e); + } + })); + } +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/crypto/CryptoService.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/crypto/CryptoService.java new file mode 100644 index 0000000000000..471520b14f577 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/crypto/CryptoService.java @@ -0,0 +1,217 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.crypto; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Setting.Property; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.CharArrays; +import org.elasticsearch.xpack.core.security.SecurityField; +import org.elasticsearch.xpack.enterprisesearch.setting.EntSearchField; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +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 javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Service that provides cryptographic methods based on a shared system key + */ +public class CryptoService { + + public static final String KEY_ALGO = "HmacSHA512"; + + public static final String ENCRYPTED_TEXT_PREFIX = "::es_encrypted::"; + + private static final String DEFAULT_ENCRYPTION_ALGORITHM = "AES/CTR/NoPadding"; + private static final String DEFAULT_KEY_ALGORITH = "AES"; + private static final int DEFAULT_KEY_LENGTH = 128; + + private static final Setting ENCRYPTION_ALGO_SETTING = new Setting<>( + SecurityField.setting("encryption.algorithm"), + s -> DEFAULT_ENCRYPTION_ALGORITHM, + s -> s, + Property.NodeScope + ); + private static final Setting ENCRYPTION_KEY_LENGTH_SETTING = Setting.intSetting( + SecurityField.setting("encryption_key.length"), + DEFAULT_KEY_LENGTH, + Property.NodeScope + ); + private static final Setting ENCRYPTION_KEY_ALGO_SETTING = new Setting<>( + SecurityField.setting("encryption_key.algorithm"), + DEFAULT_KEY_ALGORITH, + s -> s, + Property.NodeScope + ); + private static final Logger logger = LogManager.getLogger(CryptoService.class); + + private final SecureRandom secureRandom = new SecureRandom(); + private final String encryptionAlgorithm; + private final int ivLength; + /* + * The encryption key is derived from the system key. + */ + private final SecretKey encryptionKey; + + public CryptoService(Settings settings) { + this.encryptionAlgorithm = ENCRYPTION_ALGO_SETTING.get(settings); + final int keyLength = ENCRYPTION_KEY_LENGTH_SETTING.get(settings); + this.ivLength = keyLength / 8; + String 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"); + } + + try(SecureString key = EntSearchField.ENCRYPTION_KEY_SETTING.get(settings)) { + if (key == null || key.isEmpty()) { + throw new ElasticsearchException("setting [" + EntSearchField.ENCRYPTION_KEY_SETTING.getKey() + "] must be set in keystore"); + } + SecretKey systemKey = readSystemKey(key); + + try { + encryptionKey = encryptionKey(systemKey, keyLength, keyAlgorithm); + } catch (NoSuchAlgorithmException nsae) { + throw new ElasticsearchException("failed to start crypto service. could not load encryption key", nsae); + } + } + + assert encryptionKey != null : "the encryption key should never be null"; + } + + private static SecretKey readSystemKey(SecureString key) { + return new SecretKeySpec(key.toString().getBytes(StandardCharsets.UTF_8), KEY_ALGO); + } + + /** + * 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 + */ + public char[] encrypt(char[] chars) { + byte[] charBytes = CharArrays.toUtf8Bytes(chars); + String base64 = Base64.getEncoder().encodeToString(encryptInternal(charBytes, encryptionKey)); + 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 + */ + public char[] decrypt(char[] chars) { + if (isEncrypted(chars) == false) { + // 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 + */ + protected boolean isEncrypted(char[] chars) { + return CharArrays.charsBeginsWith(ENCRYPTED_TEXT_PREFIX, chars); + } + + 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); + } + } + + private 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); + } + } + + private static SecretKey encryptionKey(SecretKey systemKey, int keyLength, String algorithm) throws NoSuchAlgorithmException { + byte[] bytes = systemKey.getEncoded(); + + 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); + } + + public static void addSettings(List> settings) { + settings.add(ENCRYPTION_KEY_LENGTH_SETTING); + settings.add(ENCRYPTION_KEY_ALGO_SETTING); + settings.add(ENCRYPTION_ALGO_SETTING); + } +} + diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/index/ConnectorIndex.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/index/ConnectorIndex.java new file mode 100644 index 0000000000000..d99fd2d6d9162 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/index/ConnectorIndex.java @@ -0,0 +1,101 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.index; + +import org.elasticsearch.common.settings.Settings; + +public class ConnectorIndex { + public static final String MAPPING_JSON = "{\n" + + " \"_meta\": {\n" + + " \"pipeline\": {\n" + + " \"default_extract_binary_content\": true,\n" + + " \"default_name\": \"ent-search-generic-ingestion\",\n" + + " \"default_reduce_whitespace\": true,\n" + + " \"default_run_ml_inference\": true\n" + + " },\n" + + " \"version\": 1,\n" + + " \"es-version\": \"8.6.0\"" + + " },\n" + + " \"properties\": {\n" + + " \"api_key_id\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"configuration\": {\n" + + " \"type\": \"object\"\n" + + " },\n" + + " \"error\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"index_name\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"language\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"last_seen\": {\n" + + " \"type\": \"date\"\n" + + " },\n" + + " \"last_sync_error\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"last_sync_status\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"last_synced\": {\n" + + " \"type\": \"date\"\n" + + " },\n" + + " \"name\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"pipeline\": {\n" + + " \"properties\": {\n" + + " \"name\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"extract_binary_content\": {\n" + + " \"type\": \"boolean\"\n" + + " },\n" + + " \"reduce_whitespace\": {\n" + + " \"type\": \"boolean\"\n" + + " },\n" + + " \"run_ml_inference\": {\n" + + " \"type\": \"boolean\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"scheduling\": {\n" + + " \"properties\": {\n" + + " \"enabled\": {\n" + + " \"type\": \"boolean\"\n" + + " },\n" + + " \"interval\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"service_type\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"status\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"sync_now\": {\n" + + " \"type\": \"boolean\"\n" + + " }\n" + + " }\n" + + "}"; + + public static final Settings SETTINGS = Settings.builder() + .put("auto_expand_replicas", "0-3") + .put("hidden", true) + .put("number_of_replicas", 0) + .build(); + +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/index/SyncJobIndex.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/index/SyncJobIndex.java new file mode 100644 index 0000000000000..fdd0cff675ad6 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/index/SyncJobIndex.java @@ -0,0 +1,107 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.index; + +import org.elasticsearch.common.settings.Settings; + +public class SyncJobIndex { + public static final String MAPPING_JSON = "{\n" + + " \"_meta\": {\n" + + " \"version\": 1,\n" + + " \"es-version\": \"8.6.0\"" + + " },\n" + + " \"properties\": {\n" + + " \"completed_at\": {\n" + + " \"type\": \"date\"\n" + + " },\n" + + " \"connector\": {\n" + + " \"properties\": {\n" + + " \"api_key_id\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"configuration\": {\n" + + " \"type\": \"object\"\n" + + " },\n" + + " \"error\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"index_name\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"language\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"last_seen\": {\n" + + " \"type\": \"date\"\n" + + " },\n" + + " \"last_sync_error\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"last_sync_status\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"last_synced\": {\n" + + " \"type\": \"date\"\n" + + " },\n" + + " \"name\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"scheduling\": {\n" + + " \"properties\": {\n" + + " \"enabled\": {\n" + + " \"type\": \"boolean\"\n" + + " },\n" + + " \"interval\": {\n" + + " \"type\": \"text\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"service_type\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"status\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"sync_now\": {\n" + + " \"type\": \"boolean\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"connector_id\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"created_at\": {\n" + + " \"type\": \"date\"\n" + + " },\n" + + " \"deleted_document_count\": {\n" + + " \"type\": \"integer\"\n" + + " },\n" + + " \"error\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"indexed_document_count\": {\n" + + " \"type\": \"integer\"\n" + + " },\n" + + " \"status\": {\n" + + " \"type\": \"keyword\"\n" + + " },\n" + + " \"worker_hostname\": {\n" + + " \"type\": \"keyword\"\n" + + " }\n" + + " }\n" + + "}"; + + public static final Settings SETTINGS = Settings.builder() + .put("auto_expand_replicas", "0-3") + .put("hidden", true) + .put("number_of_replicas", 0) + .put("number_of_shards", 1) + .build(); +} diff --git a/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/setting/EntSearchField.java b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/setting/EntSearchField.java new file mode 100644 index 0000000000000..2ec716006a36a --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/main/java/org/elasticsearch/xpack/enterprisesearch/setting/EntSearchField.java @@ -0,0 +1,19 @@ + + +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.setting; + +import org.elasticsearch.common.settings.SecureSetting; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; + +public final class EntSearchField { + public static final Setting ENCRYPTION_KEY_SETTING = SecureSetting.secureString("xpack.ingestion.encryption_key", null); + +} diff --git a/x-pack/plugin/enterprise-search/src/test/java/org/elasticsearch/xpack/enterprisesearch/EnterpriseSearchPluginIT.java b/x-pack/plugin/enterprise-search/src/test/java/org/elasticsearch/xpack/enterprisesearch/EnterpriseSearchPluginIT.java new file mode 100644 index 0000000000000..8f8461acc3425 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/test/java/org/elasticsearch/xpack/enterprisesearch/EnterpriseSearchPluginIT.java @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch; + +import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.xpack.enterprisesearch.action.decrypt.DecryptAction; +import org.elasticsearch.xpack.enterprisesearch.action.decrypt.DecryptRequest; +import org.elasticsearch.xpack.enterprisesearch.action.decrypt.DecryptResponse; +import org.elasticsearch.xpack.enterprisesearch.action.encrypt.EncryptAction; +import org.elasticsearch.xpack.enterprisesearch.action.encrypt.EncryptRequest; +import org.elasticsearch.xpack.enterprisesearch.action.encrypt.EncryptResponse; +import org.elasticsearch.xpack.enterprisesearch.crypto.CryptoService; +import org.elasticsearch.xpack.enterprisesearch.setting.EntSearchField; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; + + +@ESIntegTestCase.ClusterScope(scope = ESIntegTestCase.Scope.TEST, numDataNodes = 0) +public class EnterpriseSearchPluginIT extends ESIntegTestCase { + + private String index = "test-index"; + private String id = "test_id"; + private String field = "some_field"; + private String value = "some value"; + + @Override + protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { + Settings.Builder settings = Settings.builder().put(super.nodeSettings(nodeOrdinal, otherSettings)); + MockSecureSettings secureSettingsWithPassword = new MockSecureSettings(); + secureSettingsWithPassword.setString( + EntSearchField.ENCRYPTION_KEY_SETTING.getKey(), + randomAlphaOfLength(20) + ); + settings.setSecureSettings(secureSettingsWithPassword); + return settings.build(); + } + + + @Override + protected Collection> getMockPlugins() { + List> plugins = new ArrayList<>(super.getMockPlugins()); + plugins.add(EnterpriseSearchPlugin.class); + return plugins; + } + + public void testEncryption() throws ExecutionException, InterruptedException { + //setup + internalCluster().startNodes(Settings.EMPTY); + CreateIndexResponse res = client().admin().indices().prepareCreate(index).get(); + assertTrue(res.isAcknowledged()); + Map sourceDoc = new HashMap<>(); + sourceDoc.put("other_key", "other_value"); + IndexResponse indexRes = client().prepareIndex().setIndex(index).setId(id).setSource(sourceDoc).get(); + assertEquals(indexRes.status(), RestStatus.CREATED); + + //make an encryption request + EncryptRequest encryptRequest = new EncryptRequest(); + encryptRequest.setIndex(index); + encryptRequest.setId(id); + encryptRequest.setField(field); + encryptRequest.setValue(value); + EncryptResponse response = client().execute(EncryptAction.INSTANCE, encryptRequest).get(); + assertEquals(response.status(), RestStatus.OK); + + //check that the value is encrypted + GetResponse getResponse = client().prepareGet().setIndex(index).setId(id).get(); + Map source = getResponse.getSource(); + assertTrue(((String) source.get(field)).startsWith(CryptoService.ENCRYPTED_TEXT_PREFIX)); + + //decrypt it + DecryptRequest decryptRequest = new DecryptRequest(); + decryptRequest.setIndex(index); + decryptRequest.setId(id); + decryptRequest.setField(field); + DecryptResponse decryptResponse = client().execute(DecryptAction.INSTANCE, decryptRequest).get(); + assertEquals(decryptResponse.getValue(), value); + } +} diff --git a/x-pack/plugin/enterprise-search/src/test/java/org/elasticsearch/xpack/enterprisesearch/crypto/CryptoServiceTest.java b/x-pack/plugin/enterprise-search/src/test/java/org/elasticsearch/xpack/enterprisesearch/crypto/CryptoServiceTest.java new file mode 100644 index 0000000000000..f2d77f9561786 --- /dev/null +++ b/x-pack/plugin/enterprise-search/src/test/java/org/elasticsearch/xpack/enterprisesearch/crypto/CryptoServiceTest.java @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.enterprisesearch.crypto; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.enterprisesearch.setting.EntSearchField; +import org.junit.Before; + +import java.io.IOException; +import java.util.Arrays; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + + +public class CryptoServiceTest extends ESTestCase { + + private final String testKey = "abc123xyz"; + private final String expectedDecryptedStr = "input string"; + private final String expectedEncryptedStr = "::es_encrypted::BGPWwBuQwk98AS9pKk12yhtzpCSa5gg8WAYucg=="; + private Settings settings; + + @Before + public void init() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + mockSecureSettings.setString(EntSearchField.ENCRYPTION_KEY_SETTING.getKey(), testKey); + settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + } + + public void testDecrypt() throws IOException { + CryptoService service = new CryptoService(settings); + String decryptedStr = new String(service.decrypt(expectedEncryptedStr.toCharArray())); + assertEquals(decryptedStr, expectedDecryptedStr); + } + + public void testEncryptionAndDecryptionChars() throws Exception { + CryptoService service = new CryptoService(settings); + final char[] chars = randomAlphaOfLengthBetween(0, 1000).toCharArray(); + final char[] encrypted = service.encrypt(chars); + assertThat(encrypted, notNullValue()); + assertThat(Arrays.equals(encrypted, chars), is(false)); + + final char[] decrypted = service.decrypt(encrypted); + assertThat(Arrays.equals(chars, decrypted), is(true)); + } + + public void testEncryptedChar() throws Exception { + CryptoService service = new CryptoService(settings); + + assertThat(service.isEncrypted((char[]) null), is(false)); + assertThat(service.isEncrypted(new char[0]), is(false)); + 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(randomAlphaOfLengthBetween(0, 100).toCharArray()), is(false)); + assertThat(service.isEncrypted(service.encrypt(randomAlphaOfLength(10).toCharArray())), is(true)); + } + + public void testErrorMessageWhenSecureEncryptionKeySettingDoesNotExist() throws Exception { + final ElasticsearchException e = expectThrows(ElasticsearchException.class, () -> new CryptoService(Settings.EMPTY)); + assertThat(e.getMessage(), is("setting [" + EntSearchField.ENCRYPTION_KEY_SETTING.getKey() + "] must be set in keystore")); + } +} diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index e4743ce69046a..9fc8623577069 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -423,6 +423,7 @@ public class Constants { "indices:data/read/async_search/get", "indices:data/read/async_search/submit", "indices:data/read/close_point_in_time", + "indices:data/read/ent-search-decrypt", "indices:data/read/eql", "indices:data/read/eql/async/get", "indices:data/read/explain", @@ -457,6 +458,7 @@ public class Constants { "indices:data/write/bulk_shard_operations[s]", "indices:data/write/delete", "indices:data/write/delete/byquery", + "indices:data/write/ent-search-encrypt", "indices:data/write/index", "indices:data/write/reindex", "indices:data/write/update",