最近,我不得不將數據庫加密添加到幾個字段中,并且發現了很多不好的建議。
建筑問題
最大的問題是建筑。 如果持久性管理器悄悄地處理您的加密,那么根據定義,您的體系結構將在持久性和安全性設計之間要求緊密而不必要的綁定。 您不能觸摸一個而不接觸另一個。
這似乎是不可避免的,但是有一個受人尊敬的想法,那就是最好的架構是您擁有獨立的應用程序開發人員和安全開發人員團隊的架構。 應用程序開發人員不能草率,但總的來說,他們唯一的重點是功能完成。 安全開發人員負責設計和實現安全性。 唯一考慮這兩個方面的地方是建筑和頂層設計。
過去這不是很實用,但是面向方面的編程(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