BAEL-6046 MongoDB - Field Level Encryption (#13859)
This commit is contained in:
parent
4a9e72664c
commit
7123ae8ad7
@ -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>
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user