使用OpenSSL加密,使用Java解密,使用OpenSSL RSA公钥
抽象
在2017年,我撰寫了一個由三部分組成的系列文章,內容涉及選擇最佳的哈希和加密算法。 在對該系列進行研究時,我學到了很多有關哈希和加密的知識。 我學到的最重要的事情是,盡管我必須對如何使用最安全的算法進行自我教育,但我也必須將這些算法的開發(fā)工作留給專家。 話雖如此,我開始考慮Java與加密專家(特別是OpenSSL)的互操作性。 我的第3部分系列僅從Java的角度著眼于加密。 我想知道Java與OpenSSL之類的工具進行互操作將有多么困難。 本博客的目的是演示Java與OpenSSL的互操作性:
- 使用OpenSSL生成私鑰和公鑰
- 使用OpenSSL加密值
- 用Java解密值
免責聲明
這篇文章僅供參考。 在使用所提供的任何信息之前,請認真思考。 從中學到東西,但最終自己做出決定,風險自負。
要求
我使用以下主要技術完成了本文的所有工作。 您可能可以使用不同的技術或版本來做相同的事情,但不能保證。
- OpenJDK運行時環(huán)境Zulu11.39 + 15-CA(內部版本11.0.7 + 10-LTS)
- OpenSSL 1.1.1c 2019年5月28日
- Apache NetBeans IDE 11.3
- Maven 3.3.9(與NetBeans捆綁在一起)
下載
訪問我的GitHub頁面https://github.com/mjremijan以查看我所有的開源項目。 這篇文章的代碼位于: https : //github.com/mjremijan/thoth-rsa
背景
當我使用Microservices將Monolith應用程序模塊化時,我開始懷疑是否能夠互操作OpenSSL和Java。 使用微服務時,應用程序仍然需要加密和解密敏感的配置數(shù)據(例如數(shù)據庫密碼),但是微服務使用的小型運行時環(huán)境給這帶來了挑戰(zhàn)。
借助Monolith架構,Java / Jakarta EE應用程序服務器可以處理應用程序的加密和解密。 諸如數(shù)據庫連接池之類的托管資源是在EE應用程序服務器中配置的,其他其他加密值通常可以存儲在JNDI中。 在這兩種情況下,服務器都提供加密和解密功能,而應用程序不知道任何細節(jié)。 應用服務器可以為應用提供托管資源或解密值。
但是,在微服務架構中,運行時(例如Spring Boot)保持“較小”狀態(tài),并且不提供與EE應用程序服務器一樣多的功能。 數(shù)據庫連接就是一個很好的例子。 在Spring Boot中配置數(shù)據庫連接很容易,但是您如何支持密碼加密和解密? 現(xiàn)在,它必須得到DevOps和開發(fā)團隊的支持。
注意其他微服務技術(例如Kubernetes)正在努力填補空白,并提供類似于EE應用程序服務器的加密功能。
所以這讓我開始思考。 DevOps生活在Linux / Unix世界中。 開發(fā)人員生活在Java世界中。 為什么不將兩個世界放在一起以支持加密/解密策略? 這將使DevOps和開發(fā)人員能夠盡自己所能。 為此,我首先需要明確定義目標。
目標
從Monolith架構遷移到微服務很慢。 是的,存在用于加密和解密的微服務基礎結構解決方案。 但是,如果該基礎結構不可用,則在3-5年的過渡期內不會為您提供幫助。 為了支持過渡,我決定了以下目標。
牢記這些目標,讓我們開始一段旅程。
使用哪種算法
我需要回答的第一個問題是要使用哪種加密算法。 對于加密,我可以選擇單密鑰對稱加密還是公共/私有密鑰非對稱加密。 我的選擇是:
RSA-4096公鑰/私鑰非對稱加密
選擇非對稱加密算法的原因是因為公鑰/私鑰允許最大程度的責任分離。 可能會有獨立的團隊來生成密鑰,加密值以及將所有內容放在一起以供運行時使用。 實際上,這可以全部由一個團隊甚至一個人完成,但是非對稱加密算法可以靈活地分離這些問題。
至于使用RSA-4096算法,根據我的研究,它是當今最好,最安全的(Remijan,2017)。
現(xiàn)在我們知道要使用哪種算法。 接下來,我們將研究生成私鑰。
OpenSSL生成私鑰
在Java中, PKCS8EncodedKeySpec類期望使用PKCS8編碼的RSA私鑰。 (Java代碼,nd)。 我發(fā)現(xiàn)了使用OpenSSL的兩種方法。
清單2.1 –用2個命令生成私鑰
# Generate key with pkcs1 encoding # Generate private openssl genrsa -out private_key_rsa_4096_pkcs1.pem 4096 # Convert private # Convert key to pkcs8 encoding openssl pkcs8 -topk8 -in private_key_rsa_4096_pkcs1.pem -inform pem -out private_key_rsa_4096_pkcs8-exported.pem -outform pem -nocrypt在清單2.1中(2017年,斯坦斯坦),私鑰是通過2條命令生成的。 第一條命令使用PKCS1編碼生成密鑰。 第二條命令將PKCS1編碼的密鑰轉換為PKCS8編碼的密鑰。
清單2.2 –用1個命令生成私鑰
# Generate key with pkcs8 encoding # Generate private openssl genpkey -out private_key_rsa_4096_pkcs8-generated.pem -algorithm RSA -pkeyopt rsa_keygen_bits: 4096在清單2.2中,私鑰是使用單個命令生成的。 這將產生帶有PKCS8編碼的密鑰。 無需其他轉換。
無論您使用清單2.1還是2.2生成私鑰,生成時的私鑰都將如下所示。
-----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDVgLrCSDC5mLRL JY+okYX5MOMGi+bvtRQ9qIQ90d3BO1gAao6ZsbPEFxnOTR9Q3bGsEE5oRlh/FSYS . . kvCjd0ineNZ6OgPVJ/mhPULsZb11+noSUPmFqvClb8SQ0BipbKIcSTIJlQt1ZRZ2 INdXsP5kNlRK181jtU/xtQYfwSjkKA== -----END PRIVATE KEY-----大! 私鑰已生成! 現(xiàn)在讓我們繼續(xù)生成公共密鑰。
OpenSSL生成公鑰
在Java中, X509EncodedKeySpec類期望使用X509編碼的RSA公鑰。 (Java代碼,nd)。 公鑰是從私鑰生成的,因此您必須首先擁有私鑰。
清單3.1 –生成公鑰
# Export public key in pkcs8 format openssl rsa -pubout -outform pem -in private_key_rsa_4096_pkcs8-generated.pem -out public_key_rsa_4096_pkcs8-exported.pem清單3.1顯示了使用私鑰private_key_rsa_4096_pkcs8-generated.pem生成公用密鑰public_key_rsa_4096_pkcs8-exported.pem 。
公鑰將如下所示。
-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1YC6wkgwuZi0SyWPqJGF +TDjBovm77UUPaiEPdHdwTtYAGqOmbGzxBcZzk0fUN2xrBBOaEZYfxUmEkOFzPbF . . oNta8CSsVrqgFW/tI6+MQwrQFEOcBPCbh6Pr7NbiuR2LrfoJhUJlD5ofz5eM0419 JSS0RvKh0dF3ddlOKV/TQUsCAwEAAQ== -----END PUBLIC KEY-----大! 我們同時擁有私鑰和公鑰,并且都是由OpenSSL生成的。 接下來,我們需要Java才能使用這些密鑰文件。 這樣做,我們需要創(chuàng)建KeyFactory , PrivateKey和PublicKey對象的實例。 讓我們深入一些Java代碼!
Java KeyFactory,PrivateKey,PublicKey
在使用OpenSSL生成私鑰和公鑰文件之后,是時候編寫一些Java代碼了。 清單4.1是我完整的Rsa4096類。 我將在下面詳細討論每種方法。
清單4.1 – Rsa4096類
package org.thoth.rsa; import java.io.InputStream; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.PublicKey; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import javax.crypto.Cipher; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class Rsa4096 { private KeyFactory keyFactory; private PrivateKey privateKey; private PublicKey publicKey; public Rsa4096( String privateKeyClassPathResource , String publicKeyClassPathResource ) throws Exception { setKeyFactory(); setPrivateKey(privateKeyClassPathResource); setPublicKey(publicKeyClassPathResource); } protected void setKeyFactory() throws Exception { this .keyFactory = KeyFactory.getInstance( "RSA" ); } protected void setPrivateKey(String classpathResource) throws Exception { InputStream is = this .getClass() .getClassLoader() .getResourceAsStream(classpathResource); String stringBefore = new String(is.readAllBytes()); is.close(); String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PRIVATE KEY-----" , "" ) .replaceAll( "-----END PRIVATE KEY-----" , "" ) .trim(); byte [] decoded = Base64 .getDecoder() .decode(stringAfter); KeySpec keySpec = new PKCS8EncodedKeySpec(decoded); privateKey = keyFactory.generatePrivate(keySpec); } protected void setPublicKey(String classpathResource) throws Exception { InputStream is = this .getClass() .getClassLoader() .getResourceAsStream(classpathResource); String stringBefore = new String(is.readAllBytes()); is.close(); String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PUBLIC KEY-----" , "" ) .replaceAll( "-----END PUBLIC KEY-----" , "" ) .trim() ; byte [] decoded = Base64 .getDecoder() .decode(stringAfter); KeySpec keySpec = new X509EncodedKeySpec(decoded); publicKey = keyFactory.generatePublic(keySpec); } public String encryptToBase64(String plainText) { String encoded = null ; try { Cipher cipher = Cipher.getInstance( "RSA" ); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte [] encrypted = cipher.doFinal(plainText.getBytes()); encoded = Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { e.printStackTrace(); } return encoded; } public String decryptFromBase64(String base64EncodedEncryptedBytes) { String plainText = null ; try { final Cipher cipher = Cipher.getInstance( "RSA" ); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte [] decoded = Base64 .getDecoder() .decode(base64EncodedEncryptedBytes); byte [] decrypted = cipher.doFinal(decoded); plainText = new String(decrypted); } catch (Exception ex) { ex.printStackTrace(); } return plainText; } }建設者
public Rsa4096( String privateKeyClassPathResource , String publicKeyClassPathResource ) throws Exception { setKeyFactory(); setPrivateKey(privateKeyClassPathResource); setPublicKey(publicKeyClassPathResource); }構造函數(shù)很簡單,并帶有2個參數(shù)。 通過參數(shù)名稱,您可以猜測它們是什么。 第一個參數(shù)是OpenSSL生成的私鑰文件的完全限定的類路徑位置。 第二個參數(shù)與公用密鑰文件相同。
為什么要將密鑰文件放在類路徑上? 我正在使用Maven運行單元測試來研究此代碼。 Maven使在類路徑上可用的資源變得容易,所以這就是我在這里使用的。 同樣,這是研究(請參閱免責聲明)!
請記住,目標之一是使用由DevOps和開發(fā)團隊商定并實施的策略使密鑰可用于Java運行時。 因此,您的策略可能有所不同,但最終目標卻是相同的:指向可以讀取文件字節(jié)的位置。
setKeyFactory()
protected void setKeyFactory() throws Exception { this .keyFactory = KeyFactory.getInstance( "RSA" ); }setKeyFactory()方法實例化RSA算法的KeyFactory類。 真的很簡單; 一行代碼。 稍后將使用該對象來構建PrivateKey和PublicKey …畢竟這是工廠類:)
setPrivateKey()
protected void setPrivateKey(String classpathResource) throws Exception { InputStream is = this .getClass() .getClassLoader() .getResourceAsStream(classpathResource); String stringBefore = new String(is.readAllBytes()); String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PRIVATE KEY-----" , "" ) .replaceAll( "-----END PRIVATE KEY-----" , "" ) .trim(); byte [] decoded = Base64 .getDecoder() .decode(stringAfter); KeySpec keySpec = new PKCS8EncodedKeySpec(decoded); privateKey = keyFactory.generatePrivate(keySpec); }setPrivateKey()方法實例化PrivateKey 。 在此方法中, ClassLoader用于獲取InputStream到類路徑上的私鑰文件。 文件的字節(jié)被讀入新的String 。 接下來, String的處理如下:
String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PRIVATE KEY-----" , "" ) .replaceAll( "-----END PRIVATE KEY-----" , "" ) .trim();此處理是必需的,因為即使我們使用OpenSSL生成了具有PKCS8編碼的私鑰文件,該文件也無法被Java直接使用。 如果嘗試不進行上述處理,則會出現(xiàn)以下異常:
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key formatPKCS8EncodedKeySpec類期望私鑰是一行文本,其中刪除了所有注釋(Java代碼示例,…,nd)。 這就是進行處理的原因。
處理除去換行符和注釋后,將使用PKCS8EncodedKeySpec和KeyFactory創(chuàng)建PrivateKey 。
KeySpec keySpec = new PKCS8EncodedKeySpec(decoded); privateKey = keyFactory.generatePrivate(keySpec);setPublicKey()
protected void setPublicKey(String classpathResource) throws Exception { InputStream is = this .getClass() .getClassLoader() .getResourceAsStream(classpathResource); String stringBefore = new String(is.readAllBytes()); String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PUBLIC KEY-----" , "" ) .replaceAll( "-----END PUBLIC KEY-----" , "" ) .trim(); byte [] decoded = Base64 .getDecoder() .decode(stringAfter); KeySpec keySpec = new X509EncodedKeySpec(decoded); publicKey = keyFactory.generatePublic(keySpec); }setPublicKey()方法實例化PublicKey 。 此方法與setPrivateKey()方法幾乎相同,但讓我們看一下細節(jié)。
ClassLoader用于將InputStream獲取到類路徑上的公鑰文件。 文件的字節(jié)被讀入新的String 。 接下來, String的處理如下:
String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PUBLIC KEY-----" , "" ) .replaceAll( "-----END PUBLIC KEY-----" , "" ) .trim();此處理是必需的,因為即使我們使用OpenSSL生成具有X509編碼的私鑰文件,該文件也不能被Java直接使用。 如果嘗試不進行上述處理,則會出現(xiàn)以下異常:
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key formatX509EncodedKeySpec類期望公鑰為一行文本,其中刪除了所有注釋(Java代碼示例,…,nd)。 這就是進行處理的原因。
處理除去換行符和注釋后,將使用X509EncodedKeySpec和KeyFactory創(chuàng)建PublicKey 。
KeySpec keySpec = new X509EncodedKeySpec(decoded); publicKey = keyFactory.generatePublic(keySpec);現(xiàn)在,我們有了由OpenSSL生成的私鑰和公鑰文件創(chuàng)建的PrivateKey和PublicKey實例。 那么您想開始加密和解密嗎? 我們開始做吧!
Java內存中測試
現(xiàn)在是時候將它們放在一起,看看我們是否可以加密和解密一個值了。 但是,如果沒有加密和解密方法,我們將無法做到這一點。 我們首先需要它們。
以下清單是我的Rsa4096類的Rsa4096 。 查看GitHub上的類,或通讀上面的“ Java KeyFactory,PrivateKey,PublicKey”部分,以獲得該類的完整源代碼。 Rsa4096類包含加密和解密方法。 首先讓我們看一下加密方法。
加密
清單5.1 – cryptoToBase64()方法
public String encryptToBase64(String plainText) { String encoded = null ; try { Cipher cipher = Cipher.getInstance( "RSA" ); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte [] encrypted = cipher.doFinal(plainText.getBytes()); encoded = Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { e.printStackTrace(); } return encoded; }清單5.1顯示了encryptToBase64()方法。 該方法具有一個String參數(shù),該參數(shù)是要加密的值。 傳入byte[]數(shù)組可能更健壯,但是根據我的經驗,通常需要對String值進行加密。 當然,為滿足您的需求進行更新。
方法的名稱和返回類型意味著將返回Base64編碼的String。 傳回byte[]數(shù)組可能更健壯,但是根據我的經驗,通常需要String返回值。 當然,為滿足您的需求進行更新。
加密只需要PublicKey 。
解密
清單5.2 – cryptoFromBase64()方法
public String decryptFromBase64(String base64EncodedEncryptedBytes) { String plainText = null ; try { final Cipher cipher = Cipher.getInstance( "RSA" ); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte [] decoded = Base64 .getDecoder() .decode(base64EncodedEncryptedBytes); byte [] decrypted = cipher.doFinal(decoded); plainText = new String(decrypted); } catch (Exception ex) { ex.printStackTrace(); } return plainText; }清單5.2顯示了cryptoFromBase64()方法。 該方法具有一個String參數(shù),該參數(shù)的名稱為加密的byte[]數(shù)組的Base64編碼的String 。 傳遞byte[]數(shù)組可能會更健壯,但是根據我的經驗,通常需要將String解密回其原始值。 當然,為滿足您的需求進行更新。
方法的名稱和返回類型表示將返回原始的String值。 傳回byte[]數(shù)組可能更健壯,但是根據我的經驗,原始值始終是String 。 當然,為滿足您的需求進行更新。
只有PrivateKey需要解密。
單元測試
現(xiàn)在,讓我們看一下InMemoryTest單元測試,看看是否所有功能都可以一起使用。
注意內存中的加密和解密不是我的目標之一。 目標是在應用程序外部使用OpenSSL加密,并在應用程序內部使用Java解密。 但是,首先嘗試在內存中進行測試是確保一切正常的良好測試。
清單5.3 – InMemoryTest單元測試
package org.thoth.rsa; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class InMemoryTest { @Test public void test_in_memory_encryption_decryption() throws Exception { // Setup Rsa4096 rsa = new Rsa4096( "./private_key_rsa_4096_pkcs8-generated.pem" , "./public_key_rsa_4096_pkcs8-exported.pem" ); String expected = "Text to be encrypted" ; // Test String encryptedAndEncoded = rsa.encryptToBase64(expected); String actual = rsa.decryptFromBase64(encryptedAndEncoded); // Assert Assertions.assertEquals(expected, actual); } }清單5.3顯示了InMemoryTest單元測試。 該測試最終運行所有代碼,并驗證String是否可以加密和解密回相同的值。
首先, // Setup單元測試的// Setup指定在哪里可以找到私鑰和公鑰文件。 請記住,這些文件是由OpenSSL生成的。 我將它們放在項目的src/test/resources/目錄中,以便在運行單元測試時它們將顯示在類路徑中。 它們用于創(chuàng)建我的Rsa4096類的實例。
接下來,測試進行加密和解密。 似乎有點反氣候,但是所有工作都在Rsa4096類中。
最后,JUnit斷言檢查期望值等于實際值。 如果一切順利,則測試應通過含義加密,然后解密返回原始值。 克隆我的thoth-rsa存儲庫,并親自運行單元測試以查看它是否有效!
因此,OpenSSL生成的私鑰和公鑰可在Java中用于加密和解密內存中的值。 但是,可以在 Java 外部使用OpenSSL對值進行加密,而在應用程序內部對其進行解密嗎? 試試吧!
加密文件
這項研究的既定目標之一是OpenSSL加密整個文件,而Java應用程序將對其解密。 Java應用程序將值外部化到屬性文件中非常普遍。 盡管最好只加密特定的屬性(在下一節(jié)中介紹),但是加密整個文件是確保不丟失任何敏感屬性的快速簡便的方法。
首先,我們需要加密整個文件。 我們已經有了用于加密的公共密鑰。 因此,剩下的就是正確的OpenSSL命令。 讓我們看一下命令。
文件加密
清單6.1 – OpenSSL加密文件
openssl rsautl -encrypt -inkey public_key_rsa_4096_pkcs8-exported.pem -pubin -in file_unencrypted.txt | openssl enc -A -base64 > file_encrypted_and_encoded.txt清單6.1(admin.2018)顯示了OpenSSL命令,該命令用于將純文本文件的內容加密和Base64編碼為新文件。 請記住,加密時僅需要公用密鑰文件。 因此,在處理敏感數(shù)據時可以保持職責分離。 該命令創(chuàng)建的file_encrypted_and_encoded.txt文件包含一個Base64編碼的字符串,看起來像這樣:
UwXBjowtfDQix2lOiBbaX6J8GayYmo5EsZuHxPUtS+MW9kncnVNpeWw+jpOc1yEiSanFEeRE4QQz/DKWr16LHAt4B8OMOSvXikEpnv0uvr+UtKTE1KalHZDKBHvk5op44gMhhQVpyjKQrVMY/76R83o0/kj60fNsuqpx5DIH/RHhnwBCNvjpjlsvLPPlL1YqUIn0i+t+5XCaZcTiJhpsOh2LmEhfARLgMqVGZxb0zIPvn0zPerhVSZK1wUcI4Va+nOj2rDOflL1Sr5eiimAaIC5/zZniIZP4RDdF3VvlMur5MzUkgxM8CkIJPxKUj8QsEPEcVt3p3/cIvR9YeBmP6Gsw78NutJH3vXAvduPIB2/z/w8iRn/NYcCRX8xZUEGcM44Ks1n7eT+pUWJE1T+3KfH08HOhXuMJUocaxSiZiX2ROQt/gKPJsz27b3u967y9s1DozaaJY+1nKOqEbHDg/uVcgmwYXD5CDy+/qAqKXRJ3dCmJWw46OwPSTMAhkBGOihDhrcQbid3O9rsTU/Od19Fa+OGnS55HHv/4cnIwJnKXBtziG5EaJlouu/H+poabQEoiwgcuh2OOj41Rm6nG3Ef3uxppdoXCn9x3wMDHlqc8K+0Nenc2IbAM //Vd98PVwBf5/nvNyQKwfpQOFJrT4Ygyt3qWQ00cLG7u3fsngg0=大! 加密文件; 校驗! 現(xiàn)在這是一個大問題:Java可以解密嗎? 讓我們找出答案!
單元測試
讓我們看一下EncryptedFileTest單元測試。
清單6.2 – EncryptedFileTest單元測試
package org.thoth.rsa; import java.io.InputStream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class EncryptedFileTest { protected Rsa4096 rsa; @BeforeEach public void setUp() throws Exception { rsa = new Rsa4096( "./private_key_rsa_4096_pkcs8-generated.pem" , "./public_key_rsa_4096_pkcs8-exported.pem" ); } @Test public void test_encrypted_file() throws Exception { // Setup String expected = getFileAsString( "./file_unencrypted.txt" ); String encryptedAndEncoded = getFileAsString( "./file_encrypted_and_encoded.txt" ); // Test String actual = rsa.decryptFromBase64(encryptedAndEncoded); System.out.printf( "%s%n" , actual); // Assert Assertions.assertEquals(expected, actual); } public String getFileAsString(String classPathResourceLocation) throws Exception { InputStream is = this .getClass() .getClassLoader() .getResourceAsStream( classPathResourceLocation ); byte [] bytes = is.readAllBytes(); is.close(); return new String(bytes); } }首先, @BeforeEach方法創(chuàng)建我的Rsa4096類的實例。 這將使用OpenSSL生成的私鑰和公鑰文件。 單元測試運行時,這些密鑰文件位于Java類路徑上。 Rsa4096用于解碼和解密加密文件的內容。
其次,調用getFileAsString()幫助方法。 方法的名稱準確說明了它的作用。 它在Java類路徑上找到一個文件,并將其內容讀取為String 。 請記住,OpenSSL文件加密命令既對加密也對Base64編碼,對輸出文件的內容進行編碼,因此將這些內容存儲為String是安全的。
第三, Rsa4096用于通過調用decryptFromBase64()進行解碼和解密。
最后,JUnit斷言確保解碼和解密成功,并且測試返回原始值。
而已。 我們做到了! 但這還不是全部。 當然,加密整個文件很有趣,但是更有趣的是僅加密文件中的特定值。 無法做到這一點……還是可以? 讓我們來看看。
文件中的加密值
這項研究的另一個目標是使用OpenSSL僅加密文件中的特定值。 為此,必須有一個包含占位符以替換變量的起始模板文件。 它們將被加密和編碼的值替換。 OpenSSL將用于加密和編碼,但是我們還需要使用sed進行搜索和替換。 讓我們來看看。
價值加密
清單7.1 – OpenSSL加密文件中的值
sed "s|XXXX|`printf " SECRET " | openssl rsautl -encrypt -inkey public_key_rsa_4096_pkcs8-exported.pem -pubin | openssl enc -A -base64`|g" some_template.properties > some_tmp1.properties sed "s|YYYY|`printf " 123 - 45 - 7890 " | openssl rsautl -encrypt -inkey public_key_rsa_4096_pkcs8-exported.pem -pubin | openssl enc -A -base64`|g" some_tmp1.properties > some_app.properties清單7.1通過管道Unix命令獲得了一些信息,因此讓我們來一小段來看一下。
首先,從some_template.properties文件開始。 這是一個標準的Java屬性文件,但是文件中的某些屬性沒有值,它們具有用于替換變量的占位符:
name=mike color=blue password=XXXX size=L ssn=YYYY price= 4.99如您所見, password和ssn具有用于加密敏感信息的占位符。 XXXX和YYYY應該被替換。
其次,該命令的sed "s|XXXX|`printf "SECRET"部分顯然會搜索并用純文本SECRET替換XXXX 。需要注意的是,由于這些命令都相互夾住,因此敏感文本永遠不會寫入文件。
第三,輸出文件是some_tmp1.properties 。 該文件的名稱適當,因為它只是臨時的 。 該模板有兩個值需要替換。 第一個命令僅在XXXX上進行搜索和替換。 臨時文件如下所示:
name=mike color=blue Password=sh3kiZTGtvcPlY3eqnUSkIC+HplryBs....= size=L ssn=YYYY price= 4.99四,第二個命令sed "s|YYYY|`printf "123-45-7890" ,并輸入文件some_tmp1.properties輸出寫入。 some_app.properties的。 some_app.properties文件現(xiàn)在已經可以使用由于所有敏感數(shù)據均已加密,編碼并放置在文件中,因此應用程序可以some_app.properties該文件some_app.properties現(xiàn)在如下所示:
name=mike color=blue Password=sh3kiZTGtvcPlY3eqnUSk....= size=L ssn=trpmRDvKnnjuT6hZvObthguN3A....= price= 4.99單元測試
EncryptedValuesInPropertiesFileTest是我們要看的最后一個單元測試。
清單7.2 – EncryptedValuesInPropertiesFileTest單元測試
package org.thoth.rsa; import java.util.Properties; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class EncryptedValuesInPropertiesFileTest { protected Rsa4096 rsa; @BeforeEach public void setUp() throws Exception { rsa = new Rsa4096( "./private_key_rsa_4096_pkcs8-generated.pem" , "./public_key_rsa_4096_pkcs8-exported.pem" ); } @Test public void test_encrypted_values_in_properties_file() throws Exception { // Setup Properties encryptedAndEncoded = new Properties(); encryptedAndEncoded.load( this .getClass() .getClassLoader() .getResourceAsStream( "./some_app.properties" ) ); // Test String passwordActual = rsa.decryptFromBase64( encryptedAndEncoded.getProperty( "password" ) ); String ssnActual = rsa.decryptFromBase64( encryptedAndEncoded.getProperty( "ssn" ) ); // Assert Assertions.assertEquals( "SECRET" , passwordActual); Assertions.assertEquals( "123-45-7890" , ssnActual); } }清單7.2顯示了EncryptedValuesInPropertiesFileTest單元測試。 該測試將讀取some_app.properties文件,并希望它能夠解碼和解密其中的值。
首先, @BeforeEach方法創(chuàng)建我的Rsa4096類的實例。 這將使用OpenSSL生成的私鑰和公鑰文件。 單元測試運行時,這些密鑰文件位于Java類路徑上。 Rsa4096用于解碼和解密加密文件的內容。
其次,創(chuàng)建一個Properties對象,并調用load()以將其與屬性文件的內容一起加載。 請記住,在類路徑上可以找到some_app.properties文件。
第三,從Properties對象檢索加密和編碼后的值,然后使用Rsa4096通過調用decryptFromBase64()對其進行解碼和解密。
最后,JUnit斷言確保解碼和解密成功,并且測試返回原始值。
而已。 我們做到了! 我們設定要實現(xiàn)的所有目標均已實現(xiàn)。 只是為了確保讓我們回顧一下。
摘要
本博客的目的是演示Java與OpenSSL的互操作性:
- 使用OpenSSL生成私鑰和公鑰
- 使用OpenSSL加密值
- 用Java解密值
我能夠通過定義和實現(xiàn)以下目標來證明這一點:
選擇的加密工具是OpenSSL。 它在每個Linux / Unix系統(tǒng)上都是行業(yè)標準,并且對于所有DevOps團隊都是熟悉的。 我演示了執(zhí)行所有必需操作的OpenSSL命令。 在某些情況下, openssl不能獨自完成所有操作,該命令已通過管道傳遞給sed等其他標準Linux / Unix工具。
由DevOps或其他團隊執(zhí)行的加密,因此職責分離。 開發(fā)團隊中沒有人會知道未加密的價值。 我演示了此示例,其中顯示了分別用于生成私鑰和公鑰文件以及用于加密文件或值的命令。 作為單獨的命令,如果需要,可以將職責分開。
所有環(huán)境都將使用自己的密鑰。 沒有密鑰共享。 我通過展示執(zhí)行用于生成密鑰的命令有多么容易來證明了這一點。 這些命令甚至可以由基礎結構作為每個環(huán)境的編碼過程來自動化。
可以隨時重新生成所有密鑰和加密值,而無需更改應用程序。 當運行單元測試時,Maven可以輕松地將文件添加到類路徑中,而我正是利用這一點來開發(fā)測試。 我希望很明顯,即使您像我一樣使用類路徑策略,重新生成所有密鑰和加密值也是微不足道的。 重新啟動應用程序將重新讀取所有內容。 無需更改應用程序。 請記住,您可以創(chuàng)建自己的策略并編寫代碼以支持該策略,這也使“無變化”目標成為不可能……請不要這樣做:)
加密將是整個文件或(屬性)文件中的特定值。 我用OpenSSL命令演示了這兩者。 我還提供了EncryptedFileTest和EncryptedValuesInPropertiesFileTest單元測試,以證明其有效。
使用DevOps和開發(fā)團隊共同商定并實施的策略,加密的值和密鑰可用于Java運行時。 我通過確定我的代碼將利用Maven的將文件放在類路徑中的能力來證明這一點。 因此,我的策略是從類路徑中讀取文件。 當然,您可以決定自己的策略并更新代碼以支持它。
Java應用程序出于其所需的任何目的執(zhí)行解密。 不要記錄加密值! 我用Rsa4096類演示了這一點,該類執(zhí)行解碼和解密。 另外-這非常重要-我從不記錄任何Rsa4096類或單元測試中的解碼和解密值。
而已! 感謝您和我一起旅行。 這是一個有趣的研究主題,我希望您通過閱讀這篇文章能找到一些價值。 給我發(fā)電子郵件或發(fā)表評論,讓我知道。
參考文獻
Remijan,M.(2017年12月22日)。 選擇Java加密算法第3部分–公鑰/私鑰非對稱加密。 取自http://mjremijan.blogspot.com/2017/12/choosing-java-cryptographic-algorithms_5.html 。
java.security.PrivateKey的 Java代碼示例。 (nd)取自http://www.javased.com/index.php?api=java.security.PrivateKey
德斯坦 (2017年10月1日)。 ParseRSAKeys.java。 取自https://gist.github.com/destan/b708d11bd4f403506d6d5bb5fe6a82c5
管理員。 (2018年8月21日)。 在Linux上使用OpenSSL加密消息和文件。 取自https://linuxconfig.org/using-openssl-to-encrypt-messages-and-files-on-linux
java.security.spec.PKCS8EncodedKeySpec的Java代碼示例。 (nd)取自https://www.programcreek.com/java-api-examples/java.security.spec.PKCS8EncodedKeySpec
翻譯自: https://www.javacodegeeks.com/2020/04/encrypt-with-openssl-decrypt-with-java-using-openssl-rsa-public-private-keys.html
總結
以上是生活随笔為你收集整理的使用OpenSSL加密,使用Java解密,使用OpenSSL RSA公钥的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微软重申 AI 工具 Copilot 不
- 下一篇: 为何电脑配置很高但运行软件很卡?