不存在 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 分支,它试图在内部变得更加健壮和安全,但有意保持相同的外部接口和格式,并且无论如何都没有被广泛采用。)
It is OpenSSL 定义的几种“PEM”格式之一,并且主要在许多“PEM”例程的手册页上进行了描述,包括PEM_write[_bio]_RSAPrivateKey
-- 在您的系统上(如果您有 OpenSSL 并且它不是 Windows),或者在网上 https://www.openssl.org/docs/man1.1.1/man3/PEM_write_RSAPrivateKey.html加密部分接近“PEM 加密格式”部分的末尾,并且它引用的 EVP_BytesToKey 例程类似它自己的手册页 https://www.openssl.org/docs/man1.1.1/man3/EVP_BytesToKey.html。简而言之:
它不使用 pbeSHAwith3_keyTripleDES-CBC (即 SHA1)定义的方案PKCS#12/rfc7292 https://www.rfc-editor.org/rfc/rfc7292#appendix-C orpbeMD5withDES-CBC 方案定义为PBES1 中的 PKCS#5/rfc2898 https://www.rfc-editor.org/rfc/rfc2898#appendix-A.3。相反,它使用EVP_BytesToKey
(这是partly基于 PBKDF1),使用 md5 和 1 次迭代,salt 等于 IV,以导出密钥,然后使用任何支持的使用 IV(因此不是流或 ECB)但通常默认为 DES 的对称密码模式进行加密/解密EDE3(又名 3key-TripleDES)CBC 如您所要求。是的,niter=1 的 EVP_BytesToKey 是一个很差的 PBKDF,并且会使这些文件不安全,除非您使用非常强的密码;已经有很多关于这个的问题了。
最后是纯文本该文件格式不是返回的 PKCS#8(通用)编码[RSA]PrivateKey.getEncoded() https://docs.oracle.com/javase/8/docs/api/java/security/Key.html#getFormat--而是仅由 RSA 定义的格式PKCS#1/rfc8017 等 https://www.rfc-editor.org/rfc/rfc8017#appendix-A.1.2。 Proc-type 和 DEK-info 标头与 base64 之间需要空行,并且可能需要破折号-END 行上的行终止符,具体取决于读取的软件。
最简单的方法是使用已经与 OpenSSL 私钥 PEM 格式兼容的软件,包括 OpenSSL 本身。 Java可以运行外部程序:OpenSSH的ssh-keygen
如果你有的话,或者openssl genrsa
如果你有的话。 BouncyCastle bcpkix 库支持此格式和其他 OpenSSL PEM 格式。如果“ssh 客户端”是 jsch,那么normally读取多种格式的密钥文件,包括这种格式,但是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 的情况also用于 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”都是由我的代码生成的andOpenSSH(或 openssl)需要接受它。尝试至少查看第一行(也许是前两行)的每个字节,例如cat -vet
or sed -n l
或在紧要关头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) 不行。我不知道 Cyberduck 或其他实现。