最近,我不得不将数据库加密添加到几个字段中,并且发现了很多不好的建议。
建筑问题
最大的问题是建筑。 如果持久性管理器悄悄地处理您的加密,那么根据定义,您的体系结构将在持久性和安全性设计之间要求紧密而不必要的绑定。 您不能触摸一个而不接触另一个。
这似乎是不可避免的,但是有一个受人尊敬的想法,那就是最好的架构是您拥有独立的应用程序开发人员和安全开发人员团队的架构。 应用程序开发人员不能草率,但总的来说,他们唯一的重点是功能完成。 安全开发人员负责设计和实现安全性。 唯一考虑这两个方面的地方是建筑和顶层设计。
过去这不是很实用,但是面向方面的编程(AOP)和类似的概念已经改变了这一点。 现在,在服务层和持久层之间注入一个拦截器是完全合理的,这样就可以悄悄地丢弃未授权调用方查看的值。 10个项目的列表可能会减少到7个,或者更新可能会引发异常,而不是修改只读值。 持久保存集合时要复杂一些,但是一般方法应该很明确。
这里的关键是,应用程序开发人员无需查看安全代码。 所有这些都可以通过在部署时通过配置文件添加的AOP注入来处理。 更重要的是,它可以随时更改,而无需修改应用程序本身。 (您可能需要执行一个更新过程,该过程将更改数据库中的值。)
拦截器甚至可以阻止对未记录方法的调用-不用担心流氓程序员。
在实践中,许多站点将有几个开发人员都戴上帽子,而不是拥有专门的安全团队。 只要他们能够牢记自己的职责,这不是问题。
在JPA或Hibernate字段中进行透明加密绝对比在POJO中放入加密/解密代码更好,但是它仍然在安全性和持久性层之间强加了不必要的绑定。 它还存在严重的安全问题。
安全问题
每当您处理加密时,都会遇到一个关键问题–可以将此对象写入磁盘吗? 最明显的威胁是序列化,例如,通过钝化数据以释放内存或将其迁移到其他服务器的应用服务器。
实际上,这意味着您的密钥和纯文本内容必须标记为“ transient”(对于序列化引擎)和“ @Transient”(对于JPA或Hibernate)。 如果您真的很偏执,您甚至会覆盖隐式序列化方法writeObject,因此可以绝对保证这些字段永远不会写入磁盘。
这是可行的……但是它使透明的加密/解密大为失败,因为该代码的全部目的是使这些字段看起来就像另一个字段。 您必须维护两个字段-持久加密值和瞬态未加密值-并具有某种使它们保持同步的方法。 无需在您的pojo中添加任何密码即可完成所有操作。
一个更微妙的问题是,如果攻击者可以通过使应用服务器崩溃而触发核心转储,则您的对象仍可能写入磁盘。 细心的站点管理员将禁用核心转储,但许多人忽略了它。 解决这个问题比较困难,但是如果AOP可以在需要解密值的方法周围立即解密/加密值,则有可能。 您的应用程序不关心解密在哪里发生,只要它在需要时就被解密即可。 这是应该留给安全团队的决策类型。
可以通过操作系统交换文件将对象写入磁盘的第三种方式,但这应该不是问题,因为交换文件现在通常已加密。
JPA实体侦听器
一个解决方案是JPA EntityListeners或相应的Hibernate类。 这些是侦听器类,可以提供在数据库对象创建,删除或修改之前或之后调用的方法。
样例代码
使用一些示例代码最容易看到这一点。 考虑一种情况,我们必须保留第三方站点的用户密码。 在这种情况下,我们必须使用加密,而不是哈希。
(注意:我怀疑这是Twitter第三方应用程序所需的实际信息–仅用于说明目的。)
实体
/*** 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 twitterUserprivate 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')@Lobpublic String getEncryptedPassword() { return encryptedPassword; }@Transientpublic String getPassword() { return password; }// similar definitions for setters....
}
DAO
/*** Conventional DAO to access login information.*/
@LocalBean
@Stateless
public class TwitterDao {@PersistenceContextprivate 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
为了在持久层和安全层之间保持清晰的隔离,侦听器除了调用处理加密的服务外什么也不做。 它完全不了解加密细节。
public class TwitterUserPasswordListener {@Injectprivate EncryptorBean encryptor;/*** Decrypt password after loading.*/@PostLoad@PostUpdatepublic 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@PreUpdatepublic 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。 这将使我们能够以最小的干扰安静地旋转加密密钥-这是通常的“简单加密”方法绝对不可能实现的。
此类使用OWASP / ESAPI进行加密,因为1)它应已由您的应用程序使用; 2)可移植格式允许其他应用程序使用我们的数据库,只要它们也使用OWASP / ESAPI库即可。
该实现仅涵盖字符串-健壮的解决方案应具有针对所有原始类型以及可能针对特定领域的类(例如信用卡)的方法。
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 keyprivate 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 keyKeySpec 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甚至属性文件表示。
参考: Invariant Properties博客中的JCG合作伙伴 Bear Giles 使用JPA侦听器进行数据库加密 。
翻译自: https://www.javacodegeeks.com/2012/11/database-encryption-using-jpa-listeners.html