抽象
这是涵盖Java加密算法的三部分博客系列的第2部分。 本系列介绍如何实现以下目标:
- 使用SHA–512散列
- AES–256
- RSA–4096
这第二篇文章详细介绍了如何实现单密钥对称AES-256加密。 让我们开始吧。
免责声明
这篇文章仅供参考。 在使用所提供的任何信息之前,请认真思考。 从中学到东西,但最终自己做出决定,风险自负。
要求
我使用以下主要技术完成了本文的所有工作。 您可能可以使用不同的技术或版本来做相同的事情,但不能保证。
- Java 1.8.0_152_x64
- Java密码术扩展(JCE)的无限强度
- NetBeans 8.2(内部版本201609300101)
- Maven 3.0.5(与NetBeans捆绑在一起)
下载
访问我的GitHub页面以查看我所有的开源项目。 这篇文章的代码位于项目中: thoth-cryptography
对称加密
关于
对称加密算法基于单个密钥。 此密钥用于加密和解密。 因此,对称算法仅应在严格控制密钥的地方使用。
对称算法通常用于安全环境中的数据加密和解密。 一个很好的例子是确保微服务通信的安全。 如果OAuth-2 / JWT体系结构超出范围,则API网关可以使用对称算法的单个密钥来加密令牌。 然后将此令牌传递给其他微服务。 其他微服务使用相同的密钥来解密令牌。 另一个很好的例子是电子邮件中嵌入的超链接。 电子邮件中的超链接包含一个编码的令牌,当单击该超链接时,该令牌允许自动登录请求处理。 此令牌是由对称算法生成的高度加密的值,因此只能在应用程序服务器上解码。 当然,任何时候都需要保护任何类型的密码或凭证,使用对称算法对它们进行加密,然后可以使用相同的密钥对字节进行解密。
截止目前的研究似乎表明,以下是最佳,最安全的单密钥,对称加密算法(Sheth,2017年,“选择正确的算法”,第2段):
- 算法: AES
- 模式: GCM
- 填充: PKCS5Padding
- 密钥大小: 256位
- IV大小: 96位
AES–256使用256位密钥, 需要安装Java密码学扩展(JCE)无限强度软件包。 让我们看一个例子。
注意: 256位密钥需要Java密码术扩展(JCE)无限强度软件包。 如果未安装,则最大为128位密钥。
例
如果尚未安装,请下载并安装Java Cryptography Extension(JCE)无限强度软件包。 需要使用256位密钥。 否则,必须将以下示例更新为使用128位密钥。
清单1是AesTest.java单元测试。 这是以下内容的完整演示:
- 生成并存储AES 256位密钥
- AES加密
- AES解密
清单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。
参考文献
- Java密码术扩展(JCE)无限强度。 (nd)。 检索自http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html 。
- Sheth,M.(2017年4月18日)。 Java密码学中的加密和解密。 从https://www.veracode.com/blog/research/encryption-and-decryption-java-cryptography检索。
- cpast [表示GCM IV为96位,即96/8 = 12字节]。 (2015年6月4日)。 使用AES–256加密时,我可以使用256位IV [Web日志注释]。 从https://security.stackexchange.com/questions/90848/encrypting-using-aes-256-can-i-use-256-bits-iv检索
- Bodewes [强烈建议将GCM IV设置为12个字节(12 * 8 = 96),但可以是任意大小。 其他尺寸将需要其他计算],M。(2015年7月7日)。 密文和标签大小以及在GCM模式下使用AES进行IV传输[Web日志注释]。 从https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode检索。
- Figlesquidge。 (2013年10月18日)。 “密码”和“操作模式”之间有什么区别? [网络日志评论]。 从https://crypto.stackexchange.com/questions/11132/what-is-the-difference-between-a-cipher-and-a-mode-of-operation检索。
- Toust,S.(2013年2月4日)。 为什么对称和非对称加密之间的建议密钥大小会有很大差异? 取自https://crypto.stackexchange.com/questions/6236/why-does-the-recommended-key-size-between-symmetric-and-asymmetric-encryption-di 。
- 卡罗宁(I.)(2012年10月5日)。 密钥,IV和随机数之间的主要区别是什么? 从https://crypto.stackexchange.com/questions/3965/what-is-the-main-difference-between-a-key-an-iv-and-a-nonce检索。
- 分组密码操作模式。 (2017年11月6日)。 维基百科。 取自https://zh.wikipedia.org/wiki/Block_cipher_mode_of_operation#Initialization_vector_.28IV.29