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

* bael-6046 - first draft

* review 1

* review 2

* null checks and lambda
This commit is contained in:
Ulisses Lima 2023-02-15 17:52:25 -03:00 committed by GitHub
parent 1d860a8e67
commit fc9a23a02a
6 changed files with 85 additions and 90 deletions

View File

@ -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;
} }

View File

@ -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));

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
} }
} }