如何从 Java 写入加密的 ECDSA 私钥文件
How can I write an encrypted ECDSA private key file from Java
我有一个 Java 服务可以生成 ECDSA public/private 密钥对。我想将 public 密钥和私钥(使用我在服务中管理的随机生成的密钥加密)写入本地文件系统。
我显然可以使用 base64 对密钥的字节进行编码并将其写入文件,或者我可以将它们写入我自己创建的二进制格式。但如果可能的话,我更愿意以标准化格式(例如 PEM 或 DER)将它们写出来。我可以计算出未加密的 public 密钥,但我正在努力弄清楚如何从 Java 中为加密的私钥计算 。
我知道我可以调用 OS 并在命令行上调用 openssl,但是 (a) 我宁愿在 Java 中本地执行此操作,并且 (b) 我我读过很多帖子表明 openssl 的密钥编码算法不是特别安全。因此,我希望使用 Java 加密体系结构 (JCA) API 使用我选择的算法来加密私钥,然后将加密的字节包装在使之成为有效的 PEM 或 DER 所需的任何内容中-格式化文件。
我怀疑有像 BouncyCastle 这样的库可以使这更容易,如果需要我可能会使用这样的库。但我的公司经营受监管的软件,这会为所有现成 (OTS) 软件带来持续的官僚维护成本,因此理想的解决方案是我可以使用标准 JCA [=] 直接在 Java 中编写的内容 类(当前使用 Java 11)。
对于我如何解决这个问题的任何想法和建议,我将不胜感激。
只要你在Java内(我的意思是你不想与其他系统交换(加密的)私钥)我建议加密编码 私钥 - 这样您就可以完全使用 Java 的内置资源。尝试使用“加密的 PEM 格式”需要使用外部库,例如 Bouncy Castle。
以下解决方案将生成一个 ECDSA 密钥对并打印出编码后的私钥。该字节数组使用随机生成的(32 字节长)密钥加密,该密钥在 GCM 模式操作函数中用作 AES 的输入;输出是一个由 3 个部分连接而成的字符串:
(Base64) nonce : (Base64) ciphertext : (Base64) gcmTag
优化版本可以在字节数组的基础上使用直接连接,但由于该函数是从实际项目中获取的,所以我以这种方式使用它。
我省略了字符串的保存和加载部分 - 该字符串提供给解密函数,解密函数直接给出(加载的)私钥作为输出。此加载密钥也被打印出来,以表明两个密钥相等。
这是示例输出:
Write and read encrypted ECDSA private keys
ecdsaPrivateKey:
3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420c07c0af37716b11ac76780287026935190cb3575c1475a02da687b45adfed8b4
encryptedKey: 2adcp+3lEvS8zhc5:5n5UyHThiIQweqXxJfI479qIwv4m7nm/gNeEDeXcd15zVQCTuER2Hn/SPQUM9TbPFHkdh9CWwYI74lbCyV1AJng62g==:HRWiBgME/SsyHQBvvfdTEg==
ecdsaPrivateKeyLoaded:
3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420c07c0af37716b11ac76780287026935190cb3575c1475a02da687b45adfed8b4
以下代码没有异常处理,仅供学习使用:
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class EncryptedEcdsaPrivateKey {
public static void main(String[] args)
throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException,
InvalidKeySpecException {
System.out.println("Write and read encrypted ECDSA private keys");
// step 1 generate an ecdsa key pair
KeyPair ecdsaKeyPair = generateEcdsaKeyPair(256);
PrivateKey ecdsaPrivateKey = ecdsaKeyPair.getPrivate();
System.out.println("ecdsaPrivateKey:\n" + bytesToHex(ecdsaPrivateKey.getEncoded()));
// step 2 generate a randomly generated AES-256-GCM key
byte[] randomKey = generateRandomAesKey();
// step 3 encrypt the encoded key with the randomly generated AES-256-GCM key
String encryptedKey = aesGcmEncryptToBase64(randomKey, ecdsaPrivateKey.getEncoded());
System.out.println("encryptedKey: " + encryptedKey);
// step 4 save the key to file
// ... omitted
// step 5 load the key from file
// ... omitted
// step 6 decrypt the encrypted data to an ecdsa public key
PrivateKey ecdsaPrivateKeyLoaded = aesGcmDecryptFromBase64(randomKey, encryptedKey);
System.out.println("ecdsaPrivateKeyLoaded:\n" + bytesToHex(ecdsaPrivateKeyLoaded.getEncoded()));
}
public static KeyPair generateEcdsaKeyPair(int keylengthInt)
throws NoSuchAlgorithmException {
KeyPairGenerator keypairGenerator = KeyPairGenerator.getInstance("EC");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
keypairGenerator.initialize(keylengthInt, random);
return keypairGenerator.generateKeyPair();
}
private static byte[] generateRandomAesKey() {
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[32];
secureRandom.nextBytes(key);
return key;
}
private static byte[] generateRandomNonce() {
SecureRandom secureRandom = new SecureRandom();
byte[] nonce = new byte[12];
secureRandom.nextBytes(nonce);
return nonce;
}
private static String aesGcmEncryptToBase64(byte[] key, byte[] data)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] nonce = generateRandomNonce();
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
byte[] ciphertextWithTag = cipher.doFinal(data);
byte[] ciphertext = new byte[(ciphertextWithTag.length-16)];
byte[] gcmTag = new byte[16];
System.arraycopy(ciphertextWithTag, 0, ciphertext, 0, (ciphertextWithTag.length - 16));
System.arraycopy(ciphertextWithTag, (ciphertextWithTag.length-16), gcmTag, 0, 16);
String nonceBase64 = base64Encoding(nonce);
String ciphertextBase64 = base64Encoding(ciphertext);
String gcmTagBase64 = base64Encoding(gcmTag);
return nonceBase64 + ":" + ciphertextBase64 + ":" + gcmTagBase64;
}
private static PrivateKey aesGcmDecryptFromBase64(byte[] key, String data)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException {
String[] parts = data.split(":", 0);
byte[] nonce = base64Decoding(parts[0]);
byte[] ciphertextWithoutTag = base64Decoding(parts[1]);
byte[] gcmTag = base64Decoding(parts[2]);
byte[] encryptedData = concatenateByteArrays(ciphertextWithoutTag, gcmTag);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, nonce);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
byte[] encodedEcdsaKey = cipher.doFinal(encryptedData);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedEcdsaKey);
return keyFactory.generatePrivate(privateKeySpec);
}
private static String base64Encoding(byte[] input) {
return Base64.getEncoder().encodeToString(input);
}
private static byte[] base64Decoding(String input) {
return Base64.getDecoder().decode(input);
}
private static String bytesToHex(byte[] bytes) {
StringBuffer result = new StringBuffer();
for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
return result.toString();
}
public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
return ByteBuffer
.allocate(a.length + b.length)
.put(a).put(b)
.array();
}
}
对于可能对此问题的解决方案感兴趣的任何人,我能够让事情正常工作大部分,正如我所希望的那样。我没有使用随机生成的安全性,而是使用了可配置的基于密码的加密方案。一旦我接受了解决我的问题的方法,我就能够很好地工作。
首先,这是我用来创建用于加密私钥的基于密码的密钥的代码:
private SecretKey createSecretKey() throws MyCryptoException {
try {
String password = getPassword(); // Retrieved via configuration
KeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory factory = SecretKeyFactory.getInstance(this.encryptionAlgorithm.getName());
return factory.generateSecret(keySpec);
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error creating secret key", e);
}
}
要创建我用于加密的密码:
private Cipher createCipher() throws MyCryptoException {
try {
return Cipher.getInstance(this.encryptionAlgorithm.getName());
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error creating cipher for password-based encryption", e);
}
}
对于上述方法,this.encryptionAlgorithm.getName()
将 return PBEWithMD5AndDES
或 PBEWithSHA1AndDESede
。这些似乎与 PKCS #5 版本 1.5 基于密码的加密 (PBKDF1) 一致。我最终计划致力于支持更新的(和更安全的)版本,但现在已经完成了工作。
接下来,我需要一个基于密码的参数说明:
private AlgorithmParameterSpec createParamSpec() {
byte[] saltVector = new byte[this.encryptionAlgorithm.getSaltSize()];
SecureRandom random = new SecureRandom();
random.nextBytes(saltVector);
return new PBEParameterSpec(saltVector, this.encryptionHashIterations);
}
在上面的方法中,this.encryptionAlgorithm.getSaltSize()
return是 8 或 16,取决于配置的算法名称。
然后,我将这些方法结合在一起,将私钥的字节转换为 java.crypto.EncryptedPrivateKeyInfo
实例
public EncryptedPrivateKeyInfo encryptPrivateKey(byte[] keyBytes) throws MyCryptoException {
// Create cipher and encrypt
byte[] encryptedBytes;
AlgorithmParameters parameters;
try {
Cipher cipher = createCipher();
SecretKey encryptionKey = createSecretKey();
AlgorithmParameterSpec paramSpec = createParamSpec();
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, paramSpec);
encryptedBytes = cipher.doFinal(keyBytes);
parameters = cipher.getParameters();
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error encrypting private key bytes", e);
}
// Wrap into format expected for PKCS8-formatted encrypted secret key file
try {
return new EncryptedPrivateKeyInfo(parameters, encryptedBytes);
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error packaging private key encryption info", e);
}
}
这个 EncryptedPrivateKeyInfo
实例是写入文件的内容,经过 Base64 编码并被适当的页眉和页脚文本包围。下面展示我是如何使用上述方法创建加密密钥文件的:
private static final String ENCRYPTED_KEY_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
private static final String ENCRYPTED_KEY_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----";
private static final int KEY_FILE_MAX_LINE_LENGTH = 64;
private void writePrivateKey(PrivateKey key, Path path) throws MyCryptoException {
try {
byte[] fileBytes = key.getEncoded();
encryptPrivateKey(key.getEncoded()).getEncoded();
writeKeyFile(ENCRYPTED_KEY_HEADER, ENCRYPTED_KEY_FOOTER, fileBytes, path);
}
catch (IOException e) {
throw new MyCryptoException("Can't write private key file", e);
}
}
private void writeKeyFile(String header, String footer, byte[] keyBytes, Path path) throws IOException {
// Append the header
StringBuilder builder = new StringBuilder()
.append(header)
.append(System.lineSeparator());
// Encode the key and append lines according to the max line size
String encodedBytes = Base64.getEncoder().encodeToString(keyBytes);
partitionBySize(encodedBytes, KEY_FILE_MAX_LINE_LENGTH)
.stream()
.forEach(s -> {
builder.append(s);
builder.append(System.lineSeparator());
});
// Append the footer
builder
.append(footer)
.append(System.lineSeparator());
// Write the file
Files.writeString(path, builder.toString());
}
private List<String> partitionBySize(String source, int size) {
int sourceLength = source.length();
boolean isDivisible = (sourceLength % size) == 0;
int partitionCount = (sourceLength / size) + (isDivisible ? 0 : 1);
return IntStream.range(0, partitionCount)
.mapToObj(n -> {
return ((n + 1) * size >= sourceLength) ?
source.substring(n * size) : source.substring(n * size, (n + 1) * size);
})
.collect(Collectors.toList());
}
我有一个 Java 服务可以生成 ECDSA public/private 密钥对。我想将 public 密钥和私钥(使用我在服务中管理的随机生成的密钥加密)写入本地文件系统。
我显然可以使用 base64 对密钥的字节进行编码并将其写入文件,或者我可以将它们写入我自己创建的二进制格式。但如果可能的话,我更愿意以标准化格式(例如 PEM 或 DER)将它们写出来。我可以计算出未加密的 public 密钥,但我正在努力弄清楚如何从 Java 中为加密的私钥计算 。
我知道我可以调用 OS 并在命令行上调用 openssl,但是 (a) 我宁愿在 Java 中本地执行此操作,并且 (b) 我我读过很多帖子表明 openssl 的密钥编码算法不是特别安全。因此,我希望使用 Java 加密体系结构 (JCA) API 使用我选择的算法来加密私钥,然后将加密的字节包装在使之成为有效的 PEM 或 DER 所需的任何内容中-格式化文件。
我怀疑有像 BouncyCastle 这样的库可以使这更容易,如果需要我可能会使用这样的库。但我的公司经营受监管的软件,这会为所有现成 (OTS) 软件带来持续的官僚维护成本,因此理想的解决方案是我可以使用标准 JCA [=] 直接在 Java 中编写的内容 类(当前使用 Java 11)。
对于我如何解决这个问题的任何想法和建议,我将不胜感激。
只要你在Java内(我的意思是你不想与其他系统交换(加密的)私钥)我建议加密编码 私钥 - 这样您就可以完全使用 Java 的内置资源。尝试使用“加密的 PEM 格式”需要使用外部库,例如 Bouncy Castle。
以下解决方案将生成一个 ECDSA 密钥对并打印出编码后的私钥。该字节数组使用随机生成的(32 字节长)密钥加密,该密钥在 GCM 模式操作函数中用作 AES 的输入;输出是一个由 3 个部分连接而成的字符串:
(Base64) nonce : (Base64) ciphertext : (Base64) gcmTag
优化版本可以在字节数组的基础上使用直接连接,但由于该函数是从实际项目中获取的,所以我以这种方式使用它。
我省略了字符串的保存和加载部分 - 该字符串提供给解密函数,解密函数直接给出(加载的)私钥作为输出。此加载密钥也被打印出来,以表明两个密钥相等。
这是示例输出:
Write and read encrypted ECDSA private keys
ecdsaPrivateKey:
3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420c07c0af37716b11ac76780287026935190cb3575c1475a02da687b45adfed8b4
encryptedKey: 2adcp+3lEvS8zhc5:5n5UyHThiIQweqXxJfI479qIwv4m7nm/gNeEDeXcd15zVQCTuER2Hn/SPQUM9TbPFHkdh9CWwYI74lbCyV1AJng62g==:HRWiBgME/SsyHQBvvfdTEg==
ecdsaPrivateKeyLoaded:
3041020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420c07c0af37716b11ac76780287026935190cb3575c1475a02da687b45adfed8b4
以下代码没有异常处理,仅供学习使用:
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class EncryptedEcdsaPrivateKey {
public static void main(String[] args)
throws NoSuchAlgorithmException, IllegalBlockSizeException, InvalidKeyException,
BadPaddingException, InvalidAlgorithmParameterException, NoSuchPaddingException,
InvalidKeySpecException {
System.out.println("Write and read encrypted ECDSA private keys");
// step 1 generate an ecdsa key pair
KeyPair ecdsaKeyPair = generateEcdsaKeyPair(256);
PrivateKey ecdsaPrivateKey = ecdsaKeyPair.getPrivate();
System.out.println("ecdsaPrivateKey:\n" + bytesToHex(ecdsaPrivateKey.getEncoded()));
// step 2 generate a randomly generated AES-256-GCM key
byte[] randomKey = generateRandomAesKey();
// step 3 encrypt the encoded key with the randomly generated AES-256-GCM key
String encryptedKey = aesGcmEncryptToBase64(randomKey, ecdsaPrivateKey.getEncoded());
System.out.println("encryptedKey: " + encryptedKey);
// step 4 save the key to file
// ... omitted
// step 5 load the key from file
// ... omitted
// step 6 decrypt the encrypted data to an ecdsa public key
PrivateKey ecdsaPrivateKeyLoaded = aesGcmDecryptFromBase64(randomKey, encryptedKey);
System.out.println("ecdsaPrivateKeyLoaded:\n" + bytesToHex(ecdsaPrivateKeyLoaded.getEncoded()));
}
public static KeyPair generateEcdsaKeyPair(int keylengthInt)
throws NoSuchAlgorithmException {
KeyPairGenerator keypairGenerator = KeyPairGenerator.getInstance("EC");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
keypairGenerator.initialize(keylengthInt, random);
return keypairGenerator.generateKeyPair();
}
private static byte[] generateRandomAesKey() {
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[32];
secureRandom.nextBytes(key);
return key;
}
private static byte[] generateRandomNonce() {
SecureRandom secureRandom = new SecureRandom();
byte[] nonce = new byte[12];
secureRandom.nextBytes(nonce);
return nonce;
}
private static String aesGcmEncryptToBase64(byte[] key, byte[] data)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] nonce = generateRandomNonce();
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, nonce);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec);
byte[] ciphertextWithTag = cipher.doFinal(data);
byte[] ciphertext = new byte[(ciphertextWithTag.length-16)];
byte[] gcmTag = new byte[16];
System.arraycopy(ciphertextWithTag, 0, ciphertext, 0, (ciphertextWithTag.length - 16));
System.arraycopy(ciphertextWithTag, (ciphertextWithTag.length-16), gcmTag, 0, 16);
String nonceBase64 = base64Encoding(nonce);
String ciphertextBase64 = base64Encoding(ciphertext);
String gcmTagBase64 = base64Encoding(gcmTag);
return nonceBase64 + ":" + ciphertextBase64 + ":" + gcmTagBase64;
}
private static PrivateKey aesGcmDecryptFromBase64(byte[] key, String data)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException {
String[] parts = data.split(":", 0);
byte[] nonce = base64Decoding(parts[0]);
byte[] ciphertextWithoutTag = base64Decoding(parts[1]);
byte[] gcmTag = base64Decoding(parts[2]);
byte[] encryptedData = concatenateByteArrays(ciphertextWithoutTag, gcmTag);
Cipher cipher = Cipher.getInstance("AES/GCM/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * 8, nonce);
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec);
byte[] encodedEcdsaKey = cipher.doFinal(encryptedData);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(encodedEcdsaKey);
return keyFactory.generatePrivate(privateKeySpec);
}
private static String base64Encoding(byte[] input) {
return Base64.getEncoder().encodeToString(input);
}
private static byte[] base64Decoding(String input) {
return Base64.getDecoder().decode(input);
}
private static String bytesToHex(byte[] bytes) {
StringBuffer result = new StringBuffer();
for (byte b : bytes) result.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
return result.toString();
}
public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
return ByteBuffer
.allocate(a.length + b.length)
.put(a).put(b)
.array();
}
}
对于可能对此问题的解决方案感兴趣的任何人,我能够让事情正常工作大部分,正如我所希望的那样。我没有使用随机生成的安全性,而是使用了可配置的基于密码的加密方案。一旦我接受了解决我的问题的方法,我就能够很好地工作。
首先,这是我用来创建用于加密私钥的基于密码的密钥的代码:
private SecretKey createSecretKey() throws MyCryptoException {
try {
String password = getPassword(); // Retrieved via configuration
KeySpec keySpec = new PBEKeySpec(password.toCharArray());
SecretKeyFactory factory = SecretKeyFactory.getInstance(this.encryptionAlgorithm.getName());
return factory.generateSecret(keySpec);
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error creating secret key", e);
}
}
要创建我用于加密的密码:
private Cipher createCipher() throws MyCryptoException {
try {
return Cipher.getInstance(this.encryptionAlgorithm.getName());
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error creating cipher for password-based encryption", e);
}
}
对于上述方法,this.encryptionAlgorithm.getName()
将 return PBEWithMD5AndDES
或 PBEWithSHA1AndDESede
。这些似乎与 PKCS #5 版本 1.5 基于密码的加密 (PBKDF1) 一致。我最终计划致力于支持更新的(和更安全的)版本,但现在已经完成了工作。
接下来,我需要一个基于密码的参数说明:
private AlgorithmParameterSpec createParamSpec() {
byte[] saltVector = new byte[this.encryptionAlgorithm.getSaltSize()];
SecureRandom random = new SecureRandom();
random.nextBytes(saltVector);
return new PBEParameterSpec(saltVector, this.encryptionHashIterations);
}
在上面的方法中,this.encryptionAlgorithm.getSaltSize()
return是 8 或 16,取决于配置的算法名称。
然后,我将这些方法结合在一起,将私钥的字节转换为 java.crypto.EncryptedPrivateKeyInfo
实例
public EncryptedPrivateKeyInfo encryptPrivateKey(byte[] keyBytes) throws MyCryptoException {
// Create cipher and encrypt
byte[] encryptedBytes;
AlgorithmParameters parameters;
try {
Cipher cipher = createCipher();
SecretKey encryptionKey = createSecretKey();
AlgorithmParameterSpec paramSpec = createParamSpec();
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, paramSpec);
encryptedBytes = cipher.doFinal(keyBytes);
parameters = cipher.getParameters();
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error encrypting private key bytes", e);
}
// Wrap into format expected for PKCS8-formatted encrypted secret key file
try {
return new EncryptedPrivateKeyInfo(parameters, encryptedBytes);
}
catch (GeneralSecurityException e) {
throw new MyCryptoException("Error packaging private key encryption info", e);
}
}
这个 EncryptedPrivateKeyInfo
实例是写入文件的内容,经过 Base64 编码并被适当的页眉和页脚文本包围。下面展示我是如何使用上述方法创建加密密钥文件的:
private static final String ENCRYPTED_KEY_HEADER = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
private static final String ENCRYPTED_KEY_FOOTER = "-----END ENCRYPTED PRIVATE KEY-----";
private static final int KEY_FILE_MAX_LINE_LENGTH = 64;
private void writePrivateKey(PrivateKey key, Path path) throws MyCryptoException {
try {
byte[] fileBytes = key.getEncoded();
encryptPrivateKey(key.getEncoded()).getEncoded();
writeKeyFile(ENCRYPTED_KEY_HEADER, ENCRYPTED_KEY_FOOTER, fileBytes, path);
}
catch (IOException e) {
throw new MyCryptoException("Can't write private key file", e);
}
}
private void writeKeyFile(String header, String footer, byte[] keyBytes, Path path) throws IOException {
// Append the header
StringBuilder builder = new StringBuilder()
.append(header)
.append(System.lineSeparator());
// Encode the key and append lines according to the max line size
String encodedBytes = Base64.getEncoder().encodeToString(keyBytes);
partitionBySize(encodedBytes, KEY_FILE_MAX_LINE_LENGTH)
.stream()
.forEach(s -> {
builder.append(s);
builder.append(System.lineSeparator());
});
// Append the footer
builder
.append(footer)
.append(System.lineSeparator());
// Write the file
Files.writeString(path, builder.toString());
}
private List<String> partitionBySize(String source, int size) {
int sourceLength = source.length();
boolean isDivisible = (sourceLength % size) == 0;
int partitionCount = (sourceLength / size) + (isDivisible ? 0 : 1);
return IntStream.range(0, partitionCount)
.mapToObj(n -> {
return ((n + 1) * size >= sourceLength) ?
source.substring(n * size) : source.substring(n * size, (n + 1) * size);
})
.collect(Collectors.toList());
}