如何使用 IAIK JCE 在 Java 中使用 PKCS#5 格式的 PBE 加密 RSA 私钥? [英] How to encrypt RSA private key with PBE in PKCS#5 format in Java with IAIK JCE?

查看:94
本文介绍了如何使用 IAIK JCE 在 Java 中使用 PKCS#5 格式的 PBE 加密 RSA 私钥?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我创建了一个 RSA 密钥对.现在,我正在尝试使用 DES 算法加密私钥,将其格式化为 PKCS#5 并将其打印在控制台上.不幸的是,生成的私钥不起作用.当我尝试使用它时,输入正确密码后,ssh客户端返回密码无效:

I've created an RSA Key Pair. Now, I'm trying to encrypt the private key with a DES algorithm, format it to PKCS#5 and print it on the console. Unfortunately, the generated private key does not work. When I try to use it, after entering the right passphrase, the ssh client returns the passphrase is not valid:

加载密钥test.key":用于解密私钥的密码不正确

Load key "test.key": incorrect passphrase supplied to decrypt private key

有人能告诉我我错在哪里吗?

Could please someone tells me where I'm wrong?

这是代码:

private byte[] iv;

public void generate() throws Exception {
    RSAKeyPairGenerator generator = new RSAKeyPairGenerator();
    generator.initialize(2048);
    KeyPair keyPair = generator.generateKeyPair();

    String passphrase = "passphrase";
    byte[] encryptedData = encrypt(keyPair.getPrivate().getEncoded(), passphrase);
    System.out.println(getPrivateKeyPem(Base64.encodeBase64String(encryptedData)));
}

private byte[] encrypt(byte[] data, String passphrase) throws Exception {
    String algorithm = "PBEWithMD5AndDES";
    salt = new byte[8];
    int iterations = 1024;

    // Create a key from the supplied passphrase.
    KeySpec ks = new PBEKeySpec(passphrase.toCharArray());
    SecretKeyFactory skf = SecretKeyFactory.getInstance(algorithm);
    SecretKey key = skf.generateSecret(ks);

    // Create the salt from eight bytes of the digest of P || M.
    MessageDigest md = MessageDigest.getInstance("MD5");
    md.update(passphrase.getBytes());
    md.update(data);
    byte[] digest = md.digest();
    System.arraycopy(digest, 0, salt, 0, 8);
    AlgorithmParameterSpec aps = new PBEParameterSpec(salt, iterations);

    Cipher cipher = Cipher.getInstance(AlgorithmID.pbeWithSHAAnd3_KeyTripleDES_CBC.getJcaStandardName());
    cipher.init(Cipher.ENCRYPT_MODE, key, aps);
    iv = cipher.getIV();
    byte[] output = cipher.doFinal(data);
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    out.write(salt);
    out.write(output);
    out.close();
    return out.toByteArray();
}

private String getPrivateKeyPem(String privateKey) throws Exception {
    StringBuffer formatted = new StringBuffer();
    formatted.append("-----BEGIN RSA PRIVATE KEY----- " + LINE_SEPARATOR);

    formatted.append("Proc-Type: 4,ENCRYPTED" + LINE_SEPARATOR);
    formatted.append("DEK-Info: DES-EDE3-CBC,");
    formatted.append(bytesToHex(iv));

    formatted.append(LINE_SEPARATOR);
    formatted.append(LINE_SEPARATOR);

    Arrays.stream(privateKey.split("(?<=\\G.{64})")).forEach(line -> formatted.append(line + LINE_SEPARATOR));
    formatted.append("-----END RSA PRIVATE KEY-----");

    return formatted.toString();
}

private String bytesToHex(byte[] bytes) {
    char[] hexArray = "0123456789ABCDEF".toCharArray();
    char[] hexChars = new char[bytes.length * 2];
    for (int j = 0; j < bytes.length; j++) {
        int v = bytes[j] & 0xFF;
        hexChars[j * 2] = hexArray[v >>> 4];
        hexChars[j * 2 + 1] = hexArray[v & 0x0F];
    }
    return new String(hexChars);
}

这是生成的 PKCS#5 PEM 格式的私钥:

And this is the generated private key in PKCS#5 PEM format:

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,CA138D5D3C048EBD

+aZNZJKLvNtlmnkg+rFK6NFm45pQJNnJB9ddQ3Rc5Ak0C/Igm9EqHoOS+iy+PPjx
pEKbhc4Qe3U0GOT9L5oN7iaWL82gUznRLRyUXtOrGcpE7TyrE+rydD9BsslJPCe+
y7a9LnSNZuJpJPnJCeKwzy5FGVv2KmDzGTcs9IqCMKgV69qf83pOJU6Dk+bvh9YP
3I05FHeaQYQk8c3t3onfljVIaYOfbNYFLZgNgGtPzFD4OpuDypei/61i3DeXyFUA
SNSY5fPwp6iSeSKtwduSEJMX31TKSpqWeZmEmMNcnh8oZz2E0jRWkbkaFuZfNtqt
aVpLN49oRpbsij+i1+udyuIXdBGRYt9iDZKnw+LDjC3X9R2ceq4AOdfsmEVYbO1i
YNms9eXSkANuchiI2YqkKsCwqI5S8S/2Xj76zf+pCDhCTYGV3RygkN6imX/Qg2eF
LOricZZTF/YPcKnggqNrZy4KSUzAgZ9NhzWCWOCiGFcQLYIo+qDoJ8t4FwxQYhx9
7ckzXML0n0q5ba5pGekLbBUJ9/TdtnqfqmYrHX+4OlrR7XAu478v2QH6/QtNKdZf
VRTqmKKH0n8JL9AgaXWipQstW5ERNZJ9YPBASQzewVNLv4gRZRTw8bYcU/hiPbWp
eqULYYI9324RzY3UTsz3N9X+zQsT02zNdxud7XmmoHL493yyvqT9ERmF4uckGYei
HZ16KFeKQXE9z+x0WNFAKX3nbttVlN5O7TAmUolFTwu11UDsJEjrYMZRwjheAZyD
UnV1LwhFT+QA0r68Mto3poxpAawCJqPP50V4jbhsOb0J7sxT8fo2mBVSxTdb9+t1
lG++x/gHcK51ApK1tF1FhRRKdtOzSib376Kmt23q0jVDNVyy09ys+8LRElOAY1Es
LIuMMM3F7l+F4+knKh3/IkPZwRIz3f9fpsVYIePPS1bUdagzNoMqUkTwzmq6vmUP
C5QvN6Z5ukVCObK+T8C4rya8KQ/2kwoSCRDIX6Mzpnqx6SoO4mvtBHvPcICGdOD6
aX/SbLd9J2lenTxnaAvxWW0jkF6q9x9AAIDdXTd9B5LnOG0Nq+zI+6THL+YpBCB9
6oMO4YChFNoEx0HZVdOc8E7xvXU2NqinmRnyh7hCR5KNfzsNdxg1d8ly67gdZQ1Q
bk1HPKvr6T568Ztapz1J/O6YWRIHdrGyA6liOKdArhhSI9xdk3H3JFNiuH+qkSCB
0mBYdS0BVRVdKbKcrk4WRHZxHsDsQn1/bPxok4dCG/dGO/gT0QlxV+hOV8h/4dJO
mcUvzdW4I8XKrX5KlTGNusVRiFX3Cy8FFZQtSxdWzr6XR6u0bUKS+KjDl1KoFxPH
GwYSTkJVE+fbjsSisQwXjWnwGGkNDuQ1IIMJOAHMK4Mly1jMdFF938WNY7NS4bIb
IXXkRdwxhdkRDiENSMXY8YeCNBJMjqdXZtR4cwGEXO+G+fpT5+ZrfPbQYO+0E0r4
wGPKlrpeeR74ALiaUemUYVIdw0ezlGvdhul2KZx4L82NpI6/JQ7shq9/BEW2dWhN
aDuWri2obsNL3kk2VBWPNiE6Rn/HtjwKn7ioWZ3IIgOgyavcITPBe0FAjxmfRs5w
VWLFBXqcyV9cu1xS4GoCNLk0MrVziUCwHmwkLIzQZos=
-----END RSA PRIVATE KEY-----

提前致谢.

推荐答案

没有 PKCS#5 格式这样的东西.PKCS#5 主要定义了两个基于密码的密钥派生函数和使用它们的基于密码的加密方案,以及一个基于密码的 MAC 方案,但没有定义任何数据格式.(它确实为这些操作定义了 ASN.1 OID,并为其参数定义了 ASN.1 结构——主要是 PBKDF2 和 PBES2,因为 PBKDF1 和 PBES1 的唯一参数是盐.)PKCS#5还定义了 CBC 模式数据加密的填充方案;此填充由 PKCS#7 略微增强,并被许多其他应用程序使用,通常将其称为 PKCS5 填充或 PKCS7 填充.这些都不是数据格式,也不涉及 RSA(或其他)私钥.

There is no such thing as PKCS#5 format. PKCS#5 primarily defines two password-based key derivation functions and password-based encryption schemes using them, plus a password-based MAC scheme, but does not define any format for the data. (It does define ASN.1 OIDs for these operations, and ASN.1 structures for their parameters -- primarily PBKDF2 and PBES2, because the only parameter for PBKDF1 and PBES1 is the salt.) PKCS#5 also defines a padding scheme for the CBC mode data encryption; this padding was slightly enhanced by PKCS#7 and used by many other applications which usually call it PKCS5 padding or PKCS7 padding. None of these are data formats, and none of them involves RSA (or other) private keys as such.

您显然想要的文件格式是 OpenSSH 使用的文件格式(长期以来一直使用,然后在过去几年中作为默认格式,直到一个月前 OpenSSH 7.8 将其设为可选格式),因此也被使用其他希望与 OpenSSH 兼容甚至可互换的软件.这种格式实际上是由 OpenSSL 定义的,OpenSSH 长期以来一直用于其大部分密码学.(在 Heartbleed 之后,OpenSSH 创建了一个名为 LibreSSL 的 OpenSSL 分支,它试图在内部变得更加健壮和安全,但有意维护相同的外部接口和格式,无论如何都没有被广泛采用.)

The file format you apparently want is the one used by OpenSSH (for a long time always, then for the last few years as the default, until OpenSSH 7.8 just a month ago made it optional) and as a result also used by other software that wants to be compatible or even interchangeable with OpenSSH. This format is actually defined by OpenSSL, which OpenSSH has long used for most of its cryptography. (Following Heartbleed, OpenSSH created a fork of OpenSSL called LibreSSL, which tries to be more robust and secure internally but intentionally maintains the same external interfaces and formats, and in any case hasn't been widely adopted.)

它是OpenSSL 定义的几种PEM"格式之一,主要在许多PEM"例程的手册页中进行了描述,包括PEM_write[_bio]_RSAPrivateKey -- 如果您有 OpenSSL 而不是 Windows,则在您的系统上,或者 在网络上,在PEM ENCRYPTION FORMAT"部分接近末尾的加密部分,以及它在 它自己的手册页.简而言之:它不使用由 PKCS#12/rfc7292 定义的 pbeSHAwith3_keyTripleDES-CBC(意思是 SHA1)方案 PBES1 中的 PKCS#5/rfc2898.相反,它使用带有 md5 和 1 次迭代的 EVP_BytesToKey(部分基于 PBKDF1)和等于 IV 的盐来派生密钥,然后使用任何使用 IV(因此不是流或 ECB)但通常默认为 DES-EDE3(又名 3key-TripleDES)CBC 的支持对称密码模式.是的,带有 niter=1 的 EVP_BytesToKey 是一个糟糕的 PBKDF 并且使这些文件不安全,除非您使用非常强的密码;已经有很多关于这个的问题了.

It is one of several 'PEM' formats defined by OpenSSL, and is mostly described on the man page for a number of 'PEM' routines including PEM_write[_bio]_RSAPrivateKey -- on your system if you have OpenSSL and it's not Windows, or on the web with the encryption part near the end in the section 'PEM ENCRYPTION FORMAT', and the EVP_BytesToKey routine it references similarly on its own man page. In short: it does not use the pbeSHAwith3_keyTripleDES-CBC (meaning SHA1) scheme defined by PKCS#12/rfc7292 or the pbeMD5withDES-CBC scheme defined by PKCS#5/rfc2898 in PBES1. Instead it uses EVP_BytesToKey (which is partly based on PBKDF1) with md5 and 1 iteration, and salt equal to the IV, to derive the key, and then encrypts/decrypts with any supported symmetric cipher mode that uses an IV (thus not stream or ECB) but usually defaulting to DES-EDE3 (aka 3key-TripleDES) CBC as you ask for. Yes, EVP_BytesToKey with niter=1 is a poor PBKDF and makes these files insecure unless you use a very strong password; there are numerous Qs about that already.

最后,这种文件格式的纯文本不是PKCS#8(通用)编码由 [RSA]PrivateKey.getEncoded() 返回,而是由定义的 RSA-only 格式PKCS#1/rfc8017 et pred.并且Proc-type和DEK-info标头与base64之间需要空行,破折号-END行的行终止符可能需要,具体取决于读取的软件.

And finally the plaintext of this file format is not the PKCS#8 (generic) encoding returned by [RSA]PrivateKey.getEncoded() but rather the RSA-only format defined by PKCS#1/rfc8017 et pred. And the empty line between the Proc-type and DEK-info headers and the base64 is required, and the line terminator on the dashes-END line may be needed depending on what software does the reading.

最简单的方法是使用已经与 OpenSSL 私钥 PEM 格式兼容的软件,包括 OpenSSL 本身.Java 可以运行外部程序:OpenSSH 的 ssh-keygen(如果有),或者 openssl genrsa(如果有).BouncyCastle bcpkix 库支持这种格式和其他 OpenSSL PEM 格式.如果 'ssh client' 是 jsch,那通常会读取多种格式的密钥文件,包括这种格式,但 com.jcraft.jsch.KeyPairRSA 实际上支持生成密钥并将其写入这种 PEM 格式也是如此.Puttygen 也支持这种格式,但它可以转换的其他格式不是 Java 友好的.我确定还有更多.

The easiest way to do this is to use software already compatible with OpenSSL private-key PEM format(s), including OpenSSL itself. Java can run an external program: OpenSSH's ssh-keygen if you have it, or openssl genrsa if you have that. The BouncyCastle bcpkix library supports this and other OpenSSL PEM formats. If 'ssh client' is jsch, that normally reads keyfiles in several formats including this one, but com.jcraft.jsch.KeyPairRSA actually supports generating a key and writing it in this PEM format as well. Puttygen also supports this format, but the other formats it can convert from and to aren't Java-friendly. I'm sure there are more.

但如果您需要在自己的代码中执行此操作,方法如下:

But if you need to do it in your own code, here's how:

    // given [RSA]PrivateKey privkey, get the PKCS1 part from the PKCS8 encoding
    byte[] pk8 = privkey.getEncoded();
    // this is wrong for RSA<=512 but those are totally insecure anyway
    if( pk8[0]!=0x30 || pk8[1]!=(byte)0x82 ) throw new Exception();
    if( 4 + (pk8[2]<<8 | (pk8[3]&0xFF)) != pk8.length ) throw new Exception();
    if( pk8[4]!=2 || pk8[5]!=1 || pk8[6]!= 0 ) throw new Exception();
    if( pk8[7] != 0x30 || pk8[8]==0 || pk8[8]>127 ) throw new Exception();
    // could also check contents of the AlgId but that's more work
    int i = 4 + 3 + 2 + pk8[8];
    if( i + 4 > pk8.length || pk8[i]!=4 || pk8[i+1]!=(byte)0x82 ) throw new Exception();
    byte[] old = Arrays.copyOfRange (pk8, i+4, pk8.length);

    // OpenSSL-Legacy PEM encryption = 3keytdes-cbc using random iv 
    // key from EVP_BytesToKey(3keytdes.keylen=24,hash=md5,salt=iv,,iter=1,outkey,notiv)
    byte[] passphrase = "passphrase".getBytes(); // charset doesn't matter for test value
    byte[] iv = new byte[8]; new SecureRandom().nextBytes(iv); // maybe SIV instead?
    MessageDigest pbh = MessageDigest.getInstance("MD5");
    byte[] derive = new byte[32]; // round up to multiple of pbh.getDigestLength()=16
    for(int off = 0; off < derive.length; off += 16 ){
        if( off>0 ) pbh.update(derive,off-16,16);
        pbh.update(passphrase); pbh.update(iv); 
        pbh.digest(derive, off,  16);
    }
    Cipher pbc = Cipher.getInstance("DESede/CBC/PKCS5Padding");
    pbc.init (Cipher.ENCRYPT_MODE, new SecretKeySpec(derive,0,24,"DESede"), new IvParameterSpec(iv));
    byte[] enc = pbc.doFinal(old);

    // write to PEM format (substitute other file if desired)
    System.out.println ("-----BEGIN RSA PRIVATE KEY-----");
    System.out.println ("Proc-Type: 4,ENCRYPTED");
    System.out.println ("DEK-Info: DES-EDE3-CBC," + DatatypeConverter.printHexBinary(iv));
    System.out.println (); // empty line
    String b64 = Base64.getEncoder().encodeToString(enc);
    for( int off = 0; off < b64.length(); off += 64 )
        System.out.println (b64.substring(off, off+64<b64.length()?off+64:b64.length()));
    System.out.println ("-----END RSA PRIVATE KEY-----");

最后,OpenSSL 格式要求加密 IV 和 PBKDF 盐相同,并且它使该值随机化,所以我也这样做了.您仅用于 salt 的计算值 MD5(password||data),与现在被接受用于加密的合成 IV (SIV) 结构有些相似,但它不一样,另外我不知道任何有能力的分析师都考虑过 SIV 用于 PBKDF 盐的情况,所以我不愿意在这里依赖这种技术.如果你想问这一点,它并不是真正的编程 Q,更适合在 cryptography.SX 或 security.SX 上.

Finally, OpenSSL format requires the encryption IV and the PBKDF salt be the same, and it makes that value random, so I did also. The computed value you used for salt only, MD5(password||data), vaguely resembles the synthetic-IV (SIV) construction that is now accepted for use with encryption, but it is not the same, plus I don't know if any competent analyst has considered the case where SIV is also used for PBKDF salt, so I would be reluctant to rely on this technique here. If you want to ask about that point, it's not really a programming Q and would be more suitable on cryptography.SX or maybe security.SX.

添加评论:

该代码的输出适用于 0.70 的 puttygen,无论是在 Windows(来自 upstream=chiark)还是在 CentOS6(来自 EPEL)上.根据消息来源,仅当 cmdgen 在 sshpubk.c 中调用 key_type 识别第一行以-----BEGIN"而不是-----BEGIN OPENSSH PRIVATE KEY"开头时,才会出现您给出的错误消息(这是一种非常不同的格式),然后通过 import_ssh2 和 openssh_pem_read 在 import.c 中调用 load_openssh_pem_key ,它没有找到以-----BEGIN"开头并以PRIVATE KEY-----"结尾的第一行.这很奇怪,因为两者之间的 PLUS "RSA " 都是由我的代码生成的 OpenSSH(或 openssl)需要接受它.尝试使用诸如 cat -vetsed -nl 之类的东西或在紧要关头 od - 中至少查看第一行的每个字节(可能是前两行) -c.

That code's output works for me with puttygen from 0.70, both on Windows (from upstream=chiark) and on CentOS6 (from EPEL). According to the source, the error message you gave occurs only if cmdgen has called key_type in sshpubk.c which recognized the first line as beginning with "-----BEGIN " but not "-----BEGIN OPENSSH PRIVATE KEY" (which is a very different format), then via import_ssh2 and openssh_pem_read called load_openssh_pem_key in import.c which does NOT find the first line beginning with "-----BEGIN " and ending with "PRIVATE KEY-----". This is very weird because both of those PLUS "RSA " in between is generated by my code and is needed for OpenSSH (or openssl) to accept it. Try looking at every byte of the first line at least (maybe first two lines) with something like cat -vet or sed -n l or in a pinch od -c.

RFC 2898 现在已经很老了;今天的良好实践通常是数十万到数十万次迭代,更好的做法是根本不使用迭代哈希,而是使用诸如 scrypt 或 Argon2 之类的内存困难的东西.但正如我已经写过的,OpenSSL 传统 PEM 加密是在 1990 年代设计的,它使用 ONE (un, eine, 1) 迭代,因此是一种 POOR 和 INSECURE 方案.现在没有人可以改变它,因为它就是这样设计的.如果您想要体面的 PBE,请不要使用这种格式.

RFC 2898 is rather old now; good practice today is usually 10s of thousands to 100s of thousands of iterations, and better practice is not to use an iterated hash at all but instead something memory-hard like scrypt or Argon2. But as I already wrote, OpenSSL legacy PEM encryption, which was designed back in the 1990s, uses ONE (un, eine, 1) iteration and therefore is a POOR and INSECURE scheme. Nobody can change it now because that's how it was designed. If you want decent PBE, don't use this format.

如果您只需要一个用于 SSH 的密钥:OpenSSH(几年来)支持并且最新版本的 Putty(gen) 可以导入 OpenSSH 定义的新格式",它使用 bcrypt,但 jsch 不能.OpenSSH(使用 OpenSSL)还可以读取(PEM)PKCS8,它允许 PBKDF2(虽然不是最好的)根据需要进行迭代,并且看起来像 jsch 可以,但不是 Putty(gen).我不知道 Cyber​​duck 或其他实现.

If you need a key only for SSH: OpenSSH (for several years now) supports and recent versions of Putty(gen) can import the OpenSSH-defined 'new format', which uses bcrypt, but jsch can't. OpenSSH (using OpenSSL) can also read (PEM) PKCS8 which allows PBKDF2 (better though not best) with iterations as desired, and it looks like jsch can, but not Putty(gen). I don't know for Cyberduck or other implementations.

这篇关于如何使用 IAIK JCE 在 Java 中使用 PKCS#5 格式的 PBE 加密 RSA 私钥?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆