如何使用 IAIK JCE 在 Java 中使用 PKCS#5 格式的 PBE 加密 RSA 私钥?

Ili*_*nov 3 java rsa password-encryption pkcs#5 iaik-jce

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

加载密钥“test.key”:提供的用于解密私钥的密码不正确

有人可以告诉我我错在哪里吗?

这是代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

-----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-----
Run Code Online (Sandbox Code Playgroud)

提前致谢。

dav*_*085 5

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

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

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

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

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

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

    // 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-----");
Run Code Online (Sandbox Code Playgroud)

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


添加评论:

该代码的输出对我来说适用于 0.70 版本的 puttygen,无论是在 Windows(来自上游=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-----”结尾的第一行。这很奇怪,因为两者之间的“RSA”都是由我的代码生成的,并且OpenSSH(或 openssl)需要它来接受它。尝试至少查看第一行的每个字节(也许是前两行),使用类似cat -vetor sed -n lor in the pin 的东西od -c

RFC 2898 现在已经相当老了;今天的良好实践通常是数十次数千次到数百次数千次迭代,更好的实践是根本不使用迭代哈希,而是使用像 scrypt 或 Argon2 这样的内存困难的东西。但正如我已经写过的,OpenSSL 遗留 PEM 加密是在 20 世纪 90 年代设计的,使用 ONE (un, eine, 1) 迭代,因此是一种较差且不安全的方案。现在没有人可以改变它,因为它就是这样设计的。如果您想要像样的 PBE,请不要使用此格式。

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