【问题标题】:How can I write an encrypted ECDSA private key file from Java如何从 Java 编写加密的 ECDSA 私钥文件
【发布时间】:2021-05-28 23:22:06
【问题描述】:

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

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

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

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

对于如何解决此问题的任何想法和建议,我将不胜感激。

【问题讨论】:

  • 由于加密的 ec 私钥的结构并不难,它能够从“标题”部分(迭代次数、盐和 IV)读取所有必要的数据来运行您自己的(如今)PBKDF2 密钥派生,然后是(例如)AES-CBC-256 解密(加密)密钥数据并接收与未加密(编码)密钥相同的数据。要编写加密密钥,您需要输入密码和迭代,生成随机盐 + IV,生成加密密钥并加密(编码的)私钥。
  • 最后,您需要添加一个标头,使用 Base64-Mime 对完整结构进行编码,并在前面附加 PEM 标头和页脚,您将获得一个可交换的加密 EC 私钥。
  • 在查看了 BouncyCastle 实用程序的源代码之后,我似乎要添加一个 PEM 格式的标头,其中仅包含加密算法名称和 DEK-信息标题。除非我错过了什么……

标签: java encryption pem ecdsa der


【解决方案1】:

对于任何可能对此问题的解决方案感兴趣的人,我能够大部分按我希望的那样工作。我没有使用随机生成的安全性,而是使用了可配置的基于密码的加密方案。一旦我接受了这种方法来解决我的问题,我就能够让事情顺利进行。

首先,这是我用来创建用于加密私钥的基于密码的密钥的代码:

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() 将返回PBEWithMD5AndDESPBEWithSHA1AndDESede。这些似乎与 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() 返回 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());
}

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 谢谢@Michael Fehr,但我正在寻找一种使用加密 PEM 或 DER 格式的方法。虽然我暂时可以将其保留在 Java 中,但我预计很快需要将其外部化。因此,我正在寻找专门加密密钥的方法,如果/当需要时,我不必完全重新工作。
    • @Tim Dean:祝你好运,因为到目前为止,我从未见过任何没有外部库的“openssl-encryption”写入/读取解决方案。另一种解决方案可能是使用提供密码检查的 Java 密钥存储。
    猜你喜欢
    • 2016-08-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-09-25
    • 2018-07-19
    • 1970-01-01
    相关资源
    最近更新 更多