Статьи

Шифрование базы данных с использованием JPA Listeners

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

Архитектурные проблемы

Самая большая проблема — архитектурная. Если ваш менеджер персистентности спокойно обрабатывает ваше шифрование, то, по определению, ваша архитектура требует жесткой и ненужной связи между вашими проектами персистентности и безопасности. Вы не можете прикоснуться к одному, не касаясь другого.

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

В прошлом это было не очень практично, но Аспектно-ориентированное программирование (AOP) и подобные концепции изменили это. Теперь вполне разумно внедрить перехватчик между уровнем службы и постоянством, чтобы значения, которые вызывающий не имеет права видеть, незаметно отбрасывались. Список из 10 элементов может быть сокращен до 7, или обновление может вызвать исключение вместо изменения значения только для чтения. Это немного сложнее при сохранении коллекций, но общий подход должен быть ясным.

Ключевым моментом здесь является то, что разработчикам приложений никогда не нужно видеть код безопасности. Все это может быть обработано инъекцией AOP, добавленной через файлы конфигурации во время развертывания. Что еще более важно, это может быть изменено в любое время, не требуя модификации самого приложения. (Вам может потребоваться выполнить процесс обновления, который изменит значения в базе данных.)

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

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

Прозрачное шифрование в полях JPA или Hibernate определенно лучше, чем размещение кода шифрования / дешифрования в вашем POJO, но оно все еще накладывает высокий уровень ненужной привязки между уровнями безопасности и персистентности. У него также есть серьезные проблемы с безопасностью.

Проблемы с безопасностью

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

На практике это означает, что ваши ключи и содержимое открытого текста должны быть помечены как «переходные» (для механизма сериализации) и «@Transient» (для JPA или Hibernate). Если вы действительно параноик, вы даже переопределите неявный метод сериализации writeObject, так что вы можете абсолютно гарантировать, что эти поля никогда не будут записаны на диск.

Это работает … но это уносит прозрачное шифрование / дешифрование из воды, так как весь смысл этого кода в том, чтобы эти поля выглядели как просто другое поле. Вы должны поддерживать два поля — постоянное зашифрованное значение и временное незашифрованное значение — и иметь какой-то способ их синхронизации. Все сделано без использования какого-либо крипто-кода в вашем pojo.

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

Третий способ записи объектов на диск — через файлы подкачки ОС, но это не должно вызывать проблем, поскольку файлы подкачки обычно шифруются.

JPA EntityListeners

Решением является JPA EntityListeners или соответствующий класс Hibernate. Это классы Listener, которые могут предоставлять методы, вызываемые до или после создания, удаления или изменения объекта базы данных.

Образец кода

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

(Примечание: я сомневаюсь, что это фактическая информация, требуемая Twitter для сторонних приложений — это исключительно для иллюстрации.)

Лицо

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
/**
 * Conventional POJO. Following other conventions the sensitive
 * information is written to a secondary table in addition to being
 * encrypted.
 */
@Entity
@Table(name='twitter')
@SecondaryTable(name='twitter_pw', pkJoinColumns=@PrimaryKeyJoinColumn(name='twitter_id'))
@EntityListeners(TwitterUserPasswordListener.class)
public class TwitterUser {
   private Integer id;
   private String twitterUser
   private String encryptedPassword;
   transient private String password;
 
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   public Integer getId() { return id; }
 
   @Column(name = 'twitter_user')
   public String getTwitterUser() { return twitterUser; }
 
   @Column(name = 'twitter_pw', table = 'twitter_pw')
   @Lob
   public String getEncryptedPassword() { return encryptedPassword; }
 
   @Transient
   public String getPassword() { return password; }
 
   // similar definitions for setters....
}

DAO

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
 * Conventional DAO to access login information.
 */
@LocalBean
@Stateless
public class TwitterDao {
   @PersistenceContext
   private EntityManager em;
 
   /**
    * Read an object from the database.
    */
   @TransactionAttribute(TransactionAttributeType.SUPPORTS)
   public TwitterUser getUserById(Integer id) {
      return em.find(TwitterUser.class, id);
   }
 
   /**
    * Create a new record in the database.
    */
   @TransactionAttribute(TransactionAttributeType.REQUIRED)
   public saveTwitterUser(TwitterUser user) {
      em.persist(user);
   }
 
   /**
    * Update an existing record in the database.
    *
    * Note: this method uses JPA semantics. The Hibernate
    * saveOrUpdate() method uses slightly different semantics
    * but the required changes are straightforward.
    */
   @TransactionAttribute(TransactionAttributeType.REQUIRED)
   public updateTwitterUser(TwitterUser user) {
      TwitterUser tw = em.merge(user);
 
      // we need to make one change from the standard method -
      // during a 'merge' the old data read from the database
      // will result in the decrypted value overwriting the new
      // plaintext value - changes won't be persisted! This isn't
      // a problem when the object is eventually evicted from
      // the JPA/Hibernate cache so we're fine as long as we
      // explicitly copy any fields that are hit by the listener.
      tw.setPassword(user.getPassword());
 
      return tw;
   }

EntityListener

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

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
33
34
35
36
37
38
39
40
41
42
public class TwitterUserPasswordListener {
   @Inject
   private EncryptorBean encryptor;
 
   /**
    * Decrypt password after loading.
    */
   @PostLoad
   @PostUpdate
   public void decryptPassword(Object pc) {
      if (!(pc instanceof TwitterUser)) {
         return;
      }
 
      TwitterUser user = (TwitterUser) pc;
      user.setPassword(null);
 
      if (user.getEncryptedPassword() != null) {
         user.setPassword(
            encryptor.decryptString(user.getEncryptedPassword());
      }
   }
 
   /**
    * Decrypt password before persisting
    */
   @PrePersist
   @PreUpdate
   public void encryptPassword(Object pc) {
      if (!(pc instanceof TwitterUser)) {
         return;
      }
 
      TwitterUser user = (TwitterUser) pc;
      user.setEncryptedPassword(null);
 
      if (user.getPassword() != null) {
         user.setEncryptedPassword(
            encryptor.encryptString(user.getPassword());
      }
   }
}

EncryptorBean

EncryptorBean обрабатывает шифрование, но не знает, что шифруется. Это минимальная реализация — на практике мы, вероятно, захотим передать keyId в дополнение к ciphertext / plaintext. Это позволило бы нам спокойно вращать ключи шифрования с минимальным нарушением — что, безусловно, невозможно при обычных подходах «простого шифрования».

Этот класс использует OWASP / ESAPI для шифрования, поскольку 1) он уже должен использоваться вашим приложением и 2) переносимый формат позволяет другим приложениям использовать нашу базу данных, если они также используют библиотеку OWASP / ESAPI.

Реализация распространяется только на строки — в надежном решении должны быть методы для всех примитивных типов и, возможно, для доменных классов, таких как кредитные карты.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import org.owasp.esapi.ESAPI;
import org.owasp.esapi.Encryptor;
import org.owasp.esapi.codecs.Base64;
import org.owasp.esapi.crypto.CipherText;
import org.owasp.esapi.crypto.PlainText;
import org.owasp.esapi.errors.EncryptionException;
import org.owasp.esapi.reference.crypto.JavaEncryptor;
 
@Stateless
public class EncryptorBean {
   private static final String PBE_ALGORITHM = 'PBEWITHSHA256AND128BITAES-CBC-BC';
   private static final String ALGORITHM = 'AES';
 
   // hardcoded for demonstration use. In production you might get the
   // salt from the filesystem and the password from a appserver JNDI value.
   private static final String SALT = 'WR9bdtN3tMHg75PDK9PoIQ==';
   private static final char[] PASSWORD = 'password'.toCharArray();
 
   // the key
   private transient SecretKey key;
 
   /**
    * Constructor creates secret key. In production we may want
    * to avoid keeping the secret key hanging around in memory for
    * very long.
    */
   public EncryptorBean() {
      try {
         // create the PBE key
         KeySpec spec = new PBEKeySpec(PASSWORD, Base64.decode(SALT), 1024);
         SecretKey skey = SecretKeyFactory.getInstance(PBE_ALGORITHM).generateSecret(spec);
         // recast key as straightforward AES without padding.
         key = new SecretKeySpec(skey.getEncoded(), ALGORITHM);
      } catch (SecurityException ex) {
         // handle appropriately...
      }
   }
 
   /**
    * Decrypt String
    */
   public String decryptString(String ciphertext) {
      String plaintext = null;
 
      if (ciphertext != null) {
         try {
            Encryptor encryptor = JavaEncryptor.getInstance();
            CipherText ct = CipherText.from PortableSerializedBytes(Base64.decode(ciphertext));
            plaintext = encryptor.decrypt(key, ct).toString();
         } catch (EncryptionException e) {
            // handle exception. Perhaps set value to null?
         }
      }
 
      return plaintext;
   }
 
   /**
    * Encrypt String
    */
   public String encryptString(String plaintext) {
      String ciphertext= null;
 
      if (plaintext!= null) {
         try {
            Encryptor encryptor = JavaEncryptor.getInstance();
            CipherText ct = encryptor.encrypt(key, new PlaintText(plaintext));
            ciphertext = Base64.encodeBytes(ct.asPortableSerializedByteArray());
         } catch (EncryptionException e) {
            // handle exception. Perhaps set value to null?
         }
      }
 
      return ciphertext;
   }
}

Последние мысли

Нет никаких причин, по которым вы должны иметь отношение один к одному между незашифрованными и зашифрованными полями. Совершенно разумно объединить связанные поля в одно значение — фактически, вероятно, предпочтительнее шифровать каждое поле по отдельности. Значения могут быть представлены в CSV, XML, JSON или даже в файле свойств.

Ссылка: Шифрование базы данных с использованием JPA Listeners от нашего партнера JCG Беара Джайлза в блоге Invariant Properties .