使用OpenSSL加密,使用Java解密,使​​用OpenSSL RSA公钥

抽象

在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年的过渡期内,这些将无法为您提供帮助。 为了支持过渡,我决定了以下目标。

  1. 选择的加密工具是OpenSSL。 它在每个Linux / Unix系统上都是行业标准,并且对于所有DevOps团队都是熟悉的。
  2. 由DevOps或其他团队执行的加密,因此职责分离。 开发团队中没有人会知道未加密的价值。
  3. 所有环境都将使用自己的密钥。 没有密钥共享。
  4. 可以随时重新生成所有密钥和加密值,而无需更改应用程序。
  5. 加密将是整个文件或(属性)文件中的特定值。
  6. 使用DevOps和开发团队共同商定并实施的策略,加密值和密钥可用于Java运行时。
  7. 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使用这些密钥文件。 这样做,我们需要创建KeyFactoryPrivateKeyPublicKey对象的实例。 让我们深入一些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生成的私钥文件的完全限定的Class Path位置。 第二个参数与公用密钥文件相同。

为什么要将密钥文件放在类路径上? 我正在使用Maven运行单元测试来研究此代码。 Maven使在类路径上可用的资源变得容易,所以这就是我在这里使用的。 同样,这是研究(请参阅免责声明)!

请记住,目标之一是使用由DevOps和Development团队达成一致并实施的策略,将密钥提供给Java运行时。 因此,您的策略可能有所不同,但最终目标却是相同的:指向可以读取文件字节的位置。

setKeyFactory()

 protected void setKeyFactory() throws Exception {

    this .keyFactory = KeyFactory.getInstance( "RSA" );

  }

setKeyFactory()方法实例化RSA算法的KeyFactory类。 真的很简单; 一行代码。 稍后将使用该对象来构建PrivateKeyPublicKey …毕竟这是工厂类:)

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)。 这就是进行处理的原因。

处理除去换行符和注释后,将使用PKCS8EncodedKeySpecKeyFactory创建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)。 这就是进行处理的原因。

处理除去换行符和注释后,将使用X509EncodedKeySpecKeyFactory创建PublicKey

 KeySpec keySpec

      = new X509EncodedKeySpec(decoded); 
    publicKey = keyFactory.generatePublic(keySpec);

现在,我们有由OpenSSL生成的私钥和公钥文件创建的PrivateKeyPublicKey实例。 那么您想开始加密和解密吗? 我们开始做吧!

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

如您所见, passwordssn具有用于加密敏感信息的占位符。 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命令演示了这两者。 我还提供了EncryptedFileTestEncryptedValuesInPropertiesFileTest单元测试,以证明其有效。

使用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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值