BAEL-6046 MongoDB - Field Level Encryption (#13859)

This commit is contained in:
Ulisses Lima 2023-05-06 04:59:46 -03:00 committed by GitHub
parent 4a9e72664c
commit 7123ae8ad7
7 changed files with 150 additions and 46 deletions

View File

@ -16,6 +16,21 @@
</parent> </parent>
<dependencies> <dependencies>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>${mongodb-driver.version}</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-core</artifactId>
<version>${mongodb-driver.version}</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>bson</artifactId>
<version>${mongodb-driver.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
@ -23,6 +38,16 @@
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId> <artifactId>spring-boot-starter-data-mongodb</artifactId>
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
</exclusion>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-core</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.mongodb</groupId> <groupId>org.mongodb</groupId>
@ -37,7 +62,8 @@
</dependencies> </dependencies>
<properties> <properties>
<mongodb-crypt.version>1.6.1</mongodb-crypt.version> <mongodb-crypt.version>1.7.3</mongodb-crypt.version>
<mongodb-driver.version>4.9.1</mongodb-driver.version>
</properties> </properties>
</project> </project>

View File

@ -1,5 +1,7 @@
package com.baeldung.boot.csfle.config; package com.baeldung.boot.csfle.config;
import java.io.File;
import org.bson.BsonBinary; import org.bson.BsonBinary;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -17,7 +19,13 @@ public class EncryptionConfig {
private String keyVaultAlias; private String keyVaultAlias;
@Value("${com.baeldung.csfle.auto-decryption:false}") @Value("${com.baeldung.csfle.auto-decryption:false}")
private Boolean autoDecryption; private boolean autoDecryption;
@Value("${com.baeldung.csfle.auto-encryption:false}")
private boolean autoEncryption;
@Value("${com.baeldung.csfle.auto-encryption-lib:#{null}}")
private File autoEncryptionLib;
private BsonBinary dataKeyId; private BsonBinary dataKeyId;
@ -41,7 +49,23 @@ public class EncryptionConfig {
return masterKeyPath; return masterKeyPath;
} }
public Boolean getAutoDecryption() { public boolean isAutoDecryption() {
return autoDecryption; return autoDecryption;
} }
public boolean isAutoEncryption() {
return autoEncryption;
}
public File getAutoEncryptionLib() {
return autoEncryptionLib;
}
public String dataKeyIdUuid() {
if (dataKeyId == null)
throw new IllegalStateException("data key not initialized");
return dataKeyId.asUuid()
.toString();
}
} }

View File

@ -1,14 +1,15 @@
package com.baeldung.boot.csfle.config; package com.baeldung.boot.csfle.config;
import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.bson.BsonBinary; import org.bson.BsonBinary;
import org.bson.BsonDocument; import org.bson.BsonDocument;
import org.bson.Document; import org.bson.Document;
import org.bson.conversions.Bson;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -51,14 +52,10 @@ public class MongoClientConfig extends AbstractMongoClientConfiguration {
@Bean @Bean
@Override @Override
public MongoClient mongoClient() { public MongoClient mongoClient() {
MongoClient client;
try { try {
client = MongoClients.create(clientSettings());
ClientEncryption encryption = clientEncryption(); ClientEncryption encryption = clientEncryption();
encryptionConfig.setDataKeyId(createOrRetrieveDataKey(client, encryption)); encryptionConfig.setDataKeyId(createOrRetrieveDataKey(encryption));
return MongoClients.create(clientSettings());
return client;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalStateException("unable to create client", e); throw new IllegalStateException("unable to create client", e);
} }
@ -77,19 +74,10 @@ public class MongoClientConfig extends AbstractMongoClientConfiguration {
return ClientEncryptions.create(encryptionSettings); return ClientEncryptions.create(encryptionSettings);
} }
private BsonBinary createOrRetrieveDataKey(MongoClient client, ClientEncryption encryption) { private BsonBinary createOrRetrieveDataKey(ClientEncryption encryption) {
MongoNamespace namespace = new MongoNamespace(encryptionConfig.getKeyVaultNamespace()); BsonDocument key = encryption.getKeyByAltName(encryptionConfig.getKeyVaultAlias());
MongoCollection<Document> keyVault = client.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
Bson query = Filters.in("keyAltNames", encryptionConfig.getKeyVaultAlias());
BsonDocument key = keyVault.withDocumentClass(BsonDocument.class)
.find(query)
.first();
if (key == null) { if (key == null) {
keyVault.createIndex(Indexes.ascending("keyAltNames"), new IndexOptions().unique(true) createKeyUniqueIndex();
.partialFilterExpression(Filters.exists("keyAltNames")));
DataKeyOptions options = new DataKeyOptions(); DataKeyOptions options = new DataKeyOptions();
options.keyAltNames(Arrays.asList(encryptionConfig.getKeyVaultAlias())); options.keyAltNames(Arrays.asList(encryptionConfig.getKeyVaultAlias()));
@ -99,16 +87,68 @@ public class MongoClientConfig extends AbstractMongoClientConfiguration {
} }
} }
private void createKeyUniqueIndex() {
try (MongoClient client = MongoClients.create(MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(uri))
.build())) {
MongoNamespace namespace = new MongoNamespace(encryptionConfig.getKeyVaultNamespace());
MongoCollection<Document> keyVault = client.getDatabase(namespace.getDatabaseName())
.getCollection(namespace.getCollectionName());
keyVault.createIndex(Indexes.ascending("keyAltNames"), new IndexOptions().unique(true)
.partialFilterExpression(Filters.exists("keyAltNames")));
}
}
private MongoClientSettings clientSettings() throws FileNotFoundException, IOException { private MongoClientSettings clientSettings() throws FileNotFoundException, IOException {
Builder settings = MongoClientSettings.builder() Builder settings = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(uri)); .applyConnectionString(new ConnectionString(uri));
if (encryptionConfig.getAutoDecryption()) { if (encryptionConfig.isAutoDecryption()) {
settings.autoEncryptionSettings(AutoEncryptionSettings.builder() AutoEncryptionSettings.Builder builder = AutoEncryptionSettings.builder()
.keyVaultNamespace(encryptionConfig.getKeyVaultNamespace()) .keyVaultNamespace(encryptionConfig.getKeyVaultNamespace())
.kmsProviders(LocalKmsUtils.providersMap(encryptionConfig.getMasterKeyPath())) .kmsProviders(LocalKmsUtils.providersMap(encryptionConfig.getMasterKeyPath()));
.bypassAutoEncryption(true)
.build()); if (encryptionConfig.isAutoEncryption() && encryptionConfig.getDataKeyId() != null) {
File autoEncryptionLib = encryptionConfig.getAutoEncryptionLib();
if (!autoEncryptionLib.isFile()) {
throw new IllegalArgumentException("encryption lib must be an existing file");
}
Map<String, Object> map = new HashMap<>();
map.put("cryptSharedLibRequired", true);
map.put("cryptSharedLibPath", autoEncryptionLib.toString());
builder.extraOptions(map);
String keyUuid = encryptionConfig.dataKeyIdUuid();
HashMap<String, BsonDocument> schemaMap = new HashMap<>();
schemaMap.put(getDatabaseName() + ".citizens",
BsonDocument.parse("{"
+ " bsonType: \"object\","
+ " encryptMetadata: {"
+ " keyId: [UUID(\"" + keyUuid + "\")]"
+ " },"
+ " properties: {"
+ " email: {"
+ " encrypt: {"
+ " bsonType: \"string\","
+ " algorithm: \"AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic\""
+ " }"
+ " },"
+ " birthYear: {"
+ " encrypt: {"
+ " bsonType: \"int\","
+ " algorithm: \"AEAD_AES_256_CBC_HMAC_SHA_512-Random\""
+ " }"
+ " }"
+ " }"
+ "}"));
builder.schemaMap(schemaMap);
} else {
builder.bypassAutoEncryption(true);
}
settings.autoEncryptionSettings(builder.build());
} }
return settings.build(); return settings.build();

View File

@ -35,16 +35,20 @@ public class CitizenService {
@Autowired @Autowired
private ClientEncryption clientEncryption; private ClientEncryption clientEncryption;
public EncryptedCitizen save(Citizen citizen) { public Object save(Citizen citizen) {
EncryptedCitizen encryptedCitizen = new EncryptedCitizen(citizen); if (encryptionConfig.isAutoEncryption()) {
encryptedCitizen.setEmail(encrypt(citizen.getEmail(), DETERMINISTIC_ALGORITHM)); return mongo.save(citizen);
encryptedCitizen.setBirthYear(encrypt(citizen.getBirthYear(), RANDOM_ALGORITHM)); } else {
EncryptedCitizen encryptedCitizen = new EncryptedCitizen(citizen);
encryptedCitizen.setEmail(encrypt(citizen.getEmail(), DETERMINISTIC_ALGORITHM));
encryptedCitizen.setBirthYear(encrypt(citizen.getBirthYear(), RANDOM_ALGORITHM));
return mongo.save(encryptedCitizen); return mongo.save(encryptedCitizen);
}
} }
public List<Citizen> findAll() { public List<Citizen> findAll() {
if (!encryptionConfig.getAutoDecryption()) { if (!encryptionConfig.isAutoDecryption()) {
List<EncryptedCitizen> allEncrypted = mongo.findAll(EncryptedCitizen.class); List<EncryptedCitizen> allEncrypted = mongo.findAll(EncryptedCitizen.class);
return allEncrypted.stream() return allEncrypted.stream()
@ -56,13 +60,20 @@ public class CitizenService {
} }
public Citizen findByEmail(String email) { public Citizen findByEmail(String email) {
Query byEmail = new Query(Criteria.where("email") Criteria emailCriteria = Criteria.where("email");
.is(encrypt(email, DETERMINISTIC_ALGORITHM))); if (encryptionConfig.isAutoEncryption()) {
if (!encryptionConfig.getAutoDecryption()) { emailCriteria.is(email);
} else {
emailCriteria
.is(encrypt(email, DETERMINISTIC_ALGORITHM));
}
Query byEmail = new Query(emailCriteria);
if (encryptionConfig.isAutoDecryption()) {
return mongo.findOne(byEmail, Citizen.class);
} else {
EncryptedCitizen encryptedCitizen = mongo.findOne(byEmail, EncryptedCitizen.class); EncryptedCitizen encryptedCitizen = mongo.findOne(byEmail, EncryptedCitizen.class);
return decrypt(encryptedCitizen); return decrypt(encryptedCitizen);
} else {
return mongo.findOne(byEmail, Citizen.class);
} }
} }

View File

@ -11,7 +11,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.baeldung.boot.csfle.data.Citizen; import com.baeldung.boot.csfle.data.Citizen;
import com.baeldung.boot.csfle.data.EncryptedCitizen;
import com.baeldung.boot.csfle.service.CitizenService; import com.baeldung.boot.csfle.service.CitizenService;
@RestController @RestController
@ -32,7 +31,7 @@ public class CitizenController {
} }
@PostMapping @PostMapping
public EncryptedCitizen post(@RequestBody Citizen citizen) { public Object post(@RequestBody Citizen citizen) {
return service.save(citizen); return service.save(citizen);
} }
} }

View File

@ -38,10 +38,14 @@ public class CitizenServiceLiveTest {
citizen.setName("Foo"); citizen.setName("Foo");
citizen.setEmail("foo@citizen.com"); citizen.setEmail("foo@citizen.com");
Binary encryptedEmail = service.encrypt(citizen.getEmail(), CitizenService.DETERMINISTIC_ALGORITHM); Object saved = service.save(citizen);
if (saved instanceof EncryptedCitizen) {
Binary encryptedEmail = service.encrypt(citizen.getEmail(), CitizenService.DETERMINISTIC_ALGORITHM);
EncryptedCitizen saved = service.save(citizen); assertEquals(encryptedEmail, ((EncryptedCitizen) saved).getEmail());
assertEquals(encryptedEmail, saved.getEmail()); } else {
assertEquals(citizen.getEmail(), ((Citizen) saved).getEmail());
}
} }
@Test @Test

View File

@ -1,10 +1,10 @@
spring.mongodb.embedded.version=4.4.9 spring.mongodb.embedded.version=4.4.9
spring.data.mongodb.uri=changeit #spring.data.mongodb.uri=changeit
spring.data.mongodb.database=changeit #spring.data.mongodb.database=changeit
com.baeldung.csfle.kms-provider=local com.baeldung.csfle.kms-provider=local
com.baeldung.csfle.key-vault.namespace=encryption._keyVault com.baeldung.csfle.key-vault.namespace=encryption._keyVault
com.baeldung.csfle.key-vault.alias=master.key com.baeldung.csfle.key-vault.alias=master.key
com.baeldung.csfle.master-key-path=/tmp/master.key #com.baeldung.csfle.master-key-path=/tmp/master.key
com.baeldung.csfle.auto-decryption=false com.baeldung.csfle.auto-decryption=false