From 15939a79aa5ff64f7f493424d47a907977743768 Mon Sep 17 00:00:00 2001 From: Agoston Horvath Date: Fri, 6 Oct 2017 15:07:36 +0200 Subject: [PATCH] initial commit --- .gitignore | 35 +++ README.md | 0 doc/LICENSE.txt | 30 +++ pom.xml | 95 +++++++ src/main/java/com/bol/secure/Encrypted.java | 11 + .../bol/secure/EncryptionEventListener.java | 240 ++++++++++++++++++ 6 files changed, 411 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 doc/LICENSE.txt create mode 100644 pom.xml create mode 100644 src/main/java/com/bol/secure/Encrypted.java create mode 100644 src/main/java/com/bol/secure/EncryptionEventListener.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e1397d --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# IntelliJ +*.iml +*.ipr +*.iws +.idea +classes + +# Eclipse +.classpath +.project +.buildpath +.springBeans +.settings/ +.metadata/ + +# Netbeans +nb-configuration.xml + +# Maven +target +pom.xml.versionsBackup + +# Gradle +build +.gradle + +# OS +Thumbs.db +.DS_Store + +# misc +.checkstyle +.pmd +.fbprefs +MANIFEST.MF diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/LICENSE.txt b/doc/LICENSE.txt new file mode 100644 index 0000000..2ec25a6 --- /dev/null +++ b/doc/LICENSE.txt @@ -0,0 +1,30 @@ +The BSD License + +Copyright (c) 2013 RIPE NCC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + - Neither the name of the RIPE NCC nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c55ee68 --- /dev/null +++ b/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + com.bol + spring-data-mongodb-encrypt + jar + spring-data-mongodb-encrypt + 1.0 + High performance, per-field encryption for spring-data-mongodb + https://github.com/agoston/spring-data-mongodb-encrypt + + + BSD + https://opensource.org/licenses/BSD-3-Clause + + + + https://github.com/agoston/spring-data-mongodb-encrypt + + + + Ágoston Horváth + github.com/agoston + ahorvath@bol.com + bol.com + 1 + http://bol.com + + + + + + org.springframework.data + spring-data-mongodb + 1.10.4.RELEASE + provided + + + org.hamcrest + hamcrest-library + 1.3 + test + + + junit + junit + 4.12 + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-source-plugin + 2.3 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 2.10.1 + + + attach-javadocs + + jar + + + -Xdoclint:none + + + + + + + diff --git a/src/main/java/com/bol/secure/Encrypted.java b/src/main/java/com/bol/secure/Encrypted.java new file mode 100644 index 0000000..c39f918 --- /dev/null +++ b/src/main/java/com/bol/secure/Encrypted.java @@ -0,0 +1,11 @@ +package com.bol.secure; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +public @interface Encrypted { +} diff --git a/src/main/java/com/bol/secure/EncryptionEventListener.java b/src/main/java/com/bol/secure/EncryptionEventListener.java new file mode 100644 index 0000000..6836f3b --- /dev/null +++ b/src/main/java/com/bol/secure/EncryptionEventListener.java @@ -0,0 +1,240 @@ +package com.bol.secure; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import org.bson.BSONObject; +import org.bson.BasicBSONDecoder; +import org.bson.BasicBSONEncoder; +import org.bson.types.Binary; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.security.SecureRandom; +import java.util.*; +import java.util.function.Function; + +public class EncryptionEventListener extends AbstractMongoEventListener implements ApplicationContextAware { + private static final Logger LOG = LoggerFactory.getLogger(EncryptionEventListener.class); + static final String MAP_FIELD_MATCHER = "*"; + static final String CIPHER = "AES/CBC/PKCS5Padding"; + + final int saltLength; + final String cipher; + final SecretKeySpec key; + + Map encrypted; + + public EncryptionEventListener(byte[] secret) { + this(secret, 16, CIPHER); + } + + public EncryptionEventListener(byte[] secret, int saltLength) { + this(secret, saltLength, CIPHER); + } + + public EncryptionEventListener(byte[] secret, int saltLength, String cipher) { + this.saltLength = saltLength; + this.cipher = cipher; + this.key = new SecretKeySpec(secret, cipher.substring(0, cipher.indexOf('/'))); + } + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + encrypted = new HashMap<>(); + MongoMappingContext mappingContext = applicationContext.getAutowireCapableBeanFactory().getBean(MongoMappingContext.class); + + for (BasicMongoPersistentEntity entity : mappingContext.getPersistentEntities()) { + List children = processDocument(entity.getClass()); + if (!children.isEmpty()) encrypted.put(entity.getClass(), new Node("", children, NodeType.ROOT)); + } + } + + List processDocument(Class objectClass) { + List nodes = new ArrayList<>(); + for (Field field : objectClass.getDeclaredFields()) { + try { + if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) continue; + + if (field.isAnnotationPresent(Encrypted.class)) { + // direct @Encrypted annotation - crypt the corresponding field of BasicDbObject + nodes.add(new Node(field.getName(), Collections.emptyList(), NodeType.DIRECT)); + + } else if (Collection.class.isAssignableFrom(field.getType())) { + // descending into Collection + ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); + Class genericClass = (Class) parameterizedType.getActualTypeArguments()[0]; + + List children = processDocument(genericClass); + if (!children.isEmpty()) nodes.add(new Node(field.getName(), children, NodeType.LIST)); + + } else if (Map.class.isAssignableFrom(field.getType())) { + // descending into Values of Map objects + ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType(); + Class genericClass = (Class) parameterizedType.getActualTypeArguments()[1]; + + List children = processDocument(genericClass); + if (!children.isEmpty()) { + List mapKeys = Collections.singletonList(new Node(MAP_FIELD_MATCHER, children, NodeType.DOCUMENT)); + nodes.add(new Node(field.getName(), mapKeys, NodeType.MAP)); + } + + } else { + // descending into sub-documents + List children = processDocument(field.getType()); + if (!children.isEmpty()) nodes.add(new Node(field.getName(), children, NodeType.DOCUMENT)); + } + + } catch (Exception e) { + LOG.error("{}.{}", objectClass.getName(), field.getName(), e); + } + } + + return nodes; + } + + @Override + public void onAfterLoad(AfterLoadEvent event) { + try { + DBObject dbObject = event.getDBObject(); + + Node node = encrypted.get(event.getType()); + if (node == null) return; + + BasicBSONDecoder decoder = new BasicBSONDecoder(); + cryptFields(dbObject, node, o -> decoder.readObject(decrypt((byte[]) o))); + } catch (Exception e) { + LOG.error("onAfterLoad", e); + throw e; + } + } + + @Override + public void onBeforeSave(BeforeSaveEvent event) { + try { + DBObject dbObject = event.getDBObject(); + + Node node = encrypted.get(event.getSource().getClass()); + if (node == null) return; + + BasicBSONEncoder encoder = new BasicBSONEncoder(); + cryptFields(dbObject, node, o -> new Binary(encrypt(encoder.encode((BSONObject) o)))); + } catch (Exception e) { + LOG.error("onBeforeSave", e); + throw e; + } + } + + void cryptFields(DBObject dbObject, Node node, Function crypt) { + if (node.type == NodeType.MAP) { + Node mapChildren = node.children.get(0); + for (Map.Entry entry : ((BasicDBObject) dbObject).entrySet()) { + cryptFields((DBObject) entry.getValue(), mapChildren, crypt); + } + return; + } + + for (Node childNode : node.children) { + Object value = dbObject.get(childNode.fieldName); + if (value == null) continue; + + if (!childNode.children.isEmpty()) { + if (value instanceof BasicDBList) { + for (Object o : (BasicDBList) value) + cryptFields((DBObject) o, childNode, crypt); + } else { + cryptFields((BasicDBObject) value, childNode, crypt); + } + return; + } + + if (value instanceof BasicDBList) { + BasicDBList leafArray = (BasicDBList) value; + for (int i = 0; i < leafArray.size(); i++) { + leafArray.set(i, crypt.apply(leafArray.get(i))); + } + } else { + dbObject.put(childNode.fieldName, crypt.apply(value)); + } + } + } + + Cipher cipher() { + try { + return Cipher.getInstance(cipher); + } catch (Exception e) { + LOG.error("Mongo encrypt init failed for cipher {}", cipher, e); + return null; + } + } + + public static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + static byte[] urandomBytes(int numBytes) { + byte[] bytes = new byte[numBytes]; + SECURE_RANDOM.nextBytes(bytes); + return bytes; + } + + byte[] encrypt(byte[] data) { + try { + byte[] random = urandomBytes(saltLength); + IvParameterSpec iv_spec = new IvParameterSpec(random); + Cipher cipher = cipher(); + cipher.init(Cipher.ENCRYPT_MODE, key, iv_spec); + return cipher.doFinal(data); + } catch (Exception e) { + LOG.error("encrypt failed", e); + return data; + } + } + + byte[] decrypt(byte[] data) { + try { + Cipher cipher = cipher(); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(data); + } catch (Exception e) { + LOG.error("decrypt failed", e); + return data; + } + } + + class Node { + public final String fieldName; + public final List children; + public final NodeType type; + + public Node(String fieldName, List children, NodeType type) { + this.fieldName = fieldName; + this.children = children; + this.type = type; + } + } + + enum NodeType { + /** root node, on @Document classes */ + ROOT, + /** field with @Encrypted annotation present - to be crypted directly */ + DIRECT, + /** field is a BasicDBList, descend */ + LIST, + /** field is a Map, need to descend on its values */ + MAP, + /** field is a sub-document, descend */ + DOCUMENT + } +}