Skip to content

Commit 153da24

Browse files
committed
Added legacy parsing of encrypted EdEC keys. Relates to github #1238
1 parent 4e28866 commit 153da24

3 files changed

Lines changed: 153 additions & 1 deletion

File tree

‎docs/releasenotes.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ <h3>2.1.2 Defects Fixed</h3>
2929
<li>X509CertificateFormatter was indenting the basicConstraints pathLenConstraint line incorrectly, dropping the 23-space prefix shared with other extension lines. The line now uses the same pad as the surrounding output (issue #2214).</li>
3030
<li>A range of catch-and-rethrow sites that wrapped underlying failures as IllegalArgumentException or IllegalStateException without preserving the cause have been updated to chain the original exception via org.bouncycastle.util.Exceptions, retaining the full stack trace. Affected areas include the ASN.1 parsing entry points, the ASN.1 X.500 / X.509 / CMS / DVCS / CMC support classes, the LMS / HSS / ML-DSA / SLH-DSA / XMSS signers, the DH/ECDH KEK generators, the CMS/PKIX selectors and key converters, the JCE provider IES/GM cipher and KeyFactory SPIs, the GOST3410 / Composite key wrappers, the X.509 certificate and CRL implementations, the JceTlsHMAC wrapper, the ITS signer/verifier classes, and the OpenPGP encryptor/decryptor builders (issue #2239 / PR #2250).</li>
3131
<li>PGPPublicKey.getValidSeconds() returned a stale expiration time when an earlier self-signature carried a Key Expiration Time subpacket and a more recent self-signature omitted it; per RFC 4880 5.2.4.1 the latest self-signature wins, so the absence of the subpacket on the newer signature now correctly cancels the expiry and the method returns 0 (issue #1749).</li>
32+
<li>PEMParser failed to parse a "BEGIN PRIVATE KEY" block carrying OpenSSL-legacy encryption headers (Proc-Type: 4,ENCRYPTED / DEK-Info), throwing "corrupted stream" while ASN.1-decoding the ciphertext. The PRIVATE KEY parser now honours those headers and returns a PEMEncryptedKeyPair whose decryptKeyPair() yields a PEMKeyPair holding the decrypted PKCS#8 PrivateKeyInfo (issue #1238).</li>
3233
</ul>
3334
<h3>2.2.3 Additional Features and Functionality</h3>
3435
<ul>

‎pkix/src/main/java/org/bouncycastle/openssl/PEMParser.java‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,14 +561,68 @@ public PrivateKeyParser()
561561
public Object parseObject(PemObject obj)
562562
throws IOException
563563
{
564+
// Some tooling emits a PKCS#8 PrivateKeyInfo wrapped in OpenSSL's legacy
565+
// Proc-Type/DEK-Info encryption headers under a "PRIVATE KEY" label
566+
// (e.g. github #1238). Honour those headers and return a
567+
// PEMEncryptedKeyPair that decrypts to a PrivateKeyInfo.
568+
boolean isEncrypted = false;
569+
String dekInfo = null;
570+
571+
for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
572+
{
573+
PemHeader hdr = (PemHeader)it.next();
574+
575+
if (hdr.getName().equals("Proc-Type") && hdr.getValue().equals("4,ENCRYPTED"))
576+
{
577+
isEncrypted = true;
578+
}
579+
else if (hdr.getName().equals("DEK-Info"))
580+
{
581+
dekInfo = hdr.getValue();
582+
}
583+
}
584+
585+
byte[] keyBytes = obj.getContent();
586+
564587
try
565588
{
566-
return PrivateKeyInfo.getInstance(obj.getContent());
589+
if (isEncrypted)
590+
{
591+
StringTokenizer tknz = new StringTokenizer(dekInfo, ",");
592+
String dekAlgName = tknz.nextToken();
593+
byte[] iv = Hex.decode(tknz.nextToken());
594+
595+
return new PEMEncryptedKeyPair(dekAlgName, iv, keyBytes,
596+
new PrivateKeyInfoKeyPairParser());
597+
}
598+
599+
return PrivateKeyInfo.getInstance(keyBytes);
567600
}
568601
catch (Exception e)
569602
{
603+
if (isEncrypted)
604+
{
605+
throw new PEMException("exception decoding - please check password and data.", e);
606+
}
570607
throw new PEMException("problem parsing PRIVATE KEY: " + e.toString(), e);
571608
}
572609
}
573610
}
611+
612+
private static class PrivateKeyInfoKeyPairParser
613+
implements PEMKeyPairParser
614+
{
615+
public PEMKeyPair parse(byte[] encoding)
616+
throws IOException
617+
{
618+
try
619+
{
620+
return new PEMKeyPair(null, PrivateKeyInfo.getInstance(encoding));
621+
}
622+
catch (Exception e)
623+
{
624+
throw new PEMException("problem creating private key: " + e.toString(), e);
625+
}
626+
}
627+
}
574628
}

‎pkix/src/test/java/org/bouncycastle/openssl/test/ParserTest.java‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
3030
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
3131
import org.bouncycastle.asn1.cms.ContentInfo;
32+
import org.bouncycastle.asn1.edec.EdECObjectIdentifiers;
3233
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
3334
import org.bouncycastle.asn1.x509.KeyPurposeId;
3435
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
@@ -42,6 +43,7 @@
4243
import org.bouncycastle.openssl.CertificateTrustBlock;
4344
import org.bouncycastle.openssl.PEMDecryptorProvider;
4445
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
46+
import org.bouncycastle.openssl.PEMEncryptor;
4547
import org.bouncycastle.openssl.PEMKeyPair;
4648
import org.bouncycastle.openssl.PEMParser;
4749
import org.bouncycastle.openssl.X509TrustedCertificateBlock;
@@ -50,9 +52,13 @@
5052
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
5153
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
5254
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
55+
import org.bouncycastle.openssl.jcajce.JcePEMEncryptorBuilder;
5356
import org.bouncycastle.operator.InputDecryptorProvider;
5457
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
58+
import org.bouncycastle.util.Arrays;
5559
import org.bouncycastle.util.Strings;
60+
import org.bouncycastle.util.encoders.Base64;
61+
import org.bouncycastle.util.encoders.Hex;
5662
import org.bouncycastle.util.test.SimpleTest;
5763

5864
/**
@@ -378,6 +384,97 @@ public void performTest()
378384

379385
doOpenSslGost2012Test();
380386
doParseAttrECKeyTest();
387+
doLegacyEncryptedPkcs8PemTest();
388+
doLegacyEncryptedPkcs8GenPemTest();
389+
}
390+
391+
private void doLegacyEncryptedPkcs8PemTest()
392+
throws Exception
393+
{
394+
char[] password = "Vjvyhfngz0MCUs$kwOF0".toCharArray();
395+
396+
String pem = "-----BEGIN PRIVATE KEY-----\n"
397+
+ "Proc-Type: 4,ENCRYPTED\n"
398+
+ "DEK-Info: AES-128-CBC,b619a06a16b7b7a6436579f06a14f45e\n"
399+
+ "\n"
400+
+ "QmysBFzoMkgvVTM39kvHjkKhcBjK6PVMZ6a/taF44ZXeOl3t5DUp4EWxyfs8htng\n"
401+
+ "tjsKIb0yKJigIZGrCeHROQ==\n"
402+
+ "-----END PRIVATE KEY-----\n";
403+
404+
PEMParser parser = new PEMParser(new StringReader(pem.toString()));
405+
Object o = parser.readObject();
406+
407+
if (!(o instanceof PEMEncryptedKeyPair))
408+
{
409+
fail("expected PEMEncryptedKeyPair, got " + (o == null ? "null" : o.getClass().getName()));
410+
}
411+
412+
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder()
413+
.setProvider("BC").build(password);
414+
PEMKeyPair pkp = ((PEMEncryptedKeyPair)o).decryptKeyPair(decProv);
415+
416+
PrivateKeyInfo decoded = pkp.getPrivateKeyInfo();
417+
if (decoded == null)
418+
{
419+
fail("decrypted PrivateKeyInfo was null");
420+
}
421+
isEquals(EdECObjectIdentifiers.id_Ed25519, decoded.getPrivateKeyAlgorithm().getAlgorithm());
422+
}
423+
424+
private void doLegacyEncryptedPkcs8GenPemTest()
425+
throws Exception
426+
{
427+
// Reproduces github #1238: a PKCS#8 PrivateKeyInfo wrapped in OpenSSL legacy
428+
// encryption headers ("Proc-Type: 4,ENCRYPTED" / "DEK-Info") under a
429+
// "BEGIN PRIVATE KEY" label. Earlier releases tried to ASN.1-parse the
430+
// ciphertext and failed with "corrupted stream" before this could be
431+
// recognised as encrypted.
432+
char[] password = "wibble".toCharArray();
433+
434+
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519", "BC");
435+
KeyPair kp = kpg.generateKeyPair();
436+
437+
byte[] pkcs8 = kp.getPrivate().getEncoded();
438+
439+
PEMEncryptor encryptor = new JcePEMEncryptorBuilder("AES-128-CBC")
440+
.setProvider("BC").build(password);
441+
442+
byte[] encrypted = encryptor.encrypt(pkcs8);
443+
String ivHex = Strings.fromByteArray(Hex.encode(encryptor.getIV()));
444+
String b64 = Strings.fromByteArray(Base64.encode(encrypted));
445+
446+
StringBuilder pem = new StringBuilder();
447+
pem.append("-----BEGIN PRIVATE KEY-----\n");
448+
pem.append("Proc-Type: 4,ENCRYPTED\n");
449+
pem.append("DEK-Info: AES-128-CBC,").append(ivHex).append("\n");
450+
pem.append("\n");
451+
for (int i = 0; i < b64.length(); i += 64)
452+
{
453+
pem.append(b64, i, Math.min(i + 64, b64.length())).append("\n");
454+
}
455+
pem.append("-----END PRIVATE KEY-----\n");
456+
457+
PEMParser parser = new PEMParser(new StringReader(pem.toString()));
458+
Object o = parser.readObject();
459+
460+
if (!(o instanceof PEMEncryptedKeyPair))
461+
{
462+
fail("expected PEMEncryptedKeyPair, got " + (o == null ? "null" : o.getClass().getName()));
463+
}
464+
465+
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder()
466+
.setProvider("BC").build(password);
467+
PEMKeyPair pkp = ((PEMEncryptedKeyPair)o).decryptKeyPair(decProv);
468+
469+
PrivateKeyInfo decoded = pkp.getPrivateKeyInfo();
470+
if (decoded == null)
471+
{
472+
fail("decrypted PrivateKeyInfo was null");
473+
}
474+
if (!Arrays.areEqual(pkcs8, decoded.getEncoded()))
475+
{
476+
fail("decrypted PrivateKeyInfo did not round-trip");
477+
}
381478
}
382479

383480
private void checkTrustedCert(X509TrustedCertificateBlock trusted)

0 commit comments

Comments
 (0)