Как зашифровать закрытый ключ RSA с помощью PBE в формате PKCS#5 на Java с помощью 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);
}
И это сгенерированный закрытый ключ в формате PCS PKCS#5:
-----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 ответа
Нет такого понятия, как формат PKCS#5. PKCS#5 в первую очередь определяет две функции получения ключей на основе пароля и схемы шифрования на основе пароля с их использованием, а также схему MAC на основе пароля, но не определяет никакого формата для данных. (Он определяет OID ASN.1 для этих операций и структуры ASN.1 для их параметров - прежде всего PBKDF2 и PBES2, потому что единственным параметром для PBKDF1 и PBES1 является соль.) PKCS#5 также определяет схему заполнения для Шифрование данных в режиме CBC; это заполнение было немного улучшено PKCS#7 и использовалось многими другими приложениями, которые обычно называют это заполнением PKCS5 или дополнением PKCS7. Ни один из них не является форматом данных, и ни один из них не включает закрытые ключи RSA (или другие) как таковые.
Очевидно, что вам нужен формат файла, который используется OpenSSH (долгое время всегда, затем в течение последних нескольких лет по умолчанию, пока OpenSSH 7.8 всего месяц назад не сделал его необязательным), и в результате он также используется другим программным обеспечением, которое хочет быть совместимым или даже взаимозаменяемым с OpenSSH. Этот формат фактически определяется OpenSSL, который OpenSSH долгое время использовал для большей части своей криптографии. (После Heartbleed OpenSSH создал форк OpenSSL под названием LibreSSL, который пытается быть более надежным и безопасным внутри, но намеренно поддерживает те же внешние интерфейсы и форматы, и в любом случае не получил широкого распространения.)
Это один из нескольких форматов 'PEM', определенных OpenSSL, и в основном он описан на странице руководства для ряда процедур PEM, включая PEM_write[_bio]_RSAPrivateKey
- в вашей системе, если у вас есть OpenSSL, а это не Windows, или в Интернете, где часть шифрования находится в конце раздела "ФОРМАТ ШИФРОВАНИЯ PEM", и подпрограмма EVP_BytesToKey, на которую она ссылается аналогичным образом на своей собственной странице руководства. Вкратце: он не использует схему pbeSHAwith3_keyTripleDES-CBC (имеется в виду SHA1), определенную PKCS#12/rfc7292,или схему pbeMD5withDES-CBC, определенную PKCS#5/rfc2898 в PBES1. Вместо этого он использует EVP_BytesToKey
(который частично основан на PBKDF1) с итерацией md5 и 1 и солью, равной IV, для получения ключа, а затем шифрует / дешифрует с любым поддерживаемым режимом симметричного шифра, который использует IV (таким образом, не поток или ECB), но обычно по умолчанию для CBC DES-EDE3 (он же 3key-TripleDES), как вы просите. Да, EVP_BytesToKey с niter=1 является плохим PBKDF и делает эти файлы незащищенными, если вы не используете очень надежный пароль; Есть множество вопросов об этом уже.
И, наконец, открытый текст этого формата файла не является кодировкой PKCS#8 (универсальной), возвращаемой[RSA]PrivateKey.getEncoded()
скорее формат только для RSA, определенный PKCS # 1 / rfc8017 et pred. Требуется пустая строка между заголовками Proc-type и DEK-info и base64, а также может потребоваться терминатор строки в строке dashes-END в зависимости от того, какое программное обеспечение выполняет чтение.
Самый простой способ сделать это - использовать программное обеспечение, уже совместимое с форматом (-ами) PEM с закрытым ключом OpenSSL, включая сам 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(пароль ||data), немного напоминает конструкцию синтетического IV (SIV), которая теперь принята для использования с шифрованием, но это не то же самое, плюс я не знаю, Любой компетентный аналитик рассмотрел случай, когда SIV также используется для соли PBKDF, поэтому я не хотел бы полагаться на эту технику здесь. Если вы хотите спросить об этом, это не совсем вопрос программирования, и он больше подходит для cryptography.SX или, возможно, security.SX.
добавлено для комментариев:
Вывод этого кода работает для меня с puttygen от 0.70, как в Windows (из upstream=chiark), так и в CentOS6 (из EPEL). Согласно источнику, сообщение об ошибке, которое вы дали, появляется, только если cmdgen вызвал key_type в sshpubk.c, который распознал первую строку как начинающуюся с "-----BEGIN ", но не "-----BEGIN OPENSSH PRIVATE KEY" (это совсем другой формат), затем через import_ssh2 и openssh_pem_read с именем load_openssh_pem_key в файле import.c, который НЕ находит первую строку, начинающуюся с "-----BEGIN " и заканчивающуюся "PRIVATE KEY-----". Это очень странно, потому что оба этих ПЛЮС "RSA " между ними генерируются моим кодом и необходимы для его принятия OpenSSH (или openssl). Попробуйте посмотреть на каждый байт первой строки как минимум (возможно, первые две строки) с чем-то вроде cat -vet
или же sed -n l
или в крайнем случае od -c
,
RFC 2898 сейчас довольно старый; Хорошая практика сегодня обычно составляет от 10 до 100 тысяч итераций, и лучше не использовать итеративный хеш, а вместо этого что-то с нехваткой памяти, как scrypt или Argon2. Но, как я уже писал, унаследованное PEM-шифрование OpenSSL, которое было разработано еще в 1990-х годах, использует одну итерацию (un, eine, 1) и, следовательно, является схемой POOR и INSECURE. Никто не может изменить это сейчас, потому что именно так оно и было задумано. Если вы хотите достойный PBE, не используйте этот формат.
Если вам нужен ключ только для SSH: OpenSSH (уже несколько лет) поддерживает, и последние версии Putty (gen) могут импортировать определенный OpenSSH "новый формат", который использует bcrypt, но jsch не может. OpenSSH (используя OpenSSL) также может читать (PEM) PKCS8, что позволяет PBKDF2 (лучше, но не лучше) с итерациями по желанию, и это похоже на jsch, но не Putty(gen). Я не знаю для Cyberduck или других реализаций.
Господин
Я думаю, что перед вызовом шифрования вам нужно расшифровать в два раза больше по соображениям безопасности. Вместо соли используйте также перец и соль. Не смешивайте алгоритм с aes256.
С наилучшими пожеланиями, Раджеш