From 0361c3acc970d24cc58073ca243ca0170190ba98 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 11 Nov 2022 09:09:05 +0100 Subject: [PATCH] Hacking --- spring-data-mongodb/pom.xml | 7 + .../data/mongodb/fle/FLETests.java | 284 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index fb36fdee2..f716a89fc 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -112,6 +112,13 @@ true + + org.mongodb + mongodb-crypt + 1.6.1 + true + + io.projectreactor reactor-core diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java new file mode 100644 index 000000000..e305e14a0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.data.mongodb.fle; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; + +import lombok.Data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.bson.BsonBinary; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.types.Binary; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.convert.PropertyValueConverterFactory; +import org.springframework.data.convert.ValueConverter; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; +import org.springframework.data.mongodb.core.convert.MongoValueConverter; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.fle.FLETests.Config; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.util.Lazy; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +/** + * @author Christoph Strobl + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = Config.class) +public class FLETests { + + @Autowired MongoTemplate template; + + @Test + void manualEnAndDecryption() { + + Person person = new Person(); + person.id = "id-1"; + person.name = "p1-name"; + person.ssn = "mySecretSSN"; // determinisitc encryption (queryable) + person.wallet = "myEvenMoreSecretStuff"; // random encryption (non queryable) + + template.save(person); + + System.out.println("source: " + person); + + Document savedDocument = template.execute(Person.class, collection -> { + return collection.find(new Document()).first(); + }); + + // ssn should look like "ssn": {"$binary": {"base64": "... + System.out.println("saved: " + savedDocument.toJson()); + assertThat(savedDocument.get("ssn")).isInstanceOf(org.bson.types.Binary.class); + assertThat(savedDocument.get("wallet")).isInstanceOf(org.bson.types.Binary.class); + + // count should be 1 using a deterministic algorithm + long queryCount = template.query(Person.class).matching(where("ssn").is(person.ssn)).count(); + System.out.println("query(count): " + queryCount); + assertThat(queryCount).isOne(); + + Person bySsn = template.query(Person.class).matching(where("ssn").is(person.ssn)).firstValue(); + System.out.println("queryable: " + bySsn); + assertThat(bySsn).isEqualTo(person); + + Person byWallet = template.query(Person.class).matching(where("wallet").is(person.wallet)).firstValue(); + System.out.println("not-queryable: " + byWallet); + assertThat(byWallet).isNull(); + } + + @Configuration + static class Config extends AbstractMongoClientConfiguration { + + @Autowired ApplicationContext applicationContext; + + @Override + protected String getDatabaseName() { + return "fle-test"; + } + + @Bean + public MongoClient mongoClient() { + return super.mongoClient(); + } + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) { + + converterConfigurationAdapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext)); + } + + @Bean + EncryptingConverter encryptingConverter(ClientEncryption clientEncryption) { + return new EncryptingConverter(clientEncryption); + } + + @Bean + ClientEncryption clientEncryption(MongoClient mongoClient) { + + final byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + Map> kmsProviders = new HashMap>() { + { + put("local", new HashMap() { + { + put("key", localMasterKey); + } + }); + } + }; + + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames"))); + + MongoCollection collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test"); + collection.drop(); // Clear old data + + // Create the ClientEncryption instance + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings( + MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) + .keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build(); + ClientEncryption clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + return clientEncryption; + } + } + + @Data + @org.springframework.data.mongodb.core.mapping.Document("test") + static class Person { + + String id; + String name; + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + String ssn; + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // + String wallet; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Encrypted + @ValueConverter(EncryptingConverter.class) + @interface EncryptedField { + + @AliasFor(annotation = Encrypted.class, value = "algorithm") + String algorithm() default ""; + + String altKeyName() default ""; + } + + static class EncryptingConverter implements MongoValueConverter { + + private ClientEncryption clientEncryption; + private BsonBinary dataKeyId; // should be provided from outside. + + public EncryptingConverter(ClientEncryption clientEncryption) { + + this.clientEncryption = clientEncryption; + this.dataKeyId = clientEncryption.createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))); + } + + @Nullable + @Override + public Object read(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + Object decrypted = encryptionContext.decrypt(value, clientEncryption); + return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; + } + + @Nullable + @Override + public BsonBinary write(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + return encryptionContext.encrypt(value, clientEncryption); + } + + ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { + return new ManualEncryptionContext(context.getProperty(), this.dataKeyId); + } + } + + static class ManualEncryptionContext { + + MongoPersistentProperty persistentProperty; + BsonBinary dataKeyId; + Lazy encryption; + + public ManualEncryptionContext(MongoPersistentProperty persistentProperty, BsonBinary dataKeyId) { + this.persistentProperty = persistentProperty; + this.dataKeyId = dataKeyId; + this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); + } + + BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { + + // TODO: check - encryption.get().keyId() + + EncryptOptions encryptOptions = new EncryptOptions(encryption.get().algorithm()); + + EncryptedField annotation = persistentProperty.findAnnotation(EncryptedField.class); + if (annotation != null && !annotation.altKeyName().isBlank()) { + encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + } else { + encryptOptions = encryptOptions.keyId(this.dataKeyId); + } + + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + } + + public Object decrypt(Object value, ClientEncryption clientEncryption) { + + if (value instanceof Binary binary) { + return clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); + } + if (value instanceof BsonBinary binary) { + return clientEncryption.decrypt(binary); + } + + // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java + return value; + } + } +}