Статьи

Как использовать JPA Type Converter для шифрования ваших данных

Несколько дней назад я прочитал интересную статью Bear Giles о шифровании базы данных с использованием прослушивателей JPA 2012 года. Он обсуждает свои требования к решению для шифрования и предоставляет пример кода со слушателями JPA. Его основные требования:

  • обеспечить прозрачное шифрование, которое не влияет на приложение,
  • быть в состоянии добавить шифрование во время развертывания,
  • Разрабатывать приложения и безопасность / шифрование двумя разными командами / людьми.

И я с ним полностью согласен. Но после 1,5 лет и обновления спецификации до JPA 2.1, слушатели JPA больше не являются единственным решением. В JPA 2.1 представлен конвертер типов, который можно использовать для создания лучшего решения.

Общая информация и настройка

В этом примере предполагается, что у вас есть базовые знания о преобразователе типов JPA. Если вы хотите прочитать более подробно о преобразователях типов, проверьте мою предыдущую статью о JPA 2.1 — Как реализовать преобразователь типов .

Настройка для следующего примера тихая. Вам просто нужен сервер приложений, совместимый с Java EE 7. В качестве реализации JPA я использовал Wildfly 8.0.0.Final, который содержит Hibernate 4.3.2.Final .

Создание CryptoConverter

Платежная информация, такая как номер кредитной карты, является конфиденциальной информацией, которая должна быть зашифрована. В следующем фрагменте кода показана сущность CreditCard, которую мы будем использовать в этом примере.

01
02
03
04
05
06
07
08
09
10
11
12
13
@Entity
public class CreditCard {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
 
    private String ccNumber;
 
    private String name;
 
    ...
}

Как мы указывали в начале, шифрование должно работать прозрачно. Это означает, что шифрование не влияет на приложение и его можно добавить без каких-либо изменений в существующую кодовую базу. Для меня это также включает модель данных в базе данных, потому что она часто создается некоторыми специфическими для приложения скриптами, которые не должны изменяться. Поэтому нам нужен преобразователь типов, который не изменяет тип данных при шифровании и дешифровании информации.

В следующем фрагменте кода показан пример такого преобразователя. Как видите, конвертер довольно прост. Метод convertToDatabaseColumn вызывается hibernate до сохранения сущности в базе данных. Он получает незашифрованную строку от объекта и использует алгоритм AES с PKCS5Padding для шифрования. Затем кодировка base64 используется для преобразования зашифрованного байта [] в строку, которая будет сохранена в базе данных.

Когда поставщик постоянства читает сущность из базы данных, вызывается метод convertToEntityAttribute . Он берет зашифрованную строку из базы данных, использует декодирование base64 для преобразования ее в байт [] и выполняет расшифровку. Расшифрованная строка присваивается атрибуту объекта.

Для реального приложения вы можете захотеть приложить больше усилий к шифрованию или перенести его в отдельный класс. Но это должно быть достаточно хорошо, чтобы объяснить общую идею.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Converter
public class CryptoConverter implements AttributeConverter<String, String> {
 
    private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
    private static final byte[] KEY = "MySuperSecretKey".getBytes();
 
    @Override
    public String convertToDatabaseColumn(String ccNumber) {
      // do some encryption
      Key key = new SecretKeySpec(KEY, "AES");
      try {
         Cipher c = Cipher.getInstance(ALGORITHM);
         c.init(Cipher.ENCRYPT_MODE, key);
         return Base64.encodeBytes(c.doFinal(ccNumber.getBytes()));
      } catch (Exception e) {
         throw new RuntimeException(e);
      }
    }
 
    @Override
    public String convertToEntityAttribute(String dbData) {
      // do some decryption
      Key key = new SecretKeySpec(KEY, "AES");
      try {
        Cipher c = Cipher.getInstance(ALGORITHM);
        c.init(Cipher.DECRYPT_MODE, key);
        return new String(c.doFinal(Base64.decode(dbData)));
      } catch (Exception e) {
        throw new RuntimeException(e);
      }
    }
}

Хорошо, у нас есть конвертер типов, который шифрует и дешифрует строку. Теперь нам нужно указать hibernate использовать этот конвертер для сохранения атрибута ccNumber объекта CreditCard . Как описано в одной из моих предыдущих статей , мы могли бы использовать аннотацию @Convert для этого. Но это изменило бы код нашего приложения.

Другое и для наших требований лучший вариант — назначить конвертер в конфигурации XML. Это можно сделать в файле orm.xml . Следующий фрагмент кода назначает CryptoConverter атрибуту ccNumber объекта CreditCard .

1
2
3
4
5
6
7
8
9
<entity-mappings version="2.1"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd">
 
    <entity class="blog.thoughts.on.java.jpa21.enc.entity.CreditCard">
        <convert converter="blog.thoughts.on.java.jpa21.enc.converter.CryptoConverter" attribute-name="ccNumber"/>
    </entity>
</entity-mappings>

Это все, что нам нужно сделать, чтобы реализовать и настроить шифрование на основе конвертера типов для одного поля базы данных.

Слушатели сущностей или конвертер типов?

Ответ на этот вопрос не так прост, как кажется. Оба решения имеют свои преимущества и недостатки.

Слушатель сущности, описанный Bear Giles, может использовать несколько атрибутов сущности во время шифрования. Таким образом, вы можете объединить несколько атрибутов, зашифровать их и сохранить зашифрованные данные в одном поле базы данных. Или вы можете использовать разные атрибуты для зашифрованных и дешифрованных данных, чтобы избежать сериализации дешифрованных данных ( как описано в Bear Giles ). Но использование прослушивателя сущностей также имеет недостатки. Его реализация специфична для сущности и более сложна, чем реализация конвертера типов. И если вам нужно зашифровать дополнительный атрибут, вам нужно изменить реализацию.

Как вы видели в приведенном выше примере, реализация преобразователя типов проста и может использоваться повторно. CryptoConverter может использоваться для шифрования любого атрибута String любого объекта. А использование конфигурации на основе XML для регистрации преобразователя в атрибуте сущности не требует никаких изменений в исходном коде приложения. Вы даже можете добавить его в приложение позднее, если вы перенесете существующие данные. Недостатком этого решения является то, что атрибут зашифрованного объекта не может быть помечен как переходный. Это может привести к уязвимости, если объект будет записан на диск.

Видите ли, оба подхода имеют свои плюсы и минусы. Вы должны решить, какие преимущества и недостатки более важны для вас.

Вывод

В начале этого поста мы определили 3 требования:

  • обеспечить прозрачное шифрование, которое не влияет на приложение,
  • быть в состоянии добавить шифрование во время развертывания,
  • Разрабатывать приложения и безопасность / шифрование двумя разными командами / людьми.

Описанная реализация CryptoConverter выполняет все из них. Шифрование может быть добавлено во время развертывания и не влияет на приложение, если для назначения преобразователя типов используется конфигурация XML. Разработка приложения и шифрование полностью независимы и могут быть выполнены различными командами. Кроме того, CryptoConverter может использоваться для преобразования любого атрибута String любого объекта. Так что он имеет высокую возможность повторного использования. Но это решение также имеет некоторые недостатки, как мы видели в последнем абзаце.

Вы должны принять решение, какой подход вы хотите использовать. Пожалуйста, напишите мне комментарий о вашем выборе.