3

我创建了一个 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);
}

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

提前致谢。

4

2 回答 2

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 ENCRYPTION FORMAT' 部分末尾附近的加密部分,以及它在其自己的手册页中类似地引用的 EVP_BytesToKey 例程。简而言之:它不使用PKCS#12/rfc7292定义的 pbeSHAwith3_keyTripleDES-CBC(意思是 SHA1)方案 或PBES1 中的PKCS#5/rfc2898定义的 pbeMD5withDES-CBC 方案。相反,它使用EVP_BytesToKey部分是基于 PBKDF1),md5 和 1 次迭代,salt 等于 IV,以派生密钥,然后使用任何支持的对称密码模式加密/解密,该模式使用 IV(因此不是流或 ECB),但通常默认为 DES- EDE3(又名 3key-TripleDES)CBC,如您所愿。是的,niter=1 的 EVP_BytesToKey 是一个糟糕的 PBKDF,除非您使用非常强的密码,否则这些文件会变得不安全;已经有很多关于这个的问题。

最后,这种文件格式的明文不是由PKCS#8(通用)编码返回,[RSA]PrivateKey.getEncoded()而是由PKCS#1/rfc8017 et pred定义的仅 RSA 格式。并且需要 Proc-type 和 DEK-info 标头与 base64 之间的空行,并且可能需要 dashes-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 salt 相同,它使该值随机,所以我也这样做了。您仅用于盐的计算值 MD5(password||data) 模糊地类似于现在被接受用于加密的合成 IV (SIV) 结构,但它不一样,而且我不知道是否任何有能力的分析师都考虑过 SIV用于 PBKDF 盐的情况,所以我不愿意在这里依赖这种技术。如果你想问这个问题,它并不是真正的编程 Q,它更适合 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-----”结尾的第一行。这很奇怪,因为介于两者之间的 PLUS“RSA”都是由我的代码生成的 OpenSSH(或 openssl)需要它来接受它。cat -vetsed -n l或在紧要关头od -c

RFC 2898 现在相当老了。今天的良好实践通常是成千上万次迭代的 10 到 100 次,更好的做法是根本不使用迭代哈希,而是使用像 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 或其他实现。

于 2018-10-04T05:03:24.710 回答
-1

先生

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

亲切的问候,拉吉什

于 2018-10-03T20:16:04.373 回答