BAEL-6046 - MongoDB - Field Level Encryption (#13440)
* bael-6046 - first draft * review 1 * review 2 * null checks and lambda
This commit is contained in:
parent
1d860a8e67
commit
fc9a23a02a
|
@ -4,8 +4,6 @@ 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;
|
||||||
|
|
||||||
import com.mongodb.client.vault.ClientEncryption;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class EncryptionConfig {
|
public class EncryptionConfig {
|
||||||
|
|
||||||
|
@ -21,18 +19,8 @@ public class EncryptionConfig {
|
||||||
@Value("${com.baeldung.csfle.auto-decryption:false}")
|
@Value("${com.baeldung.csfle.auto-decryption:false}")
|
||||||
private Boolean autoDecryption;
|
private Boolean autoDecryption;
|
||||||
|
|
||||||
private ClientEncryption encryption;
|
|
||||||
|
|
||||||
private BsonBinary dataKeyId;
|
private BsonBinary dataKeyId;
|
||||||
|
|
||||||
public void setEncryption(ClientEncryption encryption) {
|
|
||||||
this.encryption = encryption;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ClientEncryption getEncryption() {
|
|
||||||
return encryption;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDataKeyId(BsonBinary dataKeyId) {
|
public void setDataKeyId(BsonBinary dataKeyId) {
|
||||||
this.dataKeyId = dataKeyId;
|
this.dataKeyId = dataKeyId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,12 @@ import org.bson.Document;
|
||||||
import org.bson.conversions.Bson;
|
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.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
|
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
|
||||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
||||||
|
|
||||||
import com.baeldung.boot.csfle.config.converter.IntegerConverter;
|
import com.baeldung.boot.csfle.config.converter.BinaryConverter;
|
||||||
import com.baeldung.boot.csfle.config.converter.StringConverter;
|
|
||||||
import com.mongodb.AutoEncryptionSettings;
|
import com.mongodb.AutoEncryptionSettings;
|
||||||
import com.mongodb.ClientEncryptionSettings;
|
import com.mongodb.ClientEncryptionSettings;
|
||||||
import com.mongodb.ConnectionString;
|
import com.mongodb.ConnectionString;
|
||||||
|
@ -52,16 +52,17 @@ public class MongoClientConfig extends AbstractMongoClientConfiguration {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MongoCustomConversions customConversions() {
|
public MongoCustomConversions customConversions() {
|
||||||
return new MongoCustomConversions(Arrays.asList(new StringConverter(encryptionConfig), new IntegerConverter(encryptionConfig)));
|
return new MongoCustomConversions(Arrays.asList(new BinaryConverter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
@Override
|
@Override
|
||||||
public MongoClient mongoClient() {
|
public MongoClient mongoClient() {
|
||||||
MongoClient client;
|
MongoClient client;
|
||||||
try {
|
try {
|
||||||
client = MongoClients.create(clientSettings());
|
client = MongoClients.create(clientSettings());
|
||||||
|
|
||||||
ClientEncryption encryption = createClientEncryption();
|
ClientEncryption encryption = clientEncryption();
|
||||||
encryptionConfig.setDataKeyId(createOrRetrieveDataKey(client, encryption));
|
encryptionConfig.setDataKeyId(createOrRetrieveDataKey(client, encryption));
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
@ -70,6 +71,19 @@ public class MongoClientConfig extends AbstractMongoClientConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ClientEncryption clientEncryption() throws FileNotFoundException, IOException {
|
||||||
|
Map<String, Map<String, Object>> kmsProviders = LocalKmsUtils.providersMap(encryptionConfig.getMasterKeyPath());
|
||||||
|
|
||||||
|
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder()
|
||||||
|
.keyVaultMongoClientSettings(clientSettings())
|
||||||
|
.keyVaultNamespace(encryptionConfig.getKeyVaultNamespace())
|
||||||
|
.kmsProviders(kmsProviders)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return ClientEncryptions.create(encryptionSettings);
|
||||||
|
}
|
||||||
|
|
||||||
private BsonBinary createOrRetrieveDataKey(MongoClient client, ClientEncryption encryption) {
|
private BsonBinary createOrRetrieveDataKey(MongoClient client, ClientEncryption encryption) {
|
||||||
MongoNamespace namespace = new MongoNamespace(encryptionConfig.getKeyVaultNamespace());
|
MongoNamespace namespace = new MongoNamespace(encryptionConfig.getKeyVaultNamespace());
|
||||||
MongoCollection<Document> keyVault = client.getDatabase(namespace.getDatabaseName())
|
MongoCollection<Document> keyVault = client.getDatabase(namespace.getDatabaseName())
|
||||||
|
@ -92,19 +106,6 @@ public class MongoClientConfig extends AbstractMongoClientConfiguration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientEncryption createClientEncryption() throws FileNotFoundException, IOException {
|
|
||||||
Map<String, Map<String, Object>> kmsProviders = LocalKmsUtils.providersMap(encryptionConfig.getMasterKeyPath());
|
|
||||||
|
|
||||||
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder()
|
|
||||||
.keyVaultMongoClientSettings(clientSettings())
|
|
||||||
.keyVaultNamespace(encryptionConfig.getKeyVaultNamespace())
|
|
||||||
.kmsProviders(kmsProviders)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
encryptionConfig.setEncryption(ClientEncryptions.create(encryptionSettings));
|
|
||||||
return encryptionConfig.getEncryption();
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.baeldung.boot.csfle.config.converter;
|
||||||
|
|
||||||
|
import org.bson.BsonBinary;
|
||||||
|
import org.bson.types.Binary;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
|
||||||
|
public class BinaryConverter implements Converter<Binary, BsonBinary> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BsonBinary convert(Binary source) {
|
||||||
|
return new BsonBinary(source.getType(), source.getData());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
package com.baeldung.boot.csfle.config.converter;
|
|
||||||
|
|
||||||
import org.bson.BsonBinary;
|
|
||||||
import org.bson.BsonValue;
|
|
||||||
import org.bson.types.Binary;
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
|
||||||
|
|
||||||
import com.baeldung.boot.csfle.config.EncryptionConfig;
|
|
||||||
|
|
||||||
public class IntegerConverter implements Converter<Binary, Integer> {
|
|
||||||
|
|
||||||
private EncryptionConfig encryptionConfig;
|
|
||||||
|
|
||||||
public IntegerConverter(EncryptionConfig config) {
|
|
||||||
this.encryptionConfig = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer convert(Binary source) {
|
|
||||||
BsonBinary bin = new BsonBinary(source.getType(), source.getData());
|
|
||||||
BsonValue value = encryptionConfig.getEncryption()
|
|
||||||
.decrypt(bin);
|
|
||||||
|
|
||||||
return value.asInt32()
|
|
||||||
.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
package com.baeldung.boot.csfle.config.converter;
|
|
||||||
|
|
||||||
import org.bson.BsonBinary;
|
|
||||||
import org.bson.BsonValue;
|
|
||||||
import org.bson.types.Binary;
|
|
||||||
import org.springframework.core.convert.converter.Converter;
|
|
||||||
|
|
||||||
import com.baeldung.boot.csfle.config.EncryptionConfig;
|
|
||||||
|
|
||||||
public class StringConverter implements Converter<Binary, String> {
|
|
||||||
|
|
||||||
private EncryptionConfig encryptionConfig;
|
|
||||||
|
|
||||||
public StringConverter(EncryptionConfig config) {
|
|
||||||
this.encryptionConfig = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String convert(Binary source) {
|
|
||||||
BsonBinary bin = new BsonBinary(source.getType(), source.getData());
|
|
||||||
BsonValue value = encryptionConfig.getEncryption()
|
|
||||||
.decrypt(bin);
|
|
||||||
|
|
||||||
return value.asString()
|
|
||||||
.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.baeldung.boot.csfle.service;
|
package com.baeldung.boot.csfle.service;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.bson.BsonBinary;
|
import org.bson.BsonBinary;
|
||||||
import org.bson.BsonInt32;
|
import org.bson.BsonInt32;
|
||||||
|
@ -16,6 +17,7 @@ import com.baeldung.boot.csfle.config.EncryptionConfig;
|
||||||
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.data.EncryptedCitizen;
|
||||||
import com.mongodb.client.model.vault.EncryptOptions;
|
import com.mongodb.client.model.vault.EncryptOptions;
|
||||||
|
import com.mongodb.client.vault.ClientEncryption;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CitizenService {
|
public class CitizenService {
|
||||||
|
@ -29,6 +31,9 @@ public class CitizenService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private EncryptionConfig encryptionConfig;
|
private EncryptionConfig encryptionConfig;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ClientEncryption clientEncryption;
|
||||||
|
|
||||||
public EncryptedCitizen save(Citizen citizen) {
|
public EncryptedCitizen save(Citizen citizen) {
|
||||||
EncryptedCitizen encryptedCitizen = new EncryptedCitizen(citizen);
|
EncryptedCitizen encryptedCitizen = new EncryptedCitizen(citizen);
|
||||||
encryptedCitizen.setEmail(encrypt(citizen.getEmail(), DETERMINISTIC_ALGORITHM));
|
encryptedCitizen.setEmail(encrypt(citizen.getEmail(), DETERMINISTIC_ALGORITHM));
|
||||||
|
@ -38,26 +43,68 @@ public class CitizenService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Citizen> findAll() {
|
public List<Citizen> findAll() {
|
||||||
|
if (!encryptionConfig.getAutoDecryption()) {
|
||||||
|
List<EncryptedCitizen> allEncrypted = mongo.findAll(EncryptedCitizen.class);
|
||||||
|
|
||||||
|
return allEncrypted.stream()
|
||||||
|
.map(this::decrypt)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
return mongo.findAll(Citizen.class);
|
return mongo.findAll(Citizen.class);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Citizen findByEmail(String email) {
|
public Citizen findByEmail(String email) {
|
||||||
Query byEmail = new Query(Criteria.where("email")
|
Query byEmail = new Query(Criteria.where("email")
|
||||||
.is(encrypt(email, DETERMINISTIC_ALGORITHM)));
|
.is(encrypt(email, DETERMINISTIC_ALGORITHM)));
|
||||||
|
if (!encryptionConfig.getAutoDecryption()) {
|
||||||
|
EncryptedCitizen encryptedCitizen = mongo.findOne(byEmail, EncryptedCitizen.class);
|
||||||
|
return decrypt(encryptedCitizen);
|
||||||
|
} else {
|
||||||
return mongo.findOne(byEmail, Citizen.class);
|
return mongo.findOne(byEmail, Citizen.class);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public BsonBinary encrypt(Object value, String algorithm) {
|
public BsonBinary encrypt(Object value, String algorithm) {
|
||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
BsonValue bsonValue = value instanceof Integer
|
BsonValue bsonValue;
|
||||||
? new BsonInt32((Integer) value)
|
if (value instanceof Integer) {
|
||||||
: new BsonString(value.toString());
|
bsonValue = new BsonInt32((Integer) value);
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
bsonValue = new BsonString((String) value);
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("unsupported type: " + value.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
EncryptOptions options = new EncryptOptions(algorithm);
|
EncryptOptions options = new EncryptOptions(algorithm);
|
||||||
options.keyId(encryptionConfig.getDataKeyId());
|
options.keyId(encryptionConfig.getDataKeyId());
|
||||||
return encryptionConfig.getEncryption()
|
return clientEncryption.encrypt(bsonValue, options);
|
||||||
.encrypt(bsonValue, options);
|
}
|
||||||
|
|
||||||
|
public BsonValue decryptProperty(BsonBinary value) {
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return clientEncryption.decrypt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Citizen decrypt(EncryptedCitizen encrypted) {
|
||||||
|
Citizen citizen = new Citizen(encrypted);
|
||||||
|
|
||||||
|
BsonValue decryptedBirthYear = decryptProperty(encrypted.getBirthYear());
|
||||||
|
if (decryptedBirthYear != null) {
|
||||||
|
citizen.setBirthYear(decryptedBirthYear.asInt32()
|
||||||
|
.intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
BsonValue decryptedEmail = decryptProperty(encrypted.getEmail());
|
||||||
|
if (decryptedEmail != null) {
|
||||||
|
citizen.setEmail(decryptedEmail.asString()
|
||||||
|
.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return citizen;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue