Skip to content
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

Encrypted blob store repository - take I #46170

Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1b648a7
POC
albertzaharovits Aug 20, 2019
e48c568
EncryptedRepository lifecycle
albertzaharovits Aug 20, 2019
61e4f2a
encryptionMetadataBlobPath
albertzaharovits Aug 20, 2019
5b7f5ea
Almost...
albertzaharovits Aug 21, 2019
23bcd23
Done???
albertzaharovits Aug 22, 2019
9e50384
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Aug 22, 2019
f7ac3ed
Nit
albertzaharovits Aug 22, 2019
d892c2c
WORKS!
albertzaharovits Aug 23, 2019
5f8d77b
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Aug 26, 2019
ad6f14a
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Aug 26, 2019
f1a44de
Chunk size
albertzaharovits Aug 26, 2019
7b3eb4d
SunJCE mrrrr
albertzaharovits Aug 29, 2019
a54513c
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Sep 1, 2019
43087e5
Always failIfExists for encryption metadata
albertzaharovits Sep 2, 2019
c160245
Parameterize for provider and chunk size
albertzaharovits Sep 2, 2019
85b1803
compile oversight
albertzaharovits Sep 2, 2019
7345c9b
License
albertzaharovits Sep 2, 2019
5fd4e61
Adjust sizes
albertzaharovits Sep 2, 2019
24378fc
Refactoring in a new plugin WIP
albertzaharovits Sep 3, 2019
c1649c8
Works!
albertzaharovits Sep 4, 2019
411f5da
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Sep 12, 2019
ab8d6c6
Changes to move encrypted snapshots code to x-pack module
Sep 6, 2019
5e75538
Merge branch 'encrypted-repo-poc' of github.com:albertzaharovits/elas…
albertzaharovits Oct 3, 2019
de4aeb9
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Oct 3, 2019
69fc7e5
FIPS libs
albertzaharovits Oct 7, 2019
b483b57
Straight GCM but with the bc-fips lib
albertzaharovits Oct 9, 2019
8345c3a
Merge branch 'master' into encrypted-repo-poc
albertzaharovits Oct 9, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp

protected final ChecksumBlobStoreFormat<SnapshotInfo> snapshotFormat;

private final NamedXContentRegistry namedXContentRegistry;

private final boolean readOnly;

private final ChecksumBlobStoreFormat<BlobStoreIndexShardSnapshot> indexShardSnapshotFormat;
Expand All @@ -198,6 +200,10 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp

private final BlobPath basePath;

protected BlobStoreRepository(BlobStoreRepository other) {
this(other.metadata, other.settings, other.namedXContentRegistry, other.threadPool, other.basePath);
}

/**
* Constructs new BlobStoreRepository
* @param metadata The metadata for this repository including name and settings
Expand All @@ -214,6 +220,7 @@ protected BlobStoreRepository(RepositoryMetaData metadata, Settings settings, Na
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.snapshots;

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.Setting;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.repositories.Repository;
import org.elasticsearch.repositories.blobstore.BlobStoreRepository;

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.Map;
import java.util.function.Function;

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.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class EncryptedRepository extends BlobStoreRepository {

private static final Setting<String> DELEGATE_TYPE = new Setting<>("delegate_type", "", Function.identity(),
Setting.Property.NodeScope);
private static final Setting<String> PASSWORD = new Setting<>("password", "", Function.identity(),
Setting.Property.NodeScope);
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 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();
}

protected ByteSizeValue chunkSize() {
return ByteSizeValue.parseBytesSizeValue("16mb", "encrypted blob store repository max chunk size");
}

private static SecretKey generateSecretKeyFromPassword(String 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.toCharArray(), 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");
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);
}

/**
* Returns a new encrypted repository factory
*/
public static Repository.Factory newRepositoryFactory() {
return new Repository.Factory() {

@Override
public Repository create(RepositoryMetaData metadata) {
throw new UnsupportedOperationException();
}

@Override
public Repository create(RepositoryMetaData metaData, Function<String, Repository.Factory> typeLookup) throws Exception {
String delegateType = DELEGATE_TYPE.get(metaData.settings());
if (Strings.hasLength(delegateType) == false) {
throw new IllegalArgumentException(DELEGATE_TYPE.getKey() + " must be set");
}
String password = PASSWORD.get(metaData.settings());
if (Strings.hasLength(password) == false) {
throw new IllegalArgumentException(PASSWORD.getKey() + " must be set");
}
SecretKey secretKey = generateSecretKeyFromPassword(password);
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("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, dataDecryptionKey, gcmParameterSpec);
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("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, dataEncryptionKey, gcmParameterSpec);
this.delegatedBlobContainer.writeBlob(blobName, new CipherInputStream(inputStream, cipher), blobSize, failIfAlreadyExists);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems wrong (and like it'll be a problem potentially!). The blobSize for the purposes of cloud stores like S3 must be the exact number of bytes that will be written. This currently works with the FsRepository because it doesn't use the size here, but it will write partial (since the encrypted bytes are more than the unencrypted) data with S3 and such.

Can we guess the size up front here in some form (via padding magic or so ... I'm admittedly not that knowledgeable here)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! We can compute the cyphertext length from the padding and mode we use. It's not complicated. I will make the changes.

} 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<String, BlobMetaData> listBlobs() throws IOException {
return this.delegatedBlobContainer.listBlobs();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work so easily. We need the correct metadata be returned for each blob here (i.e. the correct size, but now it returns the encrypted size)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll make the length computation in this case as well. Thanks!

}

@Override
public Map<String, BlobContainer> children() throws IOException {
return this.delegatedBlobContainer.children();
}

@Override
public Map<String, BlobMetaData> listBlobsByPrefix(String blobNamePrefix) throws IOException {
return this.delegatedBlobContainer.listBlobsByPrefix(blobNamePrefix);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.snapshots.EncryptedRepository;
import org.elasticsearch.snapshots.SourceOnlySnapshotRepository;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
Expand All @@ -76,7 +77,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;
Expand Down Expand Up @@ -313,7 +313,8 @@ public static Path resolveConfigFile(Environment env, String name) {
@Override
public Map<String, Repository.Factory> getRepositories(Environment env, NamedXContentRegistry namedXContentRegistry,
ThreadPool threadPool) {
return Collections.singletonMap("source", SourceOnlySnapshotRepository.newRepositoryFactory());
return Map.of("source", SourceOnlySnapshotRepository.newRepositoryFactory(),
"encrypted", EncryptedRepository.newRepositoryFactory());
}

@Override
Expand Down