项目作者: bolcom

项目描述 :
Lightweight library for simple & easy per-field encryption in mongodb+spring
高级语言: Java
项目地址: git://github.com/bolcom/spring-data-mongodb-encrypt.git
创建时间: 2017-10-06T07:43:16Z
项目社区:https://github.com/bolcom/spring-data-mongodb-encrypt

开源协议:Apache License 2.0

下载


Maven Central
Build

spring-data-mongodb-encrypt

Allows any field to be marked with @Encrypted for per-field encryption.

Features

  • integrates transparently into spring-data-mongodb
  • supports nested Collections, Maps and beans
  • high performance (no reflection, optimized encryption)
  • key versioning (to help migrating to new key without need to convert data)
  • supports 256-bit AES out of the box
  • supports any encryption available in Java (via JCE)
  • simple (cca. 500 lines of code)
  • tested throughly
  • no dependencies

Backwards compatibility

For spring-data 1 projects, please use the spring-data-1 branch.

For spring-data 2 projects, please use the spring-data-2 branch.

From version 2.9.0, java 17 or higher is required. If stuck on older java, use version 2.8.0.

For the impatient

Add dependency:

  1. <dependency>
  2. <groupId>com.bol</groupId>
  3. <artifactId>spring-data-mongodb-encrypt</artifactId>
  4. <version>2.9.1</version>
  5. </dependency>

And add the following to your application.yml:

  1. mongodb.encrypt:
  2. keys:
  3. - version: 1
  4. key: hqHKBLV83LpCqzKpf8OvutbCs+O5wX5BPu3btWpEvXA=

And you’re done!

Example usage:

  1. @Document
  2. public class MyBean {
  3. @Id
  4. public String id;
  5. // not encrypted
  6. @Field
  7. public String nonSensitiveData;
  8. // encrypted primitive types
  9. @Field
  10. @Encrypted
  11. public String secretString;
  12. @Field
  13. @Encrypted
  14. public Long secretLong;
  15. // encrypted sub-document (MySubBean is serialized, encrypted and stored as byte[])
  16. @Field
  17. @Encrypted
  18. public MySubBean secretSubBean;
  19. // encrypted collection (list is serialized, encrypted and stored as byte[])
  20. @Field
  21. @Encrypted
  22. public List<String> secretStringList;
  23. // values containing @Encrypted fields are encrypted
  24. @Field
  25. public MySubBean nonSensitiveSubBean;
  26. // values containing @Encrypted fields are encrypted
  27. @Field
  28. public List<MySubBean> nonSensitiveSubBeanList;
  29. // encrypted map (values containing @Encrypted fields are replaced by encrypted byte[])
  30. @Field
  31. public Map<String, MySubBean> publicMapWithSecretParts;
  32. }
  33. public class MySubBean {
  34. @Field
  35. public String nonSensitiveData;
  36. @Field
  37. @Encrypted
  38. public String secretString;
  39. }

Example result in mongodb:

  1. > db.mybean.find().pretty()
  2. {
  3. "_id" : ObjectId("59ea0fb902da8d61252b9988"),
  4. "_class" : "com.bol.secure.MyBean",
  5. "nonSensitiveSubBeanList" : [
  6. {
  7. "nonSensitiveData" : "sky is blue",
  8. "secretString" : BinData(0,"gJNJl3Eij5hX/dJeVgJ/eATIQqahYfUxg89wtKjZL1zxL5h4PTqGqjjn4HbBXbAibw==")
  9. },
  10. {
  11. "nonSensitiveData" : "grass is green",
  12. "secretString" : BinData(0,"gL+HVZ/OtbESNtL5yWgEYVv0rhT4gdOwYFs7zKx6WGEr1dq3jj84Sq+VhQKl4EthJg==")
  13. }
  14. ]
  15. }

Manual configuration

An example if you need to manually configure spring (see also how tests set up spring mongodb context):

  1. @Configuration
  2. public class MongoDBConfiguration extends AbstractMongoClientConfiguration {
  3. // normally you would use @Value to wire a property here
  4. private static final byte[] secretKey = Base64.getDecoder().decode("hqHKBLV83LpCqzKpf8OvutbCs+O5wX5BPu3btWpEvXA=");
  5. private static final byte[] oldKey = Base64.getDecoder().decode("cUzurmCcL+K252XDJhhWI/A/+wxYXLgIm678bwsE2QM=");
  6. @Override
  7. protected String getDatabaseName() {
  8. return "test";
  9. }
  10. @Override
  11. public MongoClient mongoClient() {
  12. return MongoClients.create();
  13. }
  14. @Bean
  15. public CryptVault cryptVault() {
  16. return new CryptVault()
  17. .with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(0, oldKey)
  18. .with256BitAesCbcPkcs5PaddingAnd128BitSaltKey(1, secretKey)
  19. // can be omitted if it's the highest version
  20. .withDefaultKeyVersion(1);
  21. }
  22. @Bean
  23. public CachedEncryptionEventListener encryptionEventListener(CryptVault cryptVault) {
  24. return new CachedEncryptionEventListener(cryptVault);
  25. }
  26. }

Starting mongodb for development (docker)

A docker-compose.yml file is provided to allow for quickly running the tests and/or prototyping.
Use with docker compose up.

Polymorphism (and why it’s bad)

spring-data-mongodb supports polymorphism via a rather questionable mechanism: when the nested bean’s type is not deductable from the java generic type, it would simply place an _class field in the document to specify the fully qualified class name for deserialization.
This has some very serious drawbacks:

  • Your database becomes tightly coupled with your java code. E.g., you can’t just use another code base to access the database, or during refactoring java code, you will have to take extra steps to keep it backwards compatible. Even just changing a java class name or moving it to another package would fail.

  • Storing the fully qualified class name in each subdocument results in a database size increase, up to 10x in worst-case scenario. It also pollutes the object structure, making it harder to read your domain data when examining the database manually.

  • Exposing class names and their properties also has some security implications.

All in all, the default settings of spring-data-mongodb is quite unoptimal. It is recommended that you do not rely on polymorphism in your spring-data-mongodb data model.

To circumvent the _class feature of spring-data-mongodb, install a custom mongo mapper:

  1. @Override
  2. @Bean
  3. public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, MongoCustomConversions customConversions, MongoMappingContext mappingContext) {
  4. MappingMongoConverter converter = super.mappingMongoConverter(databaseFactory, customConversions, mappingContext);
  5. // NB: without overriding defaultMongoTypeMapper, an _class field is put in every document
  6. // since we know exactly which java class a specific document maps to, this is surplus
  7. converter.setTypeMapper(new DefaultMongoTypeMapper(null));
  8. return converter;
  9. }

So OK, polymorphism is bad, but I really really want it!

Replace the CachedEncryptionEventListener by ReflectionEncryptionEventListener:

  1. @Bean
  2. public ReflectionEncryptionEventListener encryptionEventListener(CryptVault cryptVault) {
  3. return new ReflectionEncryptionEventListener(cryptVault);
  4. }

or via application.yml:

  1. mongodb.encrypt:
  2. type: reflection

Note that using reflection at runtime will come at a performance cost and the drawbacks outlined above.

Ignore decryption failures

Sometimes (see #17) it is useful to bypass the otherwise rigid decryption framework and allow for a best-effort reading of mongodb documents. Using the EncryptionEventListener.withSilentDecryptionFailure(true) allows to bypass these failures and leave the failing fields empty. Example:

  1. @Bean
  2. public CachedEncryptionEventListener encryptionEventListener(CryptVault cryptVault) {
  3. return new CachedEncryptionEventListener(cryptVault)
  4. .withSilentDecryptionFailure(true);
  5. }

or, via application.yml:

  1. mongodb.encrypt:
  2. silent-decryption-failures: true

It is also possible to autowire EncryptionEventListener and change this setting on-the-fly.

Keys

This library supports AES 256 bit keys out of the box. It’s possible to extend this, check the source code (CryptVault specifically) on how to do so.

To generate a key, you can use the following command line:

  1. dd if=/dev/urandom bs=1 count=32 | base64

Exchange keys

It is advisable to rotate your keys every now and then. To do so, define a new key version in application.yml:

  1. mongodb.encrypt:
  2. keys:
  3. - version: 1
  4. key: hqHKBLV83LpCqzKpf8OvutbCs+O5wX5BPu3btWpEvXA=
  5. - version: 2
  6. key: ge2L+MA9jLA8UiUJ4z5fUoK+Lgj2yddlL6EzYIBqb1Q=

spring-data-mongodb-encrypt would automatically use the highest versioned key for encryption by default, but supports decryption using any of the keys. This allows you to deploy a new key, and either let old data slowly get phased out, or run a nightly load+save batch job to force key migration. Once all old keys are phased out, you may remove the old key from the configuration.

You can use

  1. mongodb.encrypt:
  2. default-key: 1

to override which version of the defined keys is considered ‘default’.

Caveats

Keep in mind that this library hooks into spring-data’s serialization/deserialization only. As such, any operation that bypasses this, for example findAndModify() or direct mongo driver accesses (mongoTemplate.getCollection()), will not do any encryption/decryption. You can either find another way to achieve your goal via spring-data, or you will have to do the encryption/decryption manually. See the next paragraph for examples.

Encrypt other data

It’s perfectly possible to use the powerful encryption functionality of this library for custom purposes. Example:

  1. @Autowired CryptVault cryptVault;
  2. // encrypt
  3. byte[] encrypted = cryptVault.encrypt("rock".getBytes());
  4. // decrypt
  5. byte[] decrypted = cryptVault.decrypt(encrypted);
  6. new String(decrypted).equals("rock"); // true

If you want to use this library to encrypt arbitrary fields directly via mongo-driver:

  1. @Autowired MongoTemplate mongoTemplate;
  2. @Autowired CryptVault cryptVault;
  3. void store(String id, String secretData) {
  4. byte[] bytes = secretData.getBytes();
  5. byte[] encrypted = cryptVault.encrypt(bytes);
  6. Binary binary = new Binary(encrypted);
  7. BasicDBObject dbObject = new BasicDBObject("_id", id);
  8. dbObject.put("blob", binary);
  9. mongoTemplate.getCollection("blobs").save(dbObject);
  10. }
  11. String load(String id) {
  12. DBObject result = mongoTemplate.getCollection("blobs").findOne(id);
  13. if (result == null) return "";
  14. Object blob = result.get("blob");
  15. if (blob == null) return "";
  16. byte[] encrypted = (byte[]) blob;
  17. byte[] decrypted = cryptVault.decrypt(encrypted);
  18. return new String(decrypted);
  19. }

Encrypting the whole document

While it was not the use case for this library, it is very well possible to do whole document encryption with it.
Since the _id field (and all the other key fields) always have to be readable by mongodb, the best approach is to extract all the indexed keys into the root of the object, and keep the rest of the data as an @Encrypted sub-document, e.g.:

  1. @Field
  2. @Id
  3. public String id;
  4. @Field
  5. @Indexed
  6. public long otherId;
  7. @Field
  8. @Encrypted
  9. public SecretData data;

If you can’t afford to reveal the keys, you could use a high-performing hash like Guava’s murmur3 to hash the keys before exposing them, compound or independently.

Encrypting an indexed field

Since this library encrypts data before it is sent to mongodb, there is no point in indexing an encrypted field. However, an approach that worked remarkably well in the past was to put a calculated field next to the encrypted one that contains the hashed value (e.g. Guava’s murmur3) of the encrypted field.

When searching by index, create a hash of the lookup key and search with that against the hashed field. Normally, you’d have 0 (if not exists) or 1 (exists) hits. However, because hashing can result in collisions, you also have to process the case of more than 1 hits, in which case you’d have to load all the matching documents (during which the encrypted field is decrypted by this library), and compare the now-decrypted field to find your exact match.

Expected size of encrypted field

The mongodb driver serializes every java object into BSON. Under the hood, we use the very same BSON serialization for maximum compatibility.

You can expect the following extra sizes when you add an @Encrypted field:

  • 17..33 bytes for encryption overhead (salt + padding);
  • 12 bytes for BSON serialization overhead.

This also means that often it is better for both performance and storage size to mark a whole sub-document with @Encrypted instead of half of its fields.
You should check the resulting mongodb document’s Binary field sizes to decide.