Skip to content

Commit 5651d85

Browse files
committed
[compat] implement RSA#private_to_der/pem
and improve compatibility parsing encrypted key info
1 parent eee9cd3 commit 5651d85

File tree

5 files changed

+337
-14
lines changed

5 files changed

+337
-14
lines changed

src/main/java/org/jruby/ext/openssl/PKey.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import org.jruby.ext.openssl.x509store.PEMInputOutput;
6363

6464
import static org.jruby.ext.openssl.OpenSSL.*;
65+
import static org.jruby.ext.openssl.Cipher._Cipher;
6566

6667
/**
6768
* @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
@@ -126,13 +127,22 @@ public static IRubyObject read(final ThreadContext context, IRubyObject recv, IR
126127

127128
final RubyString str = readInitArg(context, data);
128129
KeyPair keyPair;
129-
// d2i_PrivateKey_bio
130+
// d2i_PrivateKey_bio (PEM formats: RSA PRIVATE KEY, DSA PRIVATE KEY, PRIVATE KEY, ENCRYPTED PRIVATE KEY)
130131
try {
131132
keyPair = readPrivateKey(str, pass);
132133
} catch (IOException e) {
133134
debugStackTrace(runtime, "PKey readPrivateKey", e); /* ignore */
134135
keyPair = null;
135136
}
137+
// DER-encoded PKCS#8 PrivateKeyInfo or EncryptedPrivateKeyInfo
138+
if (keyPair == null) {
139+
try {
140+
final byte[] derInput = str.getBytes();
141+
keyPair = PEMInputOutput.readPrivateKeyFromDER(derInput, pass);
142+
} catch (IOException e) {
143+
debugStackTrace(runtime, "PKey readPrivateKeyFromDER", e); /* ignore */
144+
}
145+
}
136146
// PEM_read_bio_PrivateKey
137147
if (keyPair != null) {
138148
final String alg = getAlgorithm(keyPair);
@@ -395,9 +405,16 @@ static void addSplittedAndFormatted(StringBuilder result, CharSequence v) {
395405
}
396406

397407
protected static CipherSpec cipherSpec(final IRubyObject cipher) {
398-
if ( cipher != null && ! cipher.isNil() ) {
399-
final Cipher c = (Cipher) cipher;
400-
return new CipherSpec(c.getCipherInstance(), c.getName(), c.getKeyLength() * 8);
408+
Cipher obj = null;
409+
if (cipher instanceof RubyString) {
410+
final Ruby runtime = cipher.getRuntime();
411+
obj = new Cipher(runtime, _Cipher(runtime));
412+
obj.initializeImpl(runtime, cipher.asString().toString());
413+
} else if (cipher instanceof Cipher) {
414+
obj = (Cipher) cipher;
415+
}
416+
if (obj != null) {
417+
return new CipherSpec(obj.getCipherInstance(), obj.getName(), obj.getKeyLength() * 8);
401418
}
402419
return null;
403420
}

src/main/java/org/jruby/ext/openssl/PKeyRSA.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,15 @@
5151

5252
import static javax.crypto.Cipher.*;
5353

54+
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
5455
import org.bouncycastle.asn1.ASN1Primitive;
56+
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
57+
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
58+
import org.bouncycastle.operator.OutputEncryptor;
59+
import org.bouncycastle.operator.OperatorCreationException;
60+
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
61+
import org.bouncycastle.pkcs.jcajce.JcaPKCS8EncryptedPrivateKeyInfoBuilder;
62+
import org.bouncycastle.pkcs.jcajce.JcePKCSPBEOutputEncryptorBuilder;
5563
import org.jruby.Ruby;
5664
import org.jruby.RubyClass;
5765
import org.jruby.RubyBignum;
@@ -502,6 +510,89 @@ public RubyString public_to_pem(ThreadContext context) {
502510
}
503511
}
504512

513+
@JRubyMethod(rest = true)
514+
public RubyString private_to_der(ThreadContext context, final IRubyObject[] args) {
515+
Arity.checkArgumentCount(context.runtime, args, 0, 2);
516+
if (privateKey == null) {
517+
throw newRSAError(context.runtime, "private key is not available");
518+
}
519+
CipherSpec spec = null; char[] passwd = null;
520+
if (args.length > 0) {
521+
spec = cipherSpec(args[0]);
522+
if (args.length > 1) passwd = password(context, args[1], null);
523+
}
524+
try {
525+
if (spec != null && passwd != null) {
526+
final ASN1ObjectIdentifier cipherOid = osslNameToCipherOid(spec.getOsslName());
527+
final OutputEncryptor encryptor = new JcePKCSPBEOutputEncryptorBuilder(cipherOid)
528+
.setProvider(SecurityHelper.getSecurityProvider()).build(passwd);
529+
final PKCS8EncryptedPrivateKeyInfo enc = new JcaPKCS8EncryptedPrivateKeyInfoBuilder(privateKey).build(encryptor);
530+
return StringHelper.newString(context.runtime, enc.getEncoded());
531+
}
532+
return StringHelper.newString(context.runtime, privateKey.getEncoded());
533+
}
534+
catch (NoClassDefFoundError e) {
535+
throw newRSAError(context.runtime, bcExceptionMessage(e));
536+
}
537+
catch (OperatorCreationException | IOException e) {
538+
throw newRSAError(context.runtime, e.getMessage(), e);
539+
}
540+
}
541+
542+
@JRubyMethod(rest = true)
543+
public RubyString private_to_pem(ThreadContext context, final IRubyObject[] args) {
544+
Arity.checkArgumentCount(context.runtime, args, 0, 2);
545+
if (privateKey == null) {
546+
throw newRSAError(context.runtime, "private key is not available");
547+
}
548+
CipherSpec spec = null; char[] passwd = null;
549+
if (args.length > 0) {
550+
spec = cipherSpec(args[0]);
551+
if (args.length > 1) passwd = password(context, args[1], null);
552+
}
553+
try {
554+
final StringWriter writer = new StringWriter();
555+
if (spec != null && passwd != null) {
556+
final ASN1ObjectIdentifier cipherOid = osslNameToCipherOid(spec.getOsslName());
557+
final OutputEncryptor encryptor = new JcePKCSPBEOutputEncryptorBuilder(cipherOid)
558+
.setProvider(SecurityHelper.getSecurityProvider()).build(passwd);
559+
final PKCS8EncryptedPrivateKeyInfo enc = new JcaPKCS8EncryptedPrivateKeyInfoBuilder(privateKey).build(encryptor);
560+
PEMInputOutput.writeEncryptedPKCS8PrivateKey(writer, enc.getEncoded());
561+
} else {
562+
PEMInputOutput.writePKCS8PrivateKey(writer, privateKey.getEncoded());
563+
}
564+
return RubyString.newString(context.runtime, writer.getBuffer());
565+
}
566+
catch (NoClassDefFoundError e) {
567+
throw newRSAError(context.runtime, bcExceptionMessage(e));
568+
}
569+
catch (OperatorCreationException | IOException e) {
570+
throw newRSAError(context.runtime, e.getMessage(), e);
571+
}
572+
}
573+
574+
private static ASN1ObjectIdentifier osslNameToCipherOid(final String osslName) {
575+
switch (osslName.toUpperCase()) {
576+
case "AES-128-CBC": return NISTObjectIdentifiers.id_aes128_CBC;
577+
case "AES-192-CBC": return NISTObjectIdentifiers.id_aes192_CBC;
578+
case "AES-256-CBC": return NISTObjectIdentifiers.id_aes256_CBC;
579+
case "AES-128-ECB": return NISTObjectIdentifiers.id_aes128_ECB;
580+
case "AES-192-ECB": return NISTObjectIdentifiers.id_aes192_ECB;
581+
case "AES-256-ECB": return NISTObjectIdentifiers.id_aes256_ECB;
582+
case "AES-128-OFB": return NISTObjectIdentifiers.id_aes128_OFB;
583+
case "AES-192-OFB": return NISTObjectIdentifiers.id_aes192_OFB;
584+
case "AES-256-OFB": return NISTObjectIdentifiers.id_aes256_OFB;
585+
case "AES-128-CFB": return NISTObjectIdentifiers.id_aes128_CFB;
586+
case "AES-192-CFB": return NISTObjectIdentifiers.id_aes192_CFB;
587+
case "AES-256-CFB": return NISTObjectIdentifiers.id_aes256_CFB;
588+
case "DES-EDE3-CBC":
589+
case "DES-EDE-CBC":
590+
case "DES3": return PKCSObjectIdentifiers.des_EDE3_CBC;
591+
default:
592+
throw new IllegalArgumentException("Unsupported cipher for PKCS8 encryption: " + osslName);
593+
}
594+
}
595+
505596
private String getPadding(final int padding) {
506597
if ( padding < 1 || padding > 4 ) {
507598
throw newRSAError(getRuntime(), "");

src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@
112112
import org.bouncycastle.crypto.CipherParameters;
113113
import org.bouncycastle.crypto.InvalidCipherTextException;
114114
import org.bouncycastle.crypto.PBEParametersGenerator;
115+
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
116+
import org.bouncycastle.crypto.engines.AESEngine;
115117
import org.bouncycastle.crypto.engines.DESedeEngine;
116118
import org.bouncycastle.crypto.engines.RC2Engine;
117119
import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator;
@@ -360,14 +362,15 @@ else if ( line.indexOf(BEG_STRING_PKCS8) != -1 ) {
360362
try {
361363
byte[] bytes = readBase64Bytes(reader, BEF_E + PEM_STRING_PKCS8);
362364
EncryptedPrivateKeyInfo eIn = EncryptedPrivateKeyInfo.getInstance(bytes);
363-
AlgorithmIdentifier algId = eIn.getEncryptionAlgorithm();
364-
PrivateKey privKey;
365-
if (algId.getAlgorithm().toString().equals("1.2.840.113549.1.5.13")) { // PBES2
366-
privKey = derivePrivateKeyPBES2(eIn, algId, passwd);
367-
} else {
368-
privKey = derivePrivateKeyPBES1(eIn, algId, passwd);
369-
}
370-
return new KeyPair(null, privKey);
365+
org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo encInfo =
366+
new org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo(eIn);
367+
org.bouncycastle.operator.InputDecryptorProvider decryptor =
368+
new org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder()
369+
.setProvider(SecurityHelper.getSecurityProvider())
370+
.build(passwd);
371+
final PrivateKeyInfo keyInfo = encInfo.decryptPrivateKeyInfo(decryptor);
372+
final Type type = getPrivateKeyType(keyInfo.getPrivateKeyAlgorithm());
373+
return org.jruby.ext.openssl.impl.PKey.readPrivateKey(type, keyInfo);
371374
}
372375
catch (Exception e) {
373376
throw mapReadException("problem creating private key: ", e);
@@ -377,6 +380,43 @@ else if ( line.indexOf(BEG_STRING_PKCS8) != -1 ) {
377380
return null;
378381
}
379382

383+
/**
384+
* Attempt to read a private key from DER-encoded PKCS#8 bytes (PrivateKeyInfo or
385+
* EncryptedPrivateKeyInfo). Returns null if the input cannot be parsed as either format.
386+
*/
387+
public static KeyPair readPrivateKeyFromDER(final byte[] input, final char[] passwd) throws IOException {
388+
// Try as unencrypted PKCS#8 PrivateKeyInfo
389+
try {
390+
final PrivateKeyInfo keyInfo = PrivateKeyInfo.getInstance(ASN1Primitive.fromByteArray(input));
391+
if (keyInfo != null) {
392+
final Type type = getPrivateKeyType(keyInfo.getPrivateKeyAlgorithm());
393+
return org.jruby.ext.openssl.impl.PKey.readPrivateKey(type, keyInfo);
394+
}
395+
} catch (Exception e) {
396+
// Not a PrivateKeyInfo - try as EncryptedPrivateKeyInfo
397+
}
398+
// Try as encrypted PKCS#8 EncryptedPrivateKeyInfo
399+
if (passwd != null) {
400+
try {
401+
EncryptedPrivateKeyInfo eIn = EncryptedPrivateKeyInfo.getInstance(ASN1Primitive.fromByteArray(input));
402+
if (eIn != null) {
403+
org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo encInfo =
404+
new org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo(eIn);
405+
org.bouncycastle.operator.InputDecryptorProvider decryptor =
406+
new org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder()
407+
.setProvider(SecurityHelper.getSecurityProvider())
408+
.build(passwd);
409+
final PrivateKeyInfo keyInfo = encInfo.decryptPrivateKeyInfo(decryptor);
410+
final Type type = getPrivateKeyType(keyInfo.getPrivateKeyAlgorithm());
411+
return org.jruby.ext.openssl.impl.PKey.readPrivateKey(type, keyInfo);
412+
}
413+
} catch (Exception e) {
414+
throw new IOException("Could not decrypt PKCS#8 key: " + e.getMessage(), e);
415+
}
416+
}
417+
return null;
418+
}
419+
380420
private static IOException mapReadException(final String message, final Exception ex) {
381421
if ( ex instanceof PasswordRequiredException ) {
382422
return (PasswordRequiredException) ex;
@@ -416,12 +456,20 @@ private static PrivateKey derivePrivateKeyPBES2(EncryptedPrivateKeyInfo eIn, Alg
416456

417457
EncryptionScheme scheme = pbeParams.getEncryptionScheme();
418458
BufferedBlockCipher cipher;
419-
if ( scheme.getAlgorithm().equals( PKCSObjectIdentifiers.RC2_CBC ) ) {
459+
ASN1ObjectIdentifier encOid = scheme.getAlgorithm();
460+
if ( encOid.equals( PKCSObjectIdentifiers.RC2_CBC ) ) {
420461
RC2CBCParameter rc2Params = RC2CBCParameter.getInstance(scheme);
421462
byte[] iv = rc2Params.getIV();
422463
CipherParameters param = new ParametersWithIV(cipherParams, iv);
423464
cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new RC2Engine()));
424465
cipher.init(false, param);
466+
} else if ( encOid.equals( NISTObjectIdentifiers.id_aes128_CBC ) ||
467+
encOid.equals( NISTObjectIdentifiers.id_aes192_CBC ) ||
468+
encOid.equals( NISTObjectIdentifiers.id_aes256_CBC ) ) {
469+
byte[] iv = ASN1OctetString.getInstance( scheme.getParameters() ).getOctets();
470+
CipherParameters param = new ParametersWithIV(cipherParams, iv);
471+
cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()));
472+
cipher.init(false, param);
425473
} else {
426474
byte[] iv = ASN1OctetString.getInstance( scheme.getParameters() ).getOctets();
427475
CipherParameters param = new ParametersWithIV(cipherParams, iv);
@@ -1053,6 +1101,18 @@ public static void writeECPrivateKey(Writer _out, ECPrivateKey obj, CipherSpec c
10531101
}
10541102
}
10551103

1104+
/** Writes a PKCS#8 unencrypted private key in PEM format ("PRIVATE KEY"). */
1105+
public static void writePKCS8PrivateKey(Writer _out, byte[] pkcs8Bytes) throws IOException {
1106+
BufferedWriter out = makeBuffered(_out);
1107+
writePemPlain(out, PEM_STRING_PKCS8INF, pkcs8Bytes);
1108+
}
1109+
1110+
/** Writes a PKCS#8 encrypted private key in PEM format ("ENCRYPTED PRIVATE KEY"). */
1111+
public static void writeEncryptedPKCS8PrivateKey(Writer _out, byte[] encryptedPKCS8Bytes) throws IOException {
1112+
BufferedWriter out = makeBuffered(_out);
1113+
writePemPlain(out, PEM_STRING_PKCS8, encryptedPKCS8Bytes);
1114+
}
1115+
10561116
public static void writeECParameters(Writer _out, ASN1ObjectIdentifier obj, CipherSpec cipher, char[] passwd) throws IOException {
10571117
assert (obj != null);
10581118
final String PEM_STRING_EC = "EC PARAMETERS";
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
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
51+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)