Java 21:生成 PEM 格式的加密 RSA 私钥

问题描述 投票:0回答:1

我想使用纯 Java API 在 Java 21 中生成 RSA 密钥对(不使用 BouncyCastle 或类似的库)。私钥应该可由 OpenSSL 和 Java 处理。基于Java的限制,这意味着我想生成一个AES加密的私钥。

下面的代码可以很好地生成未加密的私钥,但是尽管进行了多次 Google 和 StackOverflow 搜索,我仍无法成功实现

pemEncrypt
方法。我有点迷失在许多算法和格式中;也许我需要做额外的工作来生成正确的 ASN.1 数据结构?

   @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
    public static final class KPGenerator {
        private final char[] passPhrase;
        
        @SneakyThrows
        public final KeyPair generate() {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(2048); 
            return kpg.generateKeyPair();
        }
        
        @SneakyThrows
        public final void writePem(Path privateKeyPath, Path publicKeyPath) {
            var kp = generate();
            writePem("PRIVATE KEY", kp.getPrivate().getEncoded(), privateKeyPath);
            writePem("PUBLIC KEY", kp.getPublic().getEncoded(), publicKeyPath);
        }
        
        @SneakyThrows
        private final void writePem(String type, byte[] key, Path path) {
            var pemString = asPem(type, key);
            Files.writeString(path, pemString, StandardOpenOption.CREATE_NEW);
        }
        
        private final String asPem(String type, byte[] key) {
            if ( "PRIVATE KEY".equals(type) && passPhrase!=null ) {
                return asPem("ENCRYPTED PRIVATE KEY", pemEncrypt(key, passPhrase));
            }
            return "-----BEGIN "+type+"-----\n"
                    + Base64.getMimeEncoder().encodeToString(key)
                    + "\n-----END "+type+"-----";
        }
        
        @SneakyThrows
        private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
           ???
        }
}

下面是我尝试过的一些

pemEncrypt
实现示例。

方法1

基于 https://medium.com/@patc888/decrypt-openssl-encrypted-data-in-java-4c31983afe19

问题:

openssl asn1parse -in privatekey.pem
抛出错误:
4047F272597F0000:error:0680009B:asn1 encoding routines:ASN1_get_object:too long:../crypto/asn1/asn1_lib.c:95

        @SneakyThrows
        private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
            var salt = createSalt();
            byte[] passAndSalt = ArrayUtils.addAll(toBytes(passPhrase), salt);
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] key = md.digest(passAndSalt);
            SecretKeySpec secretKey = new SecretKeySpec(key, "AES");
            md.reset();
            byte[] iv = Arrays.copyOfRange(md.digest(ArrayUtils.addAll(key, passAndSalt)), 0, 16);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
            byte[] encryptedSecretKey = cipher.doFinal(privateKey);
            try ( var bos = new ByteArrayOutputStream(); ) {
                bos.writeBytes("Salted__".getBytes(StandardCharsets.US_ASCII));
                bos.writeBytes(salt);
                bos.writeBytes(encryptedSecretKey);
                return bos.toByteArray();
            }
        }
        private static final byte[] toBytes(char[] chars) {
            CharBuffer charBuffer = CharBuffer.wrap(chars);
            ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
            byte[] bytes = Arrays.copyOfRange(byteBuffer.array(),
                      byteBuffer.position(), byteBuffer.limit());
            Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
            return bytes;
        }
        private static final byte[] createSalt() {
            final byte[] salt = new byte[16];
            new SecureRandom().nextBytes(salt);
            return salt;
        }

输出示例:

-----BEGIN ENCRYPTED PRIVATE KEY-----
U2FsdGVkX193omAgeegjjYcN8h3riBDrFrFVpOSxkaEIo1sFg9ZT89RkN+fCBYpzFROB4UOmfBVs
0t/3mfLIID8eDjRH59KU9BCXwXt9unpYGWjFL4Vv5OQsRefiVM57FpbQ5YS6xPhfq0qQT39uTI7p
wG9XSHEBogoYiHTSx3+IIj5/iPNWuE3rMxRJUX/tERFfl6SSY1SlWR3zzy2LwlCddklzIqyZkLSk
cUE+yHTFsFnLuWdkaVMVrWY/isArukuc/gAWA9LThdF+il87s1D6tVtZOpFAmhqYUXhX6Si4scDK
910xEhqZhxqsybBih51+y0GqvcxgVpkadoEaz2WAcAq9aOykjvWGEhtGrCJ9JqhiiIVnY7bcHZxv
cPAAGAOoP95BHdDFR0VYnX1I+OzRxl0XDTVQEsYIWFV0t3u5/dUy/x2yOgkhFer7feAiY8yo7fnU
E1YOsSW2PnF8E2pXBeiJoemw5BimSfuqbuYr140gyDvPvjcfQBwr4vUuKXRxPnTwzjzvqYY5vNXG
p3+TTTL++DMOX/Us0kRVHERdsznn/E20QqBie4f2xa06Vkpf8SZMyO+aSQXRJeVeGH1LtaBLPX9R
hflr6SaX94E9Du91r9Qif1dI451P4M0+Zo3oQTmfy6RwKwckiP+L7LRyfZKLdRk2OTMcP2PX20DZ
SpeYzEzY6LxsaGAiQnHQ+jX9173455PJ1YCRANfCVCjVFr4enAIHqbxId8XD2HdEp3wB76Bj/uFF
NPIzA/gY6ezrUeikGrPYdbgXZ0meKmEJkSxl32ojCmDmhGShxVXRI8c8Subdj+mFS6/BPCwQ2W5V
UeS3fbblUbtpii0XdJoeh+ek/+AGwnkVI7BFse1sZqG4KRr/beH4Z7P2oms1a9ZWBihVveI8xRL1
HcLVi/lgrD25Rw9c+BVDlpCNpr9C42AJgD8XiLQsfd3kj+S5nOTjf61+onz7VQR3LtFDSex2biBL
4DlAbqQzFVM9F2LO0BZeaCLcVKsQ1vmmLYahvklhAcMBco8rZ/8aw0dzVt6L5pWNytH/fYRBRbSI
F1rjnEWmGjF+aXFbjg/SMAV+YSNg03OM/BrU9HpEXH40PB7sJrbY4nqtX/4igwDkMjEVsz0ThZt/
BODzNhxND+HxcXIKjnr5W+3FKUmtkhAMHYgWShzmYCUlt4rdUWjZSL9I6YW0TpG69NBpSw0qihCD
31iUL3uDLe+1QiCoa3/MvPo5CrXheHwV2RNGwIaZRYS6Fzz5VG5j4iI2r8eB8nysiZpTC9hgrlXj
1HdZiKqQDAm3N27eDMLDOfvtJz1x8GfMI+agoj7KB9W48rMNtk0+Vmv5THf6EkeM+KhEWjff4Ju7
cShPd9tJKDwDHPKSUQyCZegIHJfEZeS5gZ6TPeE/L6mEvHBmW3YQpsDkI7lBo/qQYe7UyrAHVTIY
sYNdIA74LvrXSPHlIumjT4lPl9aTc7vaoLfrJc1EnTatujBqrVucdB0LdFWQ34s9YdLICZUV2X25
xUfIpIWZ6G6S8THqGICZ61tAXvRGRwVT2JlsiG6967G84fda0CpT/huJEiTrZRlETfAOEyfQJFJp
shxD+6gTYaIwMSmx2Tq7KyC9emddILIFu1wZgY2H6096n/MM9RuLIxgWOT9MscYWNzlGF+L9XSJH
98c=

方法2

问题:与之前类似的 OpenSSL 错误:

40B70A48E47F0000:error:0680007B:asn1 encoding routines:ASN1_get_object:header too long:../crypto/asn1/asn1_lib.c:105:

        @SneakyThrows
        private static final byte[] pemEncrypt(byte[] key, char[] passPhrase) {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, createKey(passPhrase), createIv());
            return cipher.doFinal(key);
        }
        @SneakyThrows
        private static final SecretKey createKey(char[] passPhrase) {
            //PBE results in invalid key length exception
            //SecretKeyFactory factory = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
            SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
            KeySpec spec = new PBEKeySpec(passPhrase, createSalt(), 65536, 256);
            SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
            return secret;
        }
        
        private static final IvParameterSpec createIv() {
            final byte[] iv = new byte[16];
            new SecureRandom().nextBytes(iv);
            return new IvParameterSpec(iv);
        }
        
        private static final byte[] createSalt() {
            final byte[] salt = new byte[16];
            new SecureRandom().nextBytes(salt);
            return salt;
        }

输出示例:

-----BEGIN ENCRYPTED PRIVATE KEY-----
iBGmburj5hml/segy4KnBXwIm1kYGADhB0KAhfgB1yFsyyoRXC5BlwT6Dq4OYHROtkhsttwV1+5V
cqCarwq1WJmBtWlnohh3oU4YRAkmHj6nMh8fFCRutaySBuN75bqI9tSJ0+1nRsg+saU2F1rGxHma
WhcHB3KZqZe8sNlsTBvDzR6NYU1+XNbxskbOiniA6eJAXGuYpfVtoLlY3BX3AtbK8d+e2XBMF0SA
ddMhUtZblIAsj36fnxmGIql/sHfCLEsOUBDK8fdpA1YryLpg5HhE4Fwht++hamtZUgvW0vNw1q7e
PrhSGY5pOgIdtb1EPdswnHNd77C2/qDC/it3on9mU6U9lvGbP7TyqO0vkXWdDY8hzwowNauy6pTv
YneTQCX1rLwTkuMJMq9ObvoPd+Z0ojmS25eSFlAxIin1DZ3wUKcox2u0DIVA5iF5M4wQRn3GiIJU
tjX4B45t935gxPlNMB7EGRySBCidCmWngTWlUF4KhRD6bcf/BlD70LLRxD37SBT/kpExhaewvUx8
Obrj5hXEHxhyf7h28Rb/LXj5C5MwbG3mAlWWlk1cLW6vezvVi2xpVsjIuoCVmN2iv3RklQtL018I
yUEEUeJZObfdZ13c9ha/Pf4a/+mIx6HJUkQzsH0BxkXifxSYBw7/CtH+0nRZRRswo/AbxTVDEVyV
A0UcWfCZa9PW52mWqZPp7aggI7Zy/IjMYQ6q4YMLWQbzCPGl3aQZ/b7VFO7kYrPsyZwyj9IMQIWt
AUI2gvtz5DysTekrMU0aiU/BS1rrl6i9C7FnmZHEBCf6DjtgKl3f0ZwPo5r2UejvdV4rw4sFqrR4
DYdva5McjmFRpiqpvDNXnRUliLXqXorsJgTCga+kX4YfHDhjjAdU+WvFMnC14FqbiJ6bb6dnGZ93
AdTPdOFV3tAX/M9mEMGqcVKazkVSzC0ZV7bxYO0IKpKpufRganb9emHu+A7EZPiBZKm7/IEnI8vq
NISt8raICwPfrl7lkg+VLfYC/8ubhpMY8itsDi/LHzOcAj4Tw0Hyayymy7chUSPfKJu4homVbeMY
vvSpBVJwUMa4rs+NQH6ocpUk3o/OuEdbY29KceqH8SQQtshZ7NQN6NTDJl40l3A7jdDK62PEJksc
BeHrKRiT3wMLKnhbEHbyXeL47j17Bbq0SWzUQuS7pBasIf+UsiQVKNLxYepIJdneRCsqW32zu7n3
DOx/ZftThUCU7NjmbVgWOmj0NVAEa5zfkwMKuqNyDRbTTkiw4tSxnbuqQGOZOp3G0e3pJ+kPui+p
qLsIyCY9l/wk2zy1WafD6ddP7jtvVB89SFVMN61DiPmJVQoq6hrFLHlv+lQE970sWGsmZ3b7BxBw
QLA0c3sKF9DEFIpv9B/VU6YtyYjTi81hmWZ/MuzEhj6ZW5VNqgUAAODb5Tgk8cLELYrxgoWyxMuX
yF6WZ7XIdFx5TKYstzhsDSBkpfaQxqzSmg8OvVQL9FLCjs+CoBjTUxzixHz5OSysapf4aMiWZS3t
UfNfYoJFjRCIvZBP1T9Q12kdLxM4kvcC6Ony4ZmIWLBYCOE7aBWW3+0AGH4NO2eQkJDTIvOQkC1n
YSL/k9O3eYi+eZ4d21ZcWJRmOMjftR3qVUSG8YzyNXAIBps=
-----END ENCRYPTED PRIVATE KEY-----

方法3

基于ChatGPT。

问题:Java 异常

java.security.InvalidKeyException: Wrong algorithm: AES or Rijndael required
Cipher.init()

        private static final byte[] pemEncrypt(byte[] privateKey, char[] passPhrase) {
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey);
            byte[] salt = createSalt();
            int iterationCount = 65536; // You can adjust this value
            int keyLength = 128; // You can adjust this value
    
            PBEKeySpec pbeKeySpec = new PBEKeySpec(passPhrase, salt, iterationCount, keyLength);
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);
    
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encryptedKey = cipher.doFinal(pkcs8EncodedKeySpec.getEncoded());
    
            EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(cipher.getParameters(), encryptedKey);
            return encryptedPrivateKeyInfo.getEncoded();
        }
java encryption rsa private-key
1个回答
0
投票

方法1是完全错误的。这是 openssl enc 用于

data
的加密和格式(根本不是 PEM),而不是用于密钥文件(PEM 或 DER)的加密和格式。

我不知道方法2是什么,但它也是错误的。

方法3 比较接近。 (令人惊讶的是,ChapGPT 这一次并没有在所有事情上撒谎。)

要同时加密 编码,您需要使用 PBE 算法进行 both 密钥派生和加密。然而,至少现在看来,

EncryptedPrivateKeyInfo
只能正确编码具有唯一OID的PBE方案——来自PKCS5v1或PKCS12appC的PBE方案,但不能正确编码来自PKCS5v2的PBES2,其中包括您要求的Hmac*+AES方案。有一种 PKCS12 方案(SHA1 和 3-keyTripleDES 又名 DESede)仍然被 Java 和 OpenSSL 接受,尽管不是真正首选,所以这是可行的:

        // scaffolding
        byte[] privateKey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate().getEncoded();
        char[] pw = "sekrit".toCharArray();

        // your pemEncrypt
        byte[] salt = new byte[16]; new SecureRandom().nextBytes(salt);
        String pbealg = "PBEwithSHA1andDESede"; 
        SecretKey secretKey = SecretKeyFactory.getInstance(pbealg) .generateSecret(new PBEKeySpec(pw));
        Cipher cipher = Cipher.getInstance(pbealg);
        cipher.init (Cipher.ENCRYPT_MODE, secretKey, new PBEParameterSpec(salt,65536));
        byte[] body = cipher.doFinal(privateKey);
        byte[] all = new EncryptedPrivateKeyInfo(cipher.getParameters(),body).getEncoded();

        // scaffolding
        System.out.println("-----BEGIN ENCRYPTED PRIVATE KEY-----");
        System.out.println(Base64.getMimeEncoder().encodeToString(all));
        System.out.println("-----END ENCRYPTED PRIVATE KEY-----");

应该可以通过手动编码来完成 PBES2 方案,但这需要大量的工作。如果有时间我会稍后发布更新。

© www.soinside.com 2019 - 2024. All rights reserved.