这是涵盖 Java 加密算法的三部分博客系列的第 2 部分。该系列涵盖了如何实现以下内容:
第 2 篇文章详细介绍了如何实现单密钥、对称、AES-256 加密。让我们开始吧。
这篇文章仅供参考。在使用所提供的任何信息之前,请认真思考。从中吸取教训,但最终做出自己的决定需要您自担风险。
我使用以下主要技术完成了这篇文章的所有工作。您也许可以使用不同的技术或版本做同样的事情,但不能保证。
访问我的 GitHub 页面,查看我所有的开源项目。这篇文章的代码位于项目中:thoth-cryptography
对称加密算法基于单个密钥。这一个密钥用于加密和解密。因此,对称算法只应在实施严格控制以保护密钥的情况下使用。
对称算法通常用于安全环境中的数据加密和解密。一个很好的例子是保护微服务通信。如果 OAuth–2/JWT 架构超出范围,API 网关可以使用对称算法的单一密钥来加密令牌。然后将此令牌传递给其他微服务。其他微服务使用相同的密钥来解密令牌。另一个很好的例子是嵌入在电子邮件中的超链接。电子邮件中的超链接包含一个编码令牌,允许在单击超链接时自动处理登录请求。此令牌是由对称算法生成的高度加密值,因此只能在应用服务器上对其进行解码。当然,无论何时需要保护任何类型的密码或凭据,都会使用对称算法对其进行加密,之后可以使用相同的密钥对字节进行解密。
截至今天所做的研究似乎表明最好和最安全的单密钥、对称加密算法如下(Sheth,2017 年,“选择正确的算法”,第 2 段):
AES–256 使用 256 位密钥,需要安装 Java Cryptography Extension (JCE) Unlimited Strength 包。让我们看一个例子。
注意:Java 加密扩展 (JCE) 无限强度 包是 256 位密钥所必需的。如果未安装,则最大为 128 位密钥。
如果您还没有,请下载并安装 Java Cryptography Extension (JCE) Unlimited Strength 包。要求使用 256 位密钥。否则,必须更新以下示例以使用 128 位密钥。
清单 1 是 AesTest.java 单元测试。这是对以下内容的完整演示:
清单 2 显示了 AesSecretKeyProducer.java。这是一个帮助程序类,负责生成新密钥或从 byte[]
复制现有密钥。
清单 3 显示了 ByteArrayWriter.java,清单 4 显示了 ByteArrayReader.java。这些是负责将 byte[]
读取和写入文件的辅助类。由您决定如何存储密钥的 byte[]
,但它需要安全地存储在某个地方(文件、数据库、git 存储库等)。
最后,清单 5 显示了 Aes.java。这是一个帮助程序类,负责加密和解密。
清单 1 – AesTest.java 类
package org.thoth.crypto.symmetric; import java.io.ByteArrayOutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; import javax.crypto.SecretKey; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.thoth.crypto.io.ByteArrayReader; import org.thoth.crypto.io.ByteArrayWriter; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class AesTest { static Path secretKeyFile; @BeforeClass public static void beforeClass() throws Exception { // Store the SecretKey bytes in the ./target diretory. Do // this so it will be ignore by source control. We don't // want this file committed. secretKeyFile = Paths.get("./target/Aes256.key").toAbsolutePath(); // Generate a SecretKey for the test SecretKey secretKey = new AesSecretKeyProducer().produce(); // Store the byte[] of the SecretKey. This is the // "private key file" you want to keep safe. ByteArrayWriter writer = new ByteArrayWriter(secretKeyFile); writer.write(secretKey.getEncoded()); } @Test public void encrypt_and_decrypt_using_same_Aes256_instance() { // setup SecretKey secretKey = new AesSecretKeyProducer().produce( new ByteArrayReader(secretKeyFile).read() ); Aes aes = new Aes(secretKey); String toEncrypt = "encrypt me"; // run byte[] encryptedBytes = aes.encrypt(toEncrypt, Optional.empty()); String decrypted = aes.decrypt(encryptedBytes, Optional.empty()); // assert Assert.assertEquals(toEncrypt, decrypted); } public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance() { // setup SecretKey secretKey = new AesSecretKeyProducer().produce( new ByteArrayReader(secretKeyFile).read() ); Aes aes = new Aes(secretKey); String toEncrypt = "encrypt me aad"; // run byte[] encryptedBytes = aes.encrypt(toEncrypt, Optional.of("JUnit AAD")); String decrypted = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD")); // assert Assert.assertEquals(toEncrypt, decrypted); } @Test public void encrypt_and_decrypt_using_different_Aes256_instance() throws Exception { // setup SecretKey secretKey = new AesSecretKeyProducer().produce( new ByteArrayReader(secretKeyFile).read() ); Aes aesForEncrypt = new Aes(secretKey); Aes aesForDecrypt = new Aes(secretKey); String toEncrypt = "encrypt me"; // run byte[] encryptedBytes = aesForEncrypt.encrypt(toEncrypt, Optional.empty()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(encryptedBytes); String decrypted = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty()); // assert Assert.assertEquals(toEncrypt, decrypted); } }
清单 2 – AesSecretKeyProducer.java 类
package org.thoth.crypto.symmetric; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class AesSecretKeyProducer { /** * Generates a new AES-256 bit {@code SecretKey}. * * @return {@code SecretKey}, never null * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException} */ public SecretKey produce() { KeyGenerator keyGen; try { keyGen = KeyGenerator.getInstance("AES"); keyGen.init(256); SecretKey secretKey = keyGen.generateKey(); return secretKey; } catch (Exception ex) { throw new RuntimeException(ex); } } /** * Generates an AES-256 bit {@code SecretKey}. * * @param encodedByteArray The bytes this method will use to regenerate a previously created {@code SecretKey} * * @return {@code SecretKey}, never null * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException} */ public SecretKey produce(byte [] encodedByteArray) { try { return new SecretKeySpec(encodedByteArray, "AES"); } catch (Exception ex) { throw new RuntimeException(ex); } } }
清单 3 – ByteArrayWriter.java 类
package org.thoth.crypto.io; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class ByteArrayWriter { protected Path outputFile; private void initOutputFile(Path outputFile) { this.outputFile = outputFile; } private void initOutputDirectory() { Path outputDirectory = outputFile.getParent(); if (!Files.exists(outputDirectory)) { try { Files.createDirectories(outputDirectory); } catch (IOException e) { throw new RuntimeException(e); } } } public ByteArrayWriter(Path outputFile) { initOutputFile(outputFile); initOutputDirectory(); } public void write(byte[] bytesArrayToWrite) { try ( OutputStream os = Files.newOutputStream(outputFile); PrintWriter writer = new PrintWriter(os); ){ for (int i=0; i<bytesArrayToWrite.length; i++) { if (i>0) { writer.println(); } writer.print(bytesArrayToWrite[i]); } } catch (IOException ex) { throw new RuntimeException(ex); } } }
清单 4 – ByteArrayReader.java 类
package org.thoth.crypto.io; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Path; import java.util.Scanner; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class ByteArrayReader { protected Path inputFile; public ByteArrayReader(Path inputFile) { this.inputFile = inputFile; } public byte[] read() { try ( Scanner scanner = new Scanner(inputFile); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ){ while (scanner.hasNext()) { baos.write(Byte.parseByte(scanner.nextLine())); } baos.flush(); return baos.toByteArray(); } catch (IOException ex) { throw new RuntimeException(ex); } } }
清单 5 – Aes.java 类
package org.thoth.crypto.symmetric; import java.security.SecureRandom; import java.util.Optional; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; /** * * @author Michael Remijan mjremijan@yahoo.com @mjremijan */ public class Aes { // If you don't have the Java Cryptography Extension // (JCE) Unlimited Strength packaged installed, use // a 128 bit KEY_SIZE. public static int KEY_SIZE = 256; public static int IV_SIZE = 12; // 12bytes * 8 = 96bits public static int TAG_BIT_SIZE = 128; public static String ALGORITHM_NAME = "AES"; public static String MODE_OF_OPERATION = "GCM"; public static String PADDING_SCHEME = "PKCS5Padding"; protected SecretKey secretKey; protected SecureRandom secureRandom; public Aes(SecretKey secretKey) { this.secretKey = secretKey; this.secureRandom = new SecureRandom(); } public byte[] encrypt(String message, Optional<String> aad) { try { // Transformation specifies algortihm, mode of operation and padding Cipher c = Cipher.getInstance( String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME) ); // Generate IV byte iv[] = new byte[IV_SIZE]; secureRandom.nextBytes(iv); // SecureRandom initialized using self-seeding // Initialize GCM Parameters GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv); // Init for encryption c.init(Cipher.ENCRYPT_MODE, secretKey, spec, secureRandom); // Add AAD tag data if present aad.ifPresent(t -> { try { c.updateAAD(t.getBytes("UTF-8")); } catch (Exception e) { throw new RuntimeException(e); } }); // Add message to encrypt c.update(message.getBytes("UTF-8")); // Encrypt byte[] encryptedBytes = c.doFinal(); // Concatinate IV and encrypted bytes. The IV is needed later // in order to to decrypt. The IV value does not need to be // kept secret, so it's OK to encode it in the return value // // Create a new byte[] the combined length of IV and encryptedBytes byte[] ivPlusEncryptedBytes = new byte[iv.length + encryptedBytes.length]; // Copy IV bytes into the new array System.arraycopy(iv, 0, ivPlusEncryptedBytes, 0, iv.length); // Copy encryptedBytes into the new array System.arraycopy(encryptedBytes, 0, ivPlusEncryptedBytes, iv.length, encryptedBytes.length); // Return return ivPlusEncryptedBytes; } catch (Exception e) { throw new RuntimeException(e); } } public String decrypt(byte[] ivPlusEncryptedBytes, Optional<String> aad) { try { // Get IV byte iv[] = new byte[IV_SIZE]; System.arraycopy(ivPlusEncryptedBytes, 0, iv, 0, IV_SIZE); // Initialize GCM Parameters GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv); // Transformation specifies algortihm, mode of operation and padding Cipher c = Cipher.getInstance( String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME) ); // Get encrypted bytes byte [] encryptedBytes = new byte[ivPlusEncryptedBytes.length - IV_SIZE]; System.arraycopy(ivPlusEncryptedBytes, IV_SIZE, encryptedBytes, 0, encryptedBytes.length); // Init for decryption c.init(Cipher.DECRYPT_MODE, secretKey, spec, secureRandom); // Add AAD tag data if present aad.ifPresent(t -> { try { c.updateAAD(t.getBytes("UTF-8")); } catch (Exception e) { throw new RuntimeException(e); } }); // Add message to decrypt c.update(encryptedBytes); // Decrypt byte[] decryptedBytes = c.doFinal(); // Return return new String(decryptedBytes, "UTF-8"); } catch (Exception e) { throw new RuntimeException(e); } } }
加密并不容易。简单的示例将导致您的应用程序具有安全漏洞的实现。如果您需要单个密钥、对称加密算法,请使用具有 256 位密钥和 96 位 IV 的密码 AES/GCM/PKCS5Padding。
标签2: Java教程地址:https://www.cundage.com/article/jcg-choosing-java-cryptographic-algorithms-part-2-single-key-symmetric-encryption.html