首页 > 解决方案 > 如何从 Java 编写加密的 ECDSA 私钥文件

问题描述

我有一个 Java 服务,它将生成一个 ECDSA 公钥/私钥对。我想将公钥和私钥都写入本地文件系统,并使用我在服务中管理的随机生成的密钥进行加密。

我显然可以使用 base64 对密钥的字节进行编码并将其写入文件,或者我可以将它们以我自己创建的二进制格式写入。但如果可能的话,我更愿意以 PEM 或 DER 等标准化格式将它们写出来。我可以为未加密的公钥弄清楚这一点,但我正在努力弄清楚如何从 Java中为加密的私钥做到这一点。

我知道我可以调出操作系统并在命令行上调用 openssl,但是(a)我宁愿在 Java 中本地执行此操作,并且(b)我已经阅读了许多帖子,建议使用 openssl 的算法来编码密钥不是特别安全。因此,我希望使用 Java Cryptography Architecture (JCA) API 使用我选择的算法加密私钥,然后将加密的字节包装在任何需要的内容中,以使其成为有效的 PEM 或 DER 格式的文件。

我怀疑有像 BouncyCastle 这样的库可以让这更容易,如有必要,我可能会使用这样的库。但是我的公司经营受监管的软件,这会为所有现成 (OTS) 软件带来持续的官僚维护成本,因此理想的解决方案是我可以使用标准 JCA 类(目前使用 Java 11)直接用 Java 编写)。

对于如何解决此问题,我将不胜感激。

标签: javaencryptionpemecdsader

解决方案


只要您留在 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();
    }
}

推荐阅读