diff --git a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java index b5c6ed70ad0d2..3bbe5db68353b 100644 --- a/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java +++ b/plugins/repository-azure/src/main/java/org/elasticsearch/repositories/azure/AzureRepository.java @@ -124,7 +124,7 @@ protected AzureBlobStore createBlobStore() { } @Override - protected ByteSizeValue chunkSize() { + public ByteSizeValue chunkSize() { return chunkSize; } diff --git a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java index 4b17fd6bef3ea..e5b33b8d5fdf6 100644 --- a/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java +++ b/plugins/repository-gcs/src/main/java/org/elasticsearch/repositories/gcs/GoogleCloudStorageRepository.java @@ -94,7 +94,7 @@ protected GoogleCloudStorageBlobStore createBlobStore() { } @Override - protected ByteSizeValue chunkSize() { + public ByteSizeValue chunkSize() { return chunkSize; } diff --git a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java index 72430bcd36631..dcc3076f03893 100644 --- a/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java +++ b/plugins/repository-hdfs/src/main/java/org/elasticsearch/repositories/hdfs/HdfsRepository.java @@ -233,7 +233,7 @@ protected HdfsBlobStore createBlobStore() { } @Override - protected ByteSizeValue chunkSize() { + public ByteSizeValue chunkSize() { return chunkSize; } } diff --git a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java index af895758723f5..688878b6b630a 100644 --- a/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java +++ b/plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3Repository.java @@ -207,7 +207,7 @@ protected BlobStore getBlobStore() { } @Override - protected ByteSizeValue chunkSize() { + public ByteSizeValue chunkSize() { return chunkSize; } } diff --git a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java index 59c6a248ca0f4..162932477a4d7 100644 --- a/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java @@ -180,6 +180,8 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp protected final ChecksumBlobStoreFormat snapshotFormat; + private final NamedXContentRegistry namedXContentRegistry; + private final boolean readOnly; private final ChecksumBlobStoreFormat indexShardSnapshotFormat; @@ -194,6 +196,10 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp private final BlobPath basePath; + protected BlobStoreRepository(BlobStoreRepository other) { + this(other.metadata, other.namedXContentRegistry, other.threadPool, other.basePath); + } + /** * Constructs new BlobStoreRepository * @param metadata The metadata for this repository including name and settings @@ -211,6 +217,7 @@ protected BlobStoreRepository( restoreRateLimiter = getRateLimiter(metadata.settings(), "max_restore_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB)); readOnly = metadata.settings().getAsBoolean("readonly", false); this.basePath = basePath; + this.namedXContentRegistry = namedXContentRegistry; indexShardSnapshotFormat = new ChecksumBlobStoreFormat<>(SNAPSHOT_CODEC, SNAPSHOT_NAME_FORMAT, BlobStoreIndexShardSnapshot::fromXContent, namedXContentRegistry, compress); @@ -343,7 +350,7 @@ protected final boolean isCompress() { * * @return chunk size */ - protected ByteSizeValue chunkSize() { + public ByteSizeValue chunkSize() { return null; } diff --git a/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java b/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java index 61558e4f42efa..c177de9afc6f1 100644 --- a/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java +++ b/server/src/main/java/org/elasticsearch/repositories/fs/FsRepository.java @@ -109,7 +109,7 @@ protected BlobStore createBlobStore() throws Exception { } @Override - protected ByteSizeValue chunkSize() { + public ByteSizeValue chunkSize() { return chunkSize; } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java index 1b20ceae9233e..099e06c205301 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackPlugin.java @@ -76,7 +76,6 @@ import java.time.Clock; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -313,7 +312,7 @@ public static Path resolveConfigFile(Environment env, String name) { @Override public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, ThreadPool threadPool) { - return Collections.singletonMap("source", SourceOnlySnapshotRepository.newRepositoryFactory()); + return Map.of("source", SourceOnlySnapshotRepository.newRepositoryFactory()); } @Override diff --git a/x-pack/plugin/repository-encrypted/build.gradle b/x-pack/plugin/repository-encrypted/build.gradle new file mode 100644 index 0000000000000..7e0014987d934 --- /dev/null +++ b/x-pack/plugin/repository-encrypted/build.gradle @@ -0,0 +1,16 @@ +evaluationDependsOn(xpackModule('core')) + +apply plugin: 'elasticsearch.esplugin' +esplugin { + name 'repository-encrypted' + description 'Elasticsearch Expanded Pack Plugin - client-side encrypted repositories.' + classname 'org.elasticsearch.repositories.encrypted.EncryptedRepositoryPlugin' + extendedPlugins = ['x-pack-core'] +} + +dependencies { + compile "org.bouncycastle:bc-fips:1.0.1" + compile "org.bouncycastle:bcpkix-fips:1.0.3" +} + +integTest.enabled = false diff --git a/x-pack/plugin/repository-encrypted/licenses/bc-fips-1.0.1.jar.sha1 b/x-pack/plugin/repository-encrypted/licenses/bc-fips-1.0.1.jar.sha1 new file mode 100644 index 0000000000000..2e4bb227b43bc --- /dev/null +++ b/x-pack/plugin/repository-encrypted/licenses/bc-fips-1.0.1.jar.sha1 @@ -0,0 +1 @@ +ed8dd3144761eaa33b9c56f5e2bef85f1b731d6f \ No newline at end of file diff --git a/x-pack/plugin/repository-encrypted/licenses/bc-fips-LICENSE.txt b/x-pack/plugin/repository-encrypted/licenses/bc-fips-LICENSE.txt new file mode 100644 index 0000000000000..e94fe212ff725 --- /dev/null +++ b/x-pack/plugin/repository-encrypted/licenses/bc-fips-LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/x-pack/plugin/repository-encrypted/licenses/bc-fips-NOTICE.txt b/x-pack/plugin/repository-encrypted/licenses/bc-fips-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-1.0.3.jar.sha1 b/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-1.0.3.jar.sha1 new file mode 100644 index 0000000000000..3262bdf0f3d03 --- /dev/null +++ b/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-1.0.3.jar.sha1 @@ -0,0 +1 @@ +33c47b105777c9dcc8a08188186bd35401366bd1 \ No newline at end of file diff --git a/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-LICENSE.txt b/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-LICENSE.txt new file mode 100644 index 0000000000000..e94fe212ff725 --- /dev/null +++ b/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) 2000 - 2019 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-NOTICE.txt b/x-pack/plugin/repository-encrypted/licenses/bcpkix-fips-NOTICE.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepository.java b/x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepository.java new file mode 100644 index 0000000000000..bb1317d73260d --- /dev/null +++ b/x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepository.java @@ -0,0 +1,308 @@ +/* + * 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.repositories.encrypted; + +import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; +import org.elasticsearch.cluster.metadata.RepositoryMetaData; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.blobstore.BlobContainer; +import org.elasticsearch.common.blobstore.BlobMetaData; +import org.elasticsearch.common.blobstore.BlobPath; +import org.elasticsearch.common.blobstore.BlobStore; +import org.elasticsearch.common.blobstore.DeleteResult; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.hash.MessageDigests; +import org.elasticsearch.common.io.Streams; +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.common.unit.ByteSizeUnit; +import org.elasticsearch.common.unit.ByteSizeValue; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.repositories.blobstore.BlobStoreRepository; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class EncryptedRepository extends BlobStoreRepository { + + static final Setting.AffixSetting ENCRYPTION_PASSWORD_SETTING = Setting.affixKeySetting("repository.encrypted.", + "password", key -> SecureSetting.secureString(key, null)); + + private static final Setting DELEGATE_TYPE = new Setting<>("delegate_type", "", Function.identity()); + private static final int GCM_TAG_BYTES_LENGTH = 16; + private static final String ENCRYPTION_MODE = "AES/GCM/NoPadding"; + private static final String ENCRYPTION_METADATA_PREFIX = "encryption-metadata-"; + // always the same IV because the key is randomly generated anew (Key-IV pair is never repeated) + //private static final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, new byte[] {0,1,2,3,4,5,6,7,8,9,10,11 }); + private static final IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }); + // given the mode, the IV and the tag length, the maximum "chunk" size is ~64GB, we set it to 32GB to err on the safe side + public static final ByteSizeValue MAX_CHUNK_SIZE = new ByteSizeValue(32, ByteSizeUnit.GB); + + private static final BouncyCastleFipsProvider BC_FIPS_PROV = new BouncyCastleFipsProvider(); + + private final BlobStoreRepository delegatedRepository; + private final SecretKey masterSecretKey; + + protected EncryptedRepository(BlobStoreRepository delegatedRepository, SecretKey masterSecretKey) { + super(delegatedRepository); + this.delegatedRepository = delegatedRepository; + this.masterSecretKey = masterSecretKey; + } + + @Override + protected BlobStore createBlobStore() throws Exception { + return new EncryptedBlobStoreDecorator(this.delegatedRepository.blobStore(), this.masterSecretKey); + } + + @Override + protected void doStart() { + this.delegatedRepository.start(); + super.doStart(); + } + + @Override + protected void doStop() { + super.doStop(); + this.delegatedRepository.stop(); + } + + @Override + protected void doClose() { + super.doClose(); + this.delegatedRepository.close(); + } + + @Override + public ByteSizeValue chunkSize() { + ByteSizeValue delegatedChunkSize = this.delegatedRepository.chunkSize(); + if (delegatedChunkSize == null || delegatedChunkSize.compareTo(MAX_CHUNK_SIZE) > 0) { + return MAX_CHUNK_SIZE; + } else { + return delegatedChunkSize; + } + } + + /** + * Returns a new encrypted repository factory + */ + public static Repository.Factory newRepositoryFactory(final Settings settings) { + final Map cachedRepositoryPasswords = new HashMap<>(); + for (String repositoryName : ENCRYPTION_PASSWORD_SETTING.getNamespaces(settings)) { + Setting encryptionPasswordSetting = ENCRYPTION_PASSWORD_SETTING + .getConcreteSettingForNamespace(repositoryName); + SecureString encryptionPassword = encryptionPasswordSetting.get(settings); + cachedRepositoryPasswords.put(repositoryName, encryptionPassword.getChars()); + } + return new Repository.Factory() { + + @Override + public Repository create(RepositoryMetaData metadata) { + throw new UnsupportedOperationException(); + } + + @Override + public Repository create(RepositoryMetaData metaData, Function typeLookup) throws Exception { + String delegateType = DELEGATE_TYPE.get(metaData.settings()); + if (Strings.hasLength(delegateType) == false) { + throw new IllegalArgumentException(DELEGATE_TYPE.getKey() + " must be set"); + } + + if (false == cachedRepositoryPasswords.containsKey(metaData.name())) { + throw new IllegalArgumentException( + ENCRYPTION_PASSWORD_SETTING.getConcreteSettingForNamespace(metaData.name()).getKey() + " must be set"); + } + SecretKey secretKey = generateSecretKeyFromPassword(cachedRepositoryPasswords.get(metaData.name())); + Repository.Factory factory = typeLookup.apply(delegateType); + Repository delegatedRepository = factory.create(new RepositoryMetaData(metaData.name(), + delegateType, metaData.settings())); + if (false == (delegatedRepository instanceof BlobStoreRepository)) { + throw new IllegalArgumentException("Unsupported type " + DELEGATE_TYPE.getKey()); + } + return new EncryptedRepository((BlobStoreRepository)delegatedRepository, secretKey); + } + }; + } + + private static class EncryptedBlobStoreDecorator implements BlobStore { + + private final BlobStore delegatedBlobStore; + private final SecretKey masterSecretKey; + + EncryptedBlobStoreDecorator(BlobStore blobStore, SecretKey masterSecretKey) { + this.delegatedBlobStore = blobStore; + this.masterSecretKey = masterSecretKey; + } + + @Override + public void close() throws IOException { + this.delegatedBlobStore.close(); + } + + @Override + public BlobContainer blobContainer(BlobPath path) { + BlobPath encryptionMetadataBlobPath = BlobPath.cleanPath(); + encryptionMetadataBlobPath = encryptionMetadataBlobPath.add(ENCRYPTION_METADATA_PREFIX + keyId(this.masterSecretKey)); + for (String pathComponent : path) { + encryptionMetadataBlobPath = encryptionMetadataBlobPath.add(pathComponent); + } + return new EncryptedBlobContainerDecorator(this.delegatedBlobStore.blobContainer(path), + this.delegatedBlobStore.blobContainer(encryptionMetadataBlobPath), this.masterSecretKey); + } + } + + private static class EncryptedBlobContainerDecorator implements BlobContainer { + + private final BlobContainer delegatedBlobContainer; + private final BlobContainer encryptionMetadataBlobContainer; + private final SecretKey masterSecretKey; + + EncryptedBlobContainerDecorator(BlobContainer delegatedBlobContainer, BlobContainer encryptionMetadataBlobContainer, + SecretKey masterSecretKey) { + this.delegatedBlobContainer = delegatedBlobContainer; + this.encryptionMetadataBlobContainer = encryptionMetadataBlobContainer; + this.masterSecretKey = masterSecretKey; + } + + @Override + public BlobPath path() { + return this.delegatedBlobContainer.path(); + } + + @Override + public InputStream readBlob(String blobName) throws IOException { + final BytesReference dataDecryptionKeyBytes = Streams.readFully(this.encryptionMetadataBlobContainer.readBlob(blobName)); + try { + SecretKey dataDecryptionKey = unwrapKey(BytesReference.toBytes(dataDecryptionKeyBytes), this.masterSecretKey); + Cipher cipher = Cipher.getInstance(ENCRYPTION_MODE, BC_FIPS_PROV); + cipher.init(Cipher.DECRYPT_MODE, dataDecryptionKey, ivParameterSpec); + return new CipherInputStream(this.delegatedBlobContainer.readBlob(blobName), cipher); + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) { + throw new IOException(e); + } + } + + @Override + public void writeBlob(String blobName, InputStream inputStream, long blobSize, boolean failIfAlreadyExists) throws IOException { + try { + SecretKey dataEncryptionKey = generateRandomSecretKey(); + byte[] wrappedDataEncryptionKey = wrapKey(dataEncryptionKey, this.masterSecretKey); + try (InputStream stream = new ByteArrayInputStream(wrappedDataEncryptionKey)) { + this.encryptionMetadataBlobContainer.writeBlob(blobName, stream, wrappedDataEncryptionKey.length, failIfAlreadyExists); + } + Cipher cipher = Cipher.getInstance(ENCRYPTION_MODE, BC_FIPS_PROV); + cipher.init(Cipher.ENCRYPT_MODE, dataEncryptionKey, ivParameterSpec); + this.delegatedBlobContainer.writeBlob(blobName, new CipherInputStream(inputStream, cipher), blobSize + GCM_TAG_BYTES_LENGTH, + failIfAlreadyExists); + } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | IllegalBlockSizeException + | InvalidAlgorithmParameterException e) { + throw new IOException(e); + } + } + + @Override + public void writeBlobAtomic(String blobName, InputStream inputStream, long blobSize, boolean failIfAlreadyExists) + throws IOException { + // does not support atomic write + writeBlob(blobName, inputStream, blobSize, failIfAlreadyExists); + } + + @Override + public void deleteBlob(String blobName) throws IOException { + this.delegatedBlobContainer.deleteBlob(blobName); + this.encryptionMetadataBlobContainer.deleteBlob(blobName); + } + + @Override + public DeleteResult delete() throws IOException { + DeleteResult result = this.delegatedBlobContainer.delete(); + this.encryptionMetadataBlobContainer.delete(); + return result; + } + + @Override + public Map listBlobs() throws IOException { + return this.delegatedBlobContainer.listBlobs(); + } + + @Override + public Map children() throws IOException { + return this.delegatedBlobContainer.children(); + } + + @Override + public Map listBlobsByPrefix(String blobNamePrefix) throws IOException { + Map delegatedBlobs = this.delegatedBlobContainer.listBlobsByPrefix(blobNamePrefix); + Map delegatedBlobsWithPlainSize = new HashMap<>(delegatedBlobs.size()); + for (Map.Entry entry : delegatedBlobs.entrySet()) { + delegatedBlobsWithPlainSize.put(entry.getKey(), new BlobMetaData() { + + @Override + public String name() { + return entry.getValue().name(); + } + + @Override + public long length() { + return entry.getValue().length() - GCM_TAG_BYTES_LENGTH; + } + }); + } + return delegatedBlobsWithPlainSize; + } + } + + private static SecretKey generateSecretKeyFromPassword(char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException { + byte[] salt = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; // same salt for 1:1 password to key + PBEKeySpec spec = new PBEKeySpec(password, salt, 65536, 256); + SecretKey tmp = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec); + return new SecretKeySpec(tmp.getEncoded(), "AES"); + } + + private static String keyId(SecretKey secretKey) { + return MessageDigests.toHexString(MessageDigests.sha256().digest(secretKey.getEncoded())); + } + + private static SecretKey generateRandomSecretKey() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES", BC_FIPS_PROV); + keyGen.init(256); + return keyGen.generateKey(); + } + + private static byte[] wrapKey(SecretKey toWrap, SecretKey keyWrappingKey) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance("AESWrap"); + cipher.init(Cipher.WRAP_MODE, keyWrappingKey); + return cipher.wrap(toWrap); + } + + private static SecretKey unwrapKey(byte[] toUnwrap, SecretKey keyEncryptionKey) + throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException { + Cipher cipher = Cipher.getInstance("AESWrap"); + cipher.init(Cipher.UNWRAP_MODE, keyEncryptionKey); + return (SecretKey) cipher.unwrap(toUnwrap, "AES", Cipher.SECRET_KEY); + } +} diff --git a/x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryPlugin.java b/x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryPlugin.java new file mode 100644 index 0000000000000..c2631cdafac18 --- /dev/null +++ b/x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryPlugin.java @@ -0,0 +1,45 @@ +/* + * 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.repositories.encrypted; + +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.env.Environment; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.ReloadablePlugin; +import org.elasticsearch.plugins.RepositoryPlugin; +import org.elasticsearch.repositories.Repository; +import org.elasticsearch.threadpool.ThreadPool; + +import java.util.List; +import java.util.Map; + +public class EncryptedRepositoryPlugin extends Plugin implements RepositoryPlugin, ReloadablePlugin { + + private final Repository.Factory encryptedRepositoryFactory; + + public EncryptedRepositoryPlugin(final Settings settings) { + encryptedRepositoryFactory = EncryptedRepository.newRepositoryFactory(settings); + } + + @Override + public Map getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry, + ThreadPool threadPool) { + return Map.of("encrypted", encryptedRepositoryFactory); + } + + @Override + public List> getSettings() { + return List.of(EncryptedRepository.ENCRYPTION_PASSWORD_SETTING); + } + + @Override + public void reload(Settings settings) { + // Secure settings should be readable inside this method. + } +} diff --git a/x-pack/plugin/repository-encrypted/src/main/plugin-metadata/plugin-security.policy b/x-pack/plugin/repository-encrypted/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000000000..2c9870c44dddf --- /dev/null +++ b/x-pack/plugin/repository-encrypted/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,9 @@ +/* + * 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. + */ + +grant { + permission java.security.SecurityPermission "putProviderProperty.BC"; +}; diff --git a/x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryTests.java b/x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryTests.java new file mode 100644 index 0000000000000..e65120d749fcd --- /dev/null +++ b/x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/EncryptedRepositoryTests.java @@ -0,0 +1,14 @@ +/* + * 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.repositories.encrypted; + +import org.elasticsearch.test.ESTestCase; + +public class EncryptedRepositoryTests extends ESTestCase { + public void testThatDoesNothing() { + } +} diff --git a/x-pack/plugin/repository-encrypted/src/test/resources/rest-api-spec/test/repository_encrypted/10_basic.yml b/x-pack/plugin/repository-encrypted/src/test/resources/rest-api-spec/test/repository_encrypted/10_basic.yml new file mode 100644 index 0000000000000..858ba3e21e3ae --- /dev/null +++ b/x-pack/plugin/repository-encrypted/src/test/resources/rest-api-spec/test/repository_encrypted/10_basic.yml @@ -0,0 +1,16 @@ +# Integration tests for repository-encrypted +# +"Plugin repository-encrypted is loaded": + - skip: + reason: "contains is a newly added assertion" + features: contains + - do: + cluster.state: {} + + # Get master node id + - set: { master_node: master } + + - do: + nodes.info: {} + + - contains: { nodes.$master.plugins: { name: repository-encrypted } }