抽象
在2017年,我撰写了一个由三部分组成的系列文章,内容涉及选择最佳的哈希和加密算法。 在对该系列进行研究时,我学到了很多有关哈希和加密的知识。 我学到的最重要的事情是,尽管我必须对如何使用最安全的算法进行自我教育,但我也必须将这些算法的开发工作留给专家。 话虽如此,我开始考虑Java与加密专家(特别是OpenSSL)的互操作性。 我的第3部分系列仅从Java的角度着眼于加密。 我想知道Java与OpenSSL之类的工具进行互操作将有多么困难。 本博客的目的是演示Java与OpenSSL的互操作性:
- 使用OpenSSL生成私钥和公钥
- 使用OpenSSL加密值
- 用Java解密值
免责声明
这篇文章仅供参考。 在使用所提供的任何信息之前,请认真思考。 从中学到东西,但最终自己做出决定,风险自负。
要求
我使用以下主要技术完成了本文的所有工作。 您可能可以使用不同的技术或版本来做相同的事情,但不能保证。
- OpenJDK运行时环境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捆绑在一起)
<dependencies><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-api</artifactId><version>5.5.2</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-params</artifactId><version>5.5.2</version><scope>test</scope></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter-engine</artifactId><version>5.5.2</version><scope>test</scope></dependency>
</dependencies>
<pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-clean-plugin</artifactId><version>2.5</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>2.6</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><debug>true</debug></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.0.0-M4</version><configuration><argLine>-Dfile.encoding=UTF8</argLine></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.4</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-install-plugin</artifactId><version>2.4</version></plugin></plugins>
</pluginManagement>
下载
访问我的GitHub页面https://github.com/mjremijan以查看我所有的开源项目。 这篇文章的代码位于: https : //github.com/mjremijan/thoth-rsa
背景
当我使用Microservices将Monolith应用程序模块化时,我开始怀疑是否能够互操作OpenSSL和Java。 使用微服务时,应用程序仍然需要加密和解密敏感的配置数据(例如数据库密码),但是微服务使用的小型运行时环境给这带来了挑战。
借助Monolith架构,Java / Jakarta EE应用程序服务器可以处理应用程序的加密和解密。 诸如数据库连接池之类的托管资源是在EE应用程序服务器中配置的,其他其他加密值通常可以存储在JNDI中。 在这两种情况下,服务器都提供加密和解密功能,而应用程序不知道任何细节。 应用服务器可以为应用提供托管资源或解密值。
但是,在微服务架构中,运行时(例如Spring Boot)保持“较小”状态,并且不提供与EE应用程序服务器一样多的功能。 数据库连接就是一个很好的例子。 在Spring Boot中配置数据库连接很容易,但是您如何支持密码加密和解密? 现在,它必须得到DevOps和开发团队的支持。
注意其他微服务技术(例如Kubernetes)正在努力填补空白,并提供类似于EE应用程序服务器的加密功能。
所以这让我开始思考。 DevOps生活在Linux / Unix世界中。 开发人员生活在Java世界中。 为什么不将两个世界放在一起以支持加密/解密策略? 这将使DevOps和开发人员能够尽自己所能。 为此,我首先需要明确定义目标。
目标
从Monolith架构迁移到微服务很慢。 是的,存在用于加密和解密的微服务基础结构解决方案。 但是,如果该基础结构不可用,则在3-5年的过渡期内不会为您提供帮助。 为了支持过渡,我决定了以下目标。
- 选择的加密工具是OpenSSL。 它在每个Linux / Unix系统上都是行业标准,并且对于所有DevOps团队都是熟悉的。
- 由DevOps或其他团队执行的加密,因此职责分离。 开发团队中没有人会知道未加密的价值。
- 所有环境都将使用自己的密钥。 没有密钥共享。
- 可以随时重新生成所有密钥和加密值,而无需更改应用程序。
- 加密将是整个文件或(属性)文件中的特定值。
- 使用DevOps和开发团队共同商定并实施的策略,加密的值和密钥可用于Java运行时。
- Java应用程序出于其所需的任何目的执行解密。 不要记录加密值!
牢记这些目标,让我们开始一段旅程。
使用哪种算法
我需要回答的第一个问题是要使用哪种加密算法。 对于加密,我可以选择单密钥对称加密还是公共/私有密钥非对称加密。 我的选择是:
RSA-4096公钥/私钥非对称加密
选择非对称加密算法的原因是因为公钥/私钥允许最大程度的责任分离。 可能会有独立的团队来生成密钥,加密值以及将所有内容放在一起以供运行时使用。 实际上,这可以全部由一个团队甚至一个人完成,但是非对称加密算法可以灵活地分离这些问题。
至于使用RSA-4096算法,根据我的研究,它是当今最好,最安全的(Remijan,2017)。
现在我们知道要使用哪种算法。 接下来,我们将研究生成私钥。
OpenSSL生成私钥
在Java中, PKCS8EncodedKeySpec
类期望使用PKCS8
编码的RSA私钥。 (Java代码,nd)。 我发现了使用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-----
大! 私钥已生成! 现在让我们继续生成公共密钥。
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才能使用这些密钥文件。 这样做,我们需要创建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); }
构造函数很简单,并带有2个参数。 通过参数名称,您可以猜测它们是什么。 第一个参数是OpenSSL生成的私钥文件的完全限定的类路径位置。 第二个参数与公用密钥文件相同。
为什么要将密钥文件放在类路径上? 我正在使用Maven运行单元测试来研究此代码。 Maven使在类路径上可用的资源变得容易,所以这就是我在这里使用的。 同样,这是研究(请参阅免责声明)!
请记住,目标之一是使用由DevOps和开发团队商定并实施的策略使密钥可用于Java运行时。 因此,您的策略可能有所不同,但最终目标却是相同的:指向可以读取文件字节的位置。
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
到类路径上的私钥文件。 文件的字节被读入新的String
。 接下来, String
的处理如下:
String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PRIVATE KEY-----" , "" ) .replaceAll( "-----END PRIVATE KEY-----" , "" ) .trim();
此处理是必需的,因为即使我们使用OpenSSL生成了具有PKCS8
编码的私钥文件,该文件也无法被Java直接使用。 如果尝试不进行上述处理,则会出现以下异常:
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format
PKCS8EncodedKeySpec
类期望私钥是一行文本,其中删除了所有注释(Java代码示例,…,nd)。 这就是进行处理的原因。
处理除去换行符和注释后,将使用PKCS8EncodedKeySpec
和KeyFactory
创建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()
方法几乎相同,但让我们看一下细节。
ClassLoader
用于将InputStream
获取到类路径上的公钥文件。 文件的字节被读入新的String
。 接下来, String
的处理如下:
String stringAfter = stringBefore .replaceAll( "\\n" , "" ) .replaceAll( "-----BEGIN PUBLIC KEY-----" , "" ) .replaceAll( "-----END PUBLIC KEY-----" , "" ) .trim();
此处理是必需的,因为即使我们使用OpenSSL生成具有X509
编码的私钥文件,该文件也不能被Java直接使用。 如果尝试不进行上述处理,则会出现以下异常:
java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format
X509EncodedKeySpec
类期望公钥为一行文本,其中删除了所有注释(Java代码示例,…,nd)。 这就是进行处理的原因。
处理除去换行符和注释后,将使用X509EncodedKeySpec
和KeyFactory
创建PublicKey
。
KeySpec keySpec = new X509EncodedKeySpec(decoded); publicKey = keyFactory.generatePublic(keySpec);
现在,我们有了由OpenSSL生成的私钥和公钥文件创建的PrivateKey
和PublicKey
实例。 那么您想开始加密和解密吗? 我们开始做吧!
Java内存中测试
现在是时候将它们放在一起,看看我们是否可以加密和解密一个值了。 但是,如果没有加密和解密方法,我们将无法做到这一点。 我们首先需要它们。
以下清单是我的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
参数,该参数是要加密的值。 传入byte[]
数组可能更健壮,但是根据我的经验,通常需要对String
值进行加密。 当然,为满足您的需求进行更新。
方法的名称和返回类型意味着将返回Base64
编码的String。 传回byte[]
数组可能更健壮,但是根据我的经验,通常需要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
参数,该参数的名称为加密的byte[]
数组的Base64
编码的String
。 传递byte[]
数组可能会更健壮,但是根据我的经验,通常需要将String
解密回其原始值。 当然,为满足您的需求进行更新。
方法的名称和返回类型表示将返回原始的String
值。 传回byte[]
数组可能更健壮,但是根据我的经验,原始值始终是String
。 当然,为满足您的需求进行更新。
只有PrivateKey
需要解密。
单元测试
现在,让我们看一下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/
目录中,以便在运行单元测试时它们将显示在类路径中。 它们用于创建我的Rsa4096
类的实例。
接下来,测试进行加密和解密。 似乎有点反气候,但是所有工作都在Rsa4096
类中。
最后,JUnit断言检查期望值等于实际值。 如果一切顺利,则测试应通过含义加密,然后解密返回原始值。 克隆我的thoth-rsa存储库,并亲自运行单元测试以查看它是否有效!
因此,OpenSSL生成的私钥和公钥可在Java中用于加密和解密内存中的值。 但是,可以在 Java 外部使用OpenSSL对值进行加密,而在应用程序内部对其进行解密吗? 试试吧!
加密文件
这项研究的既定目标之一是OpenSSL加密整个文件,而Java应用程序将对其解密。 Java应用程序将值外部化到属性文件中非常普遍。 尽管最好只加密特定的属性(在下一节中介绍),但是加密整个文件是确保不丢失任何敏感属性的快速简便的方法。
首先,我们需要加密整个文件。 我们已经有了用于加密的公共密钥。 因此,剩下的就是正确的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
编码为新文件。 请记住,加密时仅需要公用密钥文件。 因此,在处理敏感数据时可以保持职责分离。 该命令创建的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=
大! 加密文件; 校验! 现在这是一个大问题: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
方法创建我的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
文件现在已经可以使用由于所有敏感数据均已加密,编码并放置在文件中,因此应用程序可以some_app.properties
该文件some_app.properties
现在如下所示:
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
方法创建我的Rsa4096
类的实例。 这将使用OpenSSL生成的私钥和公钥文件。 单元测试运行时,这些密钥文件位于Java类路径上。 Rsa4096
用于解码和解密加密文件的内容。
其次,创建一个Properties
对象,并调用load()
以将其与属性文件的内容一起加载。 请记住,在类路径上可以找到some_app.properties
文件。
第三,从Properties
对象检索加密和编码后的值,然后使用Rsa4096
通过调用decryptFromBase64()
对其进行解码和解密。
最后,JUnit断言确保解码和解密成功,并且测试返回原始值。
而已。 我们做到了! 我们设定要实现的所有目标均已实现。 只是为了确保让我们回顾一下。
摘要
本博客的目的是演示Java与OpenSSL的互操作性:
- 使用OpenSSL生成私钥和公钥
- 使用OpenSSL加密值
- 用Java解密值
我能够通过定义和实现以下目标来证明这一点:
选择的加密工具是OpenSSL。 它在每个Linux / Unix系统上都是行业标准,并且对于所有DevOps团队都是熟悉的。 我演示了执行所有必需操作的OpenSSL命令。 在某些情况下, openssl
不能独自完成所有操作,该命令已通过管道传递给sed
等其他标准Linux / Unix工具。
由DevOps或其他团队执行的加密,因此职责分离。 开发团队中没有人会知道未加密的价值。 我演示了此示例,其中显示了分别用于生成私钥和公钥文件以及用于加密文件或值的命令。 作为单独的命令,如果需要,可以将职责分开。
所有环境都将使用自己的密钥。 没有密钥共享。 我通过展示执行用于生成密钥的命令有多么容易来证明了这一点。 这些命令甚至可以由基础结构作为每个环境的编码过程来自动化。
可以随时重新生成所有密钥和加密值,而无需更改应用程序。 当运行单元测试时,Maven可以轻松地将文件添加到类路径中,而我正是利用这一点来开发测试。 我希望很明显,即使您像我一样使用类路径策略,重新生成所有密钥和加密值也是微不足道的。 重新启动应用程序将重新读取所有内容。 无需更改应用程序。 请记住,您可以创建自己的策略并编写代码以支持该策略,这也使“无变化”目标成为不可能……请不要这样做:)
加密将是整个文件或(属性)文件中的特定值。 我用OpenSSL命令演示了这两者。 我还提供了EncryptedFileTest
和EncryptedValuesInPropertiesFileTest
单元测试,以证明其有效。
使用DevOps和开发团队共同商定并实施的策略,加密的值和密钥可用于Java运行时。 我通过确定我的代码将利用Maven的将文件放在类路径中的能力来证明这一点。 因此,我的策略是从类路径中读取文件。 当然,您可以决定自己的策略并更新代码以支持它。
Java应用程序出于其所需的任何目的执行解密。 不要记录加密值! 我用Rsa4096
类演示了这一点,该类执行解码和解密。 另外-这非常重要-我从不记录任何Rsa4096
类或单元测试中的解码和解密值。
而已! 感谢您和我一起旅行。 这是一个有趣的研究主题,我希望您通过阅读这篇文章能找到一些价值。 给我发电子邮件或发表评论,让我知道。
参考文献
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