簡體   English   中英

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

[英]How to encrypt RSA private key with PBE in PKCS#5 format in Java with 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);
}

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

提前致謝。

沒有 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 方案。 相反,它使用帶有 md5 和 1 次迭代的EVP_BytesToKey部分基於 PBKDF1)和等於 IV 的鹽來派生密鑰,然后使用任何支持的使用 IV 的對稱密碼模式(因此不是流或ECB),但通常默認為 DES-EDE3(又名 3key-TripleDES)CBC。 是的,帶有 niter=1 的 EVP_BytesToKey 是一個糟糕的 PBKDF 並且使這些文件不安全,除非您使用非常強的密碼; 已經有很多關於這個的問題了。

最后,這種文件格式的明文不是[RSA]PrivateKey.getEncoded()返回PKCS#8(通用)編碼,而是PKCS#1/rfc8017 et pred定義的 RSA-only 格式。 並且 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-----");

最后,OpenSSL 格式要求加密 IV 和 PBKDF 鹽相同,並且它使該值隨機,所以我也這樣做了。 您僅用於 salt 的計算值 MD5(password||data),與現在被接受用於加密的合成 IV (SIV) 結構有些相似,但它不一樣,另外我不知道任何有能力的分析師都考慮過 SIV用於 PBKDF 鹽的情況,所以我不願意在這里依賴這種技術。 如果您想詢問這一點,它並不是真正的編程 Q,而是更適合於 cryptography.SX 或 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中至少查看第一行的每個字節(可能是前兩行)。

RFC 2898 現在已經很老了; 今天的良好實踐通常是數十萬到數十萬次迭代,更好的做法是根本不使用迭代哈希,而是使用諸如 scrypt 或 Argon2 之類的內存困難的東西。 但正如我已經寫過的,OpenSSL 傳統 PEM 加密是在 1990 年代設計的,它使用 ONE (un, eine, 1) 迭代,因此是一種 POOR 和 INSECURE 方案。 現在沒有人可以改變它,因為它就是這樣設計的。 如果您想要體面的 PBE,請不要使用這種格式。

如果您只需要一個用於 SSH 的密鑰:OpenSSH(幾年來)支持並且最新版本的 Putty(gen) 可以導入 OpenSSH 定義的“新格式”,它使用 bcrypt,但 jsch 不能。 OpenSSH(使用 OpenSSL)還可以讀取(PEM)PKCS8,它允許 PBKDF2(雖然不是最好的)根據需要進行迭代,並且看起來像 jsch 可以,但不是 Putty(gen)。 我不知道 Cyber​​duck 或其他實現。

先生

我認為在調用 encrypt 之前,出於安全原因,您需要再解密兩次。 也可以用胡椒鹽和胡椒代替鹽。 不要將算法與 aes256 混合。

親切的問候,拉吉什

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM