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

时间:2018-10-03 15:32:03

标签: java rsa password-encryption pkcs#5 iaik-jce

我创建了一个RSA密钥对。现在,我尝试使用DES算法加密私钥,将其格式化为PKCS#5并在控制台上打印。不幸的是,生成的私钥不起作用。当我尝试使用它时,输入 right 密码后,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);
}

这是生成的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-----

谢谢。

2 个答案:

答案 0 :(得分:2)

没有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,或者是on the web,并且加密部分位于“ PEM ENCRYPTION FORMAT”部分的末尾,并且EVP_BytesToKey例程在its own man page上进行了引用。简而言之: 它不使用PKCS#12/rfc7292 定义的pbeSHAwith3_keyTripleDES-CBC方案(即SHA1)或PKCS#5/rfc2898 in PBES1定义的pbeMD5withDES-CBC方案。相反,它使用具有md5和1次迭代的EVP_BytesToKey(部分基于PBKDF1的 )和等于IV的salt来导出密钥,然后使用任何受支持的对称密钥进行加密/解密要求使用IV(因此不是流或ECB)但通常默认为DES-EDE3(也称为3key-TripleDES)CBC的密码模式。是的,niter = 1的EVP_BytesToKey是较差的PBKDF,除非您使用非常强的密码,否则这些文件将变得不安全。已经有很多关于此的问题。

最后,此文件格式的纯文本不是PKCS#8 (generic) encoding returned by [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 client'是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-----");

最后,OpenSSL格式要求加密IV和PBKDF盐相同,并且使该值是随机的,所以我也这样做。仅用于盐的计算值MD5(password || data)模糊地类似于现在已被接受用于加密的合成IV(SIV)构造,但它并不相同,而且我不知道是否任何胜任的分析师都考虑过SIV也用于PBKDF盐的情况,因此在这里我不愿依赖此技术。如果您想问一下这一点,那实际上不是编程问题,它更适合于cryptography.SX或security.SX。


添加了评论:

在Windows(从上游= chiark)和CentOS6(从EPEL)上,该代码的输出都适用于0.70的puttygen。根据消息来源,您给出的错误消息仅在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 -vetsed -n l之类的内容或在紧缩od -c中查看第一行的每个字节(也许是前两行)。

RFC 2898现在比较老了;当今的最佳实践通常是数万至十万次迭代,更好的实践是根本不使用迭代的哈希,而是使用诸如scrypt或Argon2之类的难以存储的东西。但是正如我已经写过的,OpenSSL遗留的PEM加密是在1990年代设计的,它使用ONE(un,eine,1)迭代,因此是一种不良和不安全的方案。现在没有人可以更改它,因为这是它的设计方式。如果您想要不错的PBE,请不要使用此格式。

如果您仅需要SSH密钥:OpenSSH(已经有几年了)支持,最新版本的Putty(gen)可以导入OpenSSH定义的“新格式”,该格式使用bcrypt,但jsch不能。 OpenSSH(使用OpenSSL)还可以读取(PEM)PKCS8,从而允许PBKDF2(虽然不是最好的)具有所需的迭代,并且看起来像jsch可以,但Putty(gen)却不行。我不了解Cyber​​duck或其他实现方式。

答案 1 :(得分:-1)

先生

我认为,出于安全原因,在调用加密之前,您需要解密两次以上。代替盐也使用胡椒盐和胡椒粉。请勿将算法与aes256混合使用。

亲切的问候,拉吉什