【问题标题】:Parsing encrypted PKCS#8 encoded pem file programatically以编程方式解析加密 PKCS#8 编码的 pem 文件
【发布时间】:2020-12-29 02:45:33
【问题描述】:

我无法弄清楚如何正确读取 pem 文件的私钥。我在stackoverflow上浏览了不同的主题,但我找不到解决方案。我想要实现的是从类路径中读取 pkcs#8 编码文件中的加密私钥,并将其作为密钥条目加载到内存密钥库中。下面是我尝试解析的示例私钥,密码是secret。它纯粹是为了在这里共享而创建的,因此它不是在生产机器上使用的私钥。

我使用以下命令从 p12 文件创建它:openssl pkcs12 -in key-pair.p12 -out key-pair.pem

有效示例(一次性)密钥对

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI9/FSBonUacYCAggA
MBQGCCqGSIb3DQMHBAidGkS8wYhOpwSCBMi8JaSYOKudMNNFRpzL7QMIZgFtzDay
MmOroy3lW34dOa7dusqDl4d2gklKcHCpbEaTYxm5aQJ1LuiOdGtFy7HwxOvKU5xz
4qsJoeBIpE0eCTKjQW7/I38DzLXx2wUURqhMWOtFsWZEyR5Dqok3N9mIKKKBXAFG
AwNjlTRW2LyPSttiIUGN01lthjifMWoLTWB1aSGOmGeJRBdSZeqZ15xKneR4H5ja
yE88YcpOHCDKMIxi6ZVoKs7jDQhu8bBKqS8NsYyh1AlP9QkvWNal36jWSzhqYNzk
NRWUOZngfkdbMALVfRtbrC215jHGWVwosPIIs8rkoarRv8s6QWS1Rg3YfQ3qgcRf
s7hkDFKJf3TUXr+askfamV5hc300ZG64+ldX1YxWXY8Vd/wIvHAc/YE/lTyCgYrY
19Am6MNBfp8/kXvzKj+PizB8oNDO4S8sSShEEzOQ5a/+MTC6bqB0DLWYGUqRbjLc
PyYTC2C4i9Agx/GeGVE3c1UdtXiwwnt2XUn7Y1YGqABk0xGIY4J1NFTbSOxKl9hO
arwopAFrZU5nsjjFzv1DJvhfQWnYX18kPSKNHDlia019M118qZ8ERwD9tH8ix9Fa
R2tQdxn1aRGmvXSw+zFkbWD8aWs9n/B+QN1yllJqVoWypOld1yj+fVYYnYOtV1gK
eiygrtrh3JJCvLbEQl4nOgJM3PlEtfBHSaunehIXQMD1z/NDUqgBYjuDPyqRxJeH
Va5k72Nds5PeySKJJnICB3nZKjqgfLhNUrXa1SAQ4vqr0Ik/Lu9P7T+B1XiYwuUT
a20+bxi/x89ZZqwp3jnDuHup7XcO1MtqsoOKP/JgkjVMesb8Q1W8i2dXzg+l4gkk
l1ipreEGtT1YfFTq0DFelz6CjZFLDlGGeGWob94sW94DWTW0nsLPhQWEnwW1CcyJ
oJbJdDEgdiIbRJoABDkTuVXLwTlgzHSHh6zeJvNvcojI7UI3nWYCVYvD3kwghXiP
67sKGL3ug7PFDqLia46AudGY7CFh4+wpxyH+fidLC3FMdkDBA6xR6mGgEjRLXR9M
TnJ/eSYP7eqYZeKn9EarcI7v1zM2IG0/PDQCetiI0ABiHpdKyRQuuiEavp3xC5Vi
h7UmJNYt8Zsz3rwqAQ4FR2+Su5R34OOdRmxTaYLe96PXTpLcLef5TkYixSY7Tzgd
PMyRxRPrywklUEFe4KK/KOcdolxybfsIsxQnupLAMEsO7/Cs7mouNHISK51haDRc
vNbKQ5E4xOq1U4ThW5dHR29cGZillfmMzj05ZQh3ZX2TQJP45ahFET3v9kInWCwQ
8atqclVPOSnASsJZ0PxjYgKZuY8QWYM6zpfWyWnfu/CHhWbRS/qX8T1ow2SMyPBL
CQbZ+MhcdP0IrjoXhDFQsns16i/BPK5TTVqtEC2ywDf5P4/BOEZkySG9YNOd6THp
VA/dVPafzmLy3ltqH+jG8ZH2+RtWx7kwBjiDWs5cF33BFrPS7AZlzMzZoCHLXD/r
T/SmisybUKHMqri0x0RHeIByW0hogSByWiyIn8POabDzJV6Df9nQPziDGcSsvWfG
7q+hizh6+nnXOY+GZx3ptwg9mA9R4QyCiFNQradOaXSPxyEL2IC77/srFfVEIaU4
SRo=
-----END ENCRYPTED PRIVATE KEY-----

-----BEGIN CERTIFICATE-----
MIIDTjCCAjagAwIBAgIEVnHI3TANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJO
TDEVMBMGA1UEChMMVGh1bmRlcmJlcnJ5MRIwEAYDVQQLEwlBbXN0ZXJkYW0xDjAM
BgNVBAMTBUhha2FuMB4XDTIwMDgzMTA4MDczOVoXDTMwMDgyOTA4MDczOVowSDEL
MAkGA1UEBhMCTkwxFTATBgNVBAoTDFRodW5kZXJiZXJyeTESMBAGA1UECxMJQW1z
dGVyZGFtMQ4wDAYDVQQDEwVIYWthbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAJZ+pqirEqEk4k1ow8vryld79oCO4P/1y+v60CkLpS2MpeHE3BogTr7g
WWP5HdHBMU5l8yT5tXuyVZZqgyzo0q2Sdm7GGrNunHf6Z8vPQFu69sQC1utDM9u8
ZFKiKsTNJ+5QS6KsOtlACyhaLoaPAWVjtvueMjVwmM9hfv/Gq6VyyuBm2x1C4HTj
zPCLHE2G1D13EJaWsvyZLGSbl0GGXZGPhaDd/vnw5TW36mvNTWW+37ZIEk4kXANa
FUNsJemm8HjB/PzHs3/SXGxuD8NKobg3+cNXYwAz2s2DI0W6Xw2g5bbrMQAdBRvn
9/kNftMymDORw3RGwDM2ld4zQfIkNrkCAwEAAaNAMD4wHQYDVR0OBBYEFHhT7ATg
oVVFIsglxD/1iUBRB6gDMB0GA1UdEQEB/wQTMBGCCWxvY2FsaG9zdIcEfwAAATAN
BgkqhkiG9w0BAQsFAAOCAQEAhVWH/CgZ0ZNmTfiFAOnWdaZVaa7vAFPT2YbXuvlY
YIRlru0B/zn2Mfwmn5W2o1CqoBuhyfErkkF4aRM1vduIirUjlcH4+cFXrV2gtlnf
eWTg/sJJmYzkJTGeOIqRlB1HKCqoeNCrykkcsikECQ1nCqr1qLh9DXsUgWVW57YW
qvP1P8xOO2/J9shMB6lOhftpawrqZ2hNG8fqMKjVVuUpFBNR+WODQ/rRRtqa6uU2
V8aOOZx1QJUkTdN76YOCuGET7edevjpdbRXde+HQN6mbT9OLxSZHO0aQrDyDmNhp
aVHuQn/KtYNWCZ78XKK8wtVnflmfqE/c9xO1n/EcVvLCdg==
-----END CERTIFICATE-----

我知道 BouncyCastle 能够解析它,但我想避免使用额外的库。所以我想知道是否可以仅使用普通 jdk 或其他一些轻量级库。

我已经能够使用以下页眉/页脚解析私钥:

-----BEGIN PRIVATE KEY-----
           *
-----END PRIVATE KEY-----

-----BEGIN RSA PRIVATE KEY-----
           *
-----END RSA PRIVATE KEY-----

我已经使用以下 sn-p 来完成此操作:

import com.hierynomus.asn1.ASN1InputStream;
import com.hierynomus.asn1.encodingrules.der.DERDecoder;
import com.hierynomus.asn1.types.ASN1Object;
import com.hierynomus.asn1.types.constructed.ASN1Sequence;
import com.hierynomus.asn1.types.primitive.ASN1Integer;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

class App {

    private static final String KEYSTORE_TYPE = "PKCS12";
    private static final String KEY_FACTORY_ALGORITHM = "RSA";
    private static final String CERTIFICATE_TYPE = "X.509";

    private static final Pattern CERTIFICATE_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", Pattern.DOTALL);
    private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN PRIVATE KEY-----(.*?)-----END PRIVATE KEY-----", Pattern.DOTALL);
    private static final Pattern ENCRYPTED_PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL);
    private static final Pattern RSA_PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN RSA PRIVATE KEY-----(.*?)-----END RSA PRIVATE KEY-----", Pattern.DOTALL);

    private static final String NEW_LINE = "\n";
    private static final String EMPTY = "";

    public static void main(String[] args) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, InvalidKeySpecException {
        String privateKeyContent = "";
        String certificateContent = "";

        PrivateKey privateKey = parsePrivateKey(privateKeyContent);
        Certificate[] certificates = parseCertificate(certificateContent).values()
                .toArray(new Certificate[]{});

        KeyStore keyStore = createEmptyKeyStore();
        keyStore.setKeyEntry("client", privateKey, null, certificates);
    }

    private static KeyStore createEmptyKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
        keyStore.load(null, null);
        return keyStore;
    }

    private static PrivateKey parsePrivateKey(String identityContent) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
        KeySpec keySpec = null;

        Matcher privateKeyMatcher = PRIVATE_KEY_PATTERN.matcher(identityContent);
        if (privateKeyMatcher.find()) {
            String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
            keySpec = new PKCS8EncodedKeySpec(decodedPrivateKeyContent);
        }

        privateKeyMatcher = RSA_PRIVATE_KEY_PATTERN.matcher(identityContent);
        if (privateKeyMatcher.find()) {
            String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
            keySpec = getKeySpecFromAsn1StructuredData(decodedPrivateKeyContent);
        }

        privateKeyMatcher = ENCRYPTED_PRIVATE_KEY_PATTERN.matcher(identityContent);
        if (privateKeyMatcher.find()) {
            String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
            keySpec = null; //TODO
        }

        Objects.requireNonNull(keySpec);

        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        return keyFactory.generatePrivate(keySpec);
    }

    private static KeySpec getKeySpecFromAsn1StructuredData(byte[] decodedPrivateKeyContent) throws IOException {
        try(ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
            ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
            ASN1Sequence asn1Sequence = stream.readObject();

            if (asn1Sequence.getValue().size() < 9) {
                throw new RuntimeException("Parsed key content doesn't have the minimum required sequence size of 9");
            }

            BigInteger modulus = extractIntValueFrom(asn1Sequence.get(1));
            BigInteger publicExponent = extractIntValueFrom(asn1Sequence.get(2));
            BigInteger privateExponent = extractIntValueFrom(asn1Sequence.get(3));
            BigInteger primeP = extractIntValueFrom(asn1Sequence.get(4));
            BigInteger primeQ = extractIntValueFrom(asn1Sequence.get(5));
            BigInteger primeExponentP = extractIntValueFrom(asn1Sequence.get(6));
            BigInteger primeExponentQ = extractIntValueFrom(asn1Sequence.get(7));
            BigInteger crtCoefficient = extractIntValueFrom(asn1Sequence.get(8));

            return new RSAPrivateCrtKeySpec(
                    modulus, publicExponent, privateExponent,
                    primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient
            );
        }
    }

    private static BigInteger extractIntValueFrom(ASN1Object<?> asn1Object) {
        if (asn1Object instanceof ASN1Integer) {
            return ((ASN1Integer) asn1Object).getValue();
        } else {
            throw new RuntimeException(String.format(
                    "Unable to parse the provided value of the object type [%s]. The type should be an instance of [%s]",
                    asn1Object.getClass().getName(), ASN1Integer.class.getName())
            );
        }
    }

    private static Map<String, Certificate> parseCertificate(String certificateContent) throws IOException, CertificateException {
        Map<String, Certificate> certificates = new HashMap<>();
        Matcher certificateMatcher = CERTIFICATE_PATTERN.matcher(certificateContent);

        while (certificateMatcher.find()) {
            String sanitizedCertificate = certificateMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedCertificate = Base64.getDecoder().decode(sanitizedCertificate);
            try(ByteArrayInputStream certificateAsInputStream = new ByteArrayInputStream(decodedCertificate)) {
                CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
                Certificate certificate = certificateFactory.generateCertificate(certificateAsInputStream);
                certificates.put(UUID.randomUUID().toString(), certificate);
            }
        }

        return certificates;
    }

}

我还需要处理 asn.1 编码的私钥。我可以使用 BouncyCastle,但我又想避免它,因为它很重。我从这个主题中得到了启发:https://stackoverflow.com/a/42733858/6777695 但从 jdk 11 开始,DerInputStreamDerValue 就不再可用了。并且应该避免使用sun包......所以我发现asn-one java library可以解析asn.1编码的私钥,并且效果很好。

我已经分享了我处理其他案件的完整经验,所以当有人想帮助我时,尝试一下会更容易。

从以下文章:ASN.1 key structures in DER and PEM 我了解到具有以下页眉/页脚的私钥:-----BEGIN ENCRYPTED PRIVATE KEY----- * -----END ENCRYPTED PRIVATE KEY----- 也是一个 asn.1 编码的私钥,具有以下结构:

EncryptedPrivateKeyInfo ::= SEQUENCE {
  encryptionAlgorithm  EncryptionAlgorithmIdentifier,
  encryptedData        EncryptedData
}

EncryptionAlgorithmIdentifier ::= AlgorithmIdentifier

EncryptedData ::= OCTET STRING

我也尝试了How to read a password encrypted key with java? 此处提供的答案,但这些也没有奏效。但是我不太确定如何将其正确解析为 java 对象并将其作为 KeySpec 加载。所以欢迎任何帮助!

======> 根据dave_thompson_085的输入,于2020年9月12日更新进度

Matcher privateKeyMatcher = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL)
        .matcher(identityContent);

if (privateKeyMatcher.find()) {
    String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
    byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
    try (ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
        ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
        ASN1Sequence asn1Sequence = stream.readObject();

        ASN1OctetString privateKeyAsOctetString = (ASN1OctetString) asn1Sequence.get(1);
        ASN1OctetString saltAsOctetString = (ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(0);
        ASN1OctetString initializationVectorAsOctec = ((ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(1)).get(1));
        ASN1Integer iterationCount = (ASN1Integer) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(1);

        IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVectorAsOctec.getValue());

        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1");
        EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(secretKeyFactory.getAlgorithm(), privateKeyAsOctetString.getValue());

        int keyLength = encryptedPrivateKeyInfo.getEncoded().length;
        PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword, saltAsOctetString.getValue(), iterationCount.getValue().intValue(), keyLength);

        Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding/");

        SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec);

        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
        PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
    }
}

导致以下异常:java.security.InvalidKeyException: Wrong algorithm: DESede or TripleDES required 上声明cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);

======> 根据Michael Fehr的输入,于2020年9月13日更新进度

Matcher privateKeyMatcher = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL)
        .matcher(identityContent);

if (privateKeyMatcher.find()) {
    String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
    byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
    try (ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
        ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
        ASN1Sequence asn1Sequence = stream.readObject();

        ASN1OctetString privateKeyAsOctetString = (ASN1OctetString) asn1Sequence.get(1);
        ASN1OctetString saltAsOctetString = (ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(0);
        ASN1OctetString initializationVectorAsOctec = ((ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(1)).get(1));
        ASN1Integer iterationCount = (ASN1Integer) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(1);

        IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVectorAsOctec.getValue());

        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1");
        EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(secretKeyFactory.getAlgorithm(), privateKeyAsOctetString.getValue());

        int keyLength = 24 * 8;
        PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword, saltAsOctetString.getValue(), iterationCount.getValue().intValue(), keyLength);

        Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding/");

        SecretKey secretKeyPbkdf = secretKeyFactory.generateSecret(pbeKeySpec);
        SecretKey secretKey = new SecretKeySpec(secretKeyPbkdf.getEncoded(), "DESede");

        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
        PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
    }
}

【问题讨论】:

  • 试试这个out
  • 您在使用本机 Java 类时的主要问题将是以下错误:“PBE 参数解析错误:期望 AES 密码的对象标识符”,因为您的加密密钥是使用 PBKDF + TripleDES 加密的(请参阅标识符:现代Java版本(DES部分)不(更长?)支持的对象标识符des-EDE3-CBC(1 2 840 113549 3 7))。我相信你必须使用 Bouncy Castle 阅读这种钥匙。
  • @MichaelFehr:我不知道你在看哪里,但是 TripleDES/TDEA 仍然广泛用于 PKCS8(还有 PKCS12,但通常在 PKCS12PBE 而不是 PBES2 中),对于 short 数据,如密钥文件,由 Java 加密支持,算法名称为 DESede。虽然 PKCS8+PKCS5 格式 允许 AES,但我不知道目前有人在使用它。 JCE 甚至支持单一/原始 DES,尽管它不应再使用,并且在 JSSE for SSL/TLS 中默认禁用。
  • @dave_thompson_085:我尝试使用以下代码解析 PEM(剥离页眉和页脚行后):“EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(Base64.getMimeDecoder().decode(encrypted)); "并收到异常“java.io.IOException:PBE 参数解析错误:期望 AES 密码的对象标识符”。因此,对于 EncryptedPrivateKeyInfo 似乎没有可用的 TripleDES/TDEA 密码,尽管在“独立”使用它时它就在那里。正如您在回答中所写,我们必须自己进行密钥推导和解密。感谢您的许可。
  • @MichaelFehr:啊! That's a known problem but it changed. 以前不处理any PBES2 参数;我没有注意到在 11.0.1 中他们将其修复为仅适用于 AES-128 和 AES-256(可能并非巧合,JCE 直接实施的唯一 PBES2 方案)所以我没有认识到新症状。但是我看到他们现在计划为明年到期的 j16 修复它。耶!

标签: java ssl encryption keystore pem


【解决方案1】:

此 PEM 格式的更官方参考是 rfc7468 section 11,它指定 ASN.1 内容(de-PEM 之后)是在 rfc5208 section 6appendix A 中定义的 PKCS8 加密形式,将其稍微修改为:

EncryptedPrivateKeyInfo ::= SEQUENCE {
    encryptionAlgorithm AlgorithmIdentifier {{KeyEncryptionAlgorithms}},
    encryptedData EncryptedData
}

EncryptedData ::= OCTET STRING

(redacted)

KeyEncryptionAlgorithms ALGORITHM-IDENTIFIER ::= {
     ... -- For local profiles
}

未能解释用于 PKCS8(PKCS12 除外)的密钥加密算法通常是在 PKCS5 v2 rfc2898 或现在 v2.1 rfc8018 中定义的基于密码的加密(微小的差异在这里无关紧要)。特别是对于 Q 中的密钥文件,AlgorithmIdentifier 将(使用 OpenSSL)解析为:

    4:d=1  hl=2 l=  64 cons:  SEQUENCE
    6:d=2  hl=2 l=   9 prim:   OBJECT            :PBES2
   17:d=2  hl=2 l=  51 cons:   SEQUENCE
   19:d=3  hl=2 l=  27 cons:    SEQUENCE
   21:d=4  hl=2 l=   9 prim:     OBJECT            :PBKDF2
   32:d=4  hl=2 l=  14 cons:     SEQUENCE
   34:d=5  hl=2 l=   8 prim:      OCTET STRING      [HEX DUMP]:F7F1520689D469C6
   44:d=5  hl=2 l=   2 prim:      INTEGER           :0800
   48:d=3  hl=2 l=  20 cons:    SEQUENCE
   50:d=4  hl=2 l=   8 prim:     OBJECT            :des-ede3-cbc
   60:d=4  hl=2 l=   8 prim:     OCTET STRING      [HEX DUMP]:9D1A44BCC1884EA7

PBES2 在section 6.2 中定义,由 PBKDF2 密钥派生和几乎任何对称加密/解密组成。 您应该能够看到顶级参数与appendix A.4 中的PBES2-Params 匹配,并且keyDerivationFunc 部分中的参数与appendix A.2 中的PBKDF2-Params 匹配(存在salt 和iterationCount,省略了keyLength 和prf),而参数对于 encryptionScheme 部分只是一个包含初始化向量 (IV) 的 OCTET STRING,请参阅appendix B.2.2

因此要解密这个,您需要使用这些参数实现section 5.2 中指定的 PBKDF2 密钥派生,Java 加密支持 SecretKeyFactory.getInstance("PBKDF2withHmacSHA1")PBEKeySpec 然后是三重 DES TDEA 解密在 CBC 模式下使用三重长度密钥,也就是密钥选项 1,带有 PKCS5/7 填充,Java 加密支持 Cipher.getInstance("DESede/CBC/PKCS5Padding"),使用从第一步派生的密钥和来自输入数据的 IV 作为 IVParameterSpec。结果将是一个 PKCS8-unencrypted 密钥结构,您可以通过合适的 KeyFactory 作为PKCS8EncodedKeySpec 运行它,就像您已经为未加密的输入格式所做的那样。

虽然考虑到 JRE 通常一开始就超过 100MB,而 Bouncy 增加了大约 5MB 并且更易于使用,但我不能说我同意您的评估。


更新:我没有意识到,但 JCE 的 PBKDF2 SKF 实际上并没有进行密钥派生,它只是打包数据元素以供合适的Cipher 算法使用——它没有实现Cipher 算法用于我们需要的情况。因此,您需要手动编码 PBKDF2,幸运的是(?)我已经为 Decrypt file encrypted using openssl with aes-cbc-256 做过。将它们放在一起,这是经过测试的代码:

static void SO63832456ManualPKCS8EPBES2 (String[] args) throws Exception {
    String file = args[0], pass = args[1];
    // I use Bouncy to read because it's much easier for me, 
    // but you should get the same results with any ASN.1 lib
    PemObject obj; try(Reader rdr = new FileReader(file);
            PEMParser p = new PEMParser(rdr) ){ obj = p.readPemObject(); }
    if( ! obj.getType().equals("ENCRYPTED PRIVATE KEY") ) throw new Exception ("wrong type");
    org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo eki = org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo.getInstance(obj.getContent());
    AlgorithmIdentifier algid = eki.getEncryptionAlgorithm();
    if( ! algid.getAlgorithm().equals(PKCSObjectIdentifiers.id_PBES2) ) throw new Exception("not PBES2");
    PBES2Parameters top = PBES2Parameters.getInstance(algid.getParameters());
    KeyDerivationFunc kdf = top.getKeyDerivationFunc();
    if( ! kdf.getAlgorithm().equals(PKCSObjectIdentifiers.id_PBKDF2) ) throw new Exception("not PBKDF2");
    PBKDF2Params kdp = PBKDF2Params.getInstance(kdf.getParameters());
    EncryptionScheme esa = top.getEncryptionScheme();
    if( ! esa.getAlgorithm().equals(PKCSObjectIdentifiers.des_EDE3_CBC) ) throw new Exception("not DES3");
    ASN1OctetString esp = ASN1OctetString.getInstance(esa.getParameters());
    // these are the (only) items actually used
    byte[] salt = kdp.getSalt(), iv = esp.getOctets(), edata = eki.getEncryptedData(); 
    int iter = kdp.getIterationCount().intValueExact();
    
    //SecretKeyFactory fact = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1");
    //SecretKey skey = fact.generateSecret(new PBEKeySpec(pass.toCharArray(), salt, iter, 24));
    byte[] skey = PBKDF2 ("HmacSHA1", pass.getBytes("UTF-8"), salt, iter, 24);
    Cipher ciph = Cipher.getInstance("DESEDE/CBC/PKCS5Padding"); 
    ciph.init(Cipher.DECRYPT_MODE, new SecretKeySpec(skey,"DESEDE"), new IvParameterSpec(iv));
    byte[] plain = ciph.doFinal(edata);
    
    KeyFactory fac2 = KeyFactory.getInstance("RSA");
    PrivateKey pkey = fac2.generatePrivate(new PKCS8EncodedKeySpec(plain));
    System.out.println ( ((java.security.interfaces.RSAPrivateKey)pkey).getModulus() );
}

public static byte[] PBKDF2 (String prf, byte[] pass, byte[] salt, int iter, int len)
        throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeyException {
    byte[] result = new byte[len];
    Mac mac = Mac.getInstance(prf);
    mac.init (new SecretKeySpec (pass,prf));
    byte[] saltcnt = Arrays.copyOf (salt, salt.length+4);
    while( /*remaining*/len>0 ){
        for( int o = saltcnt.length; --o>=saltcnt.length-4; ) if( ++saltcnt[o] != 0 ) break; 
        byte[] u = saltcnt, x = new byte[mac.getMacLength()];
        for( int i = 1; i <= iter; i++ ){
            u = mac.doFinal (u); 
            for( int o = 0; o < x.length; o++ ) x[o] ^= u[o];
        }
        int len2 = Math.min (len, x.length);
        System.arraycopy (x,0, result,result.length-len, len2);
        len -= len2;
    }
    return result;
}

-->
18998199686208613730227071552042680566660281525162377778227794768879762338654464000476266705180956908841709177555966832423085875590556580445428066261332223955361646420713402269532603328557674453787190302285379571808527818182992417461094551685532849252957724361115461909983939654809533488667032982867230996411860328826589806804115764031480737011900431983025490936659008858316889367140757195359233313370592669605576636360123189540118611655293825465309125578750878878980002346272931570343463549347548711194000065166346537274382220211242975801253306067862461345994215674963367480607539333468674983712264803816027753559737

呸!

【讨论】:

  • 感谢您通过参考 tools.ietf.org 提供如此详细的答案和额外的上下文。这真的很有帮助,现在我明白这些东西到底有多复杂了。关于 BouncyCastle 库,您是对的,它并没有那么重。我实际上想在没有任何额外依赖的情况下实现它,所以这是我尝试不使用 BC 的主要原因。根据您提供的答案,我尝试实现 java 代码,不幸的是我遇到了异常。我在最初的问题上发布了我的进展,但是我不确定我是否正确应用了您的建议。可以看看吗?
  • 更新后的答案看起来很神奇,对我来说很神奇。我刚试了一下,效果很好!很棒的戴夫,非常感谢。我现在明白它有多复杂以及它有多容易出错。我认为我只去充气城堡会更安全
【解决方案2】:

您编辑的代码给出了一个错误“java.security.InvalidKeyException: Wrong key size”,这是因为您使用这一行“读取”了 keyLength:

int keyLength = encryptedPrivateKeyInfo.getEncoded().length;

使用给定的私钥,长度将为 1247 位,这不是允许的 TripleDES 长度。您可以将其修复为(硬编码):

int keyLength = 24 * 8;

现在我们得到一个 192 位 = 24 字节的密钥长度,这对于 TripleDES 密钥来说没问题,但我们收到一个新错误:

java.security.InvalidKeyException: Wrong algorithm: DESede or TripleDES required

此行为的原因是以下行(已注释掉),可以通过另外 2 行来修复:

// SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec); // ### old routine
SecretKey secretKeyPbkdf = secretKeyFactory.generateSecret(pbeKeySpec); // ### new
SecretKey secretKey = new SecretKeySpec(secretKeyPbkdf.getEncoded(), "DESede"); // ### new

现在您的代码已成功运行并打印出私钥和证书:

original keyLength:  1247
fixed keyLength(bit): 192

privateKey:
SunRsaSign RSA private CRT key, 2048 bits
  params: null
  modulus: 18998199686208613730227071552042680566660281525162377778227794768879762338654464000476266705180956908841709177555966832423085875590556580445428066261332223955361646420713402269532603328557674453787190302285379571808527818182992417461094551685532849252957724361115461909983939654809533488667032982867230996411860328826589806804115764031480737011900431983025490936659008858316889367140757195359233313370592669605576636360123189540118611655293825465309125578750878878980002346272931570343463549347548711194000065166346537274382220211242975801253306067862461345994215674963367480607539333468674983712264803816027753559737
  private exponent: 2470690692670644289832084636740463653655882622624760758103597888447170520657826825702415751838767348735186037205082706146786714644526202528897926495648786865000645626802269367582073915029633022103975204027927584273373553784482725392082470421529769049284506228997163157587212073758970565083984956787424047216319298607371397767542633302071090323391531162434678662485530085857362259661656308472034152915312425224731842425134593468185574918070451135641322780271191791839285884885643517240131520070881951542294552185519522325857178404160441369354693465035929601010771762928436963178293113507661955562266846499005032897533

number of certificates: 1

certificate: 0 data:
[
[
  Version: V3
  Subject: CN=Hakan, OU=Amsterdam, O=Thunderberry, C=NL
  Signature Algorithm: SHA256withRSA, OID = 1.2.840.113549.1.1.11

  Key:  Sun RSA public key, 2048 bits
  params: null
  modulus: 18998199686208613730227071552042680566660281525162377778227794768879762338654464000476266705180956908841709177555966832423085875590556580445428066261332223955361646420713402269532603328557674453787190302285379571808527818182992417461094551685532849252957724361115461909983939654809533488667032982867230996411860328826589806804115764031480737011900431983025490936659008858316889367140757195359233313370592669605576636360123189540118611655293825465309125578750878878980002346272931570343463549347548711194000065166346537274382220211242975801253306067862461345994215674963367480607539333468674983712264803816027753559737
  public exponent: 65537
  Validity: [From: Mon Aug 31 10:07:39 CEST 2020,
               To: Thu Aug 29 10:07:39 CEST 2030]
  Issuer: CN=Hakan, OU=Amsterdam, O=Thunderberry, C=NL
  SerialNumber: [    5671c8dd]

Certificate Extensions: 2
[1]: ObjectId: 2.5.29.17 Criticality=true
SubjectAlternativeName [
  DNSName: localhost
  IPAddress: 127.0.0.1
]

[2]: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 78 53 EC 04 E0 A1 55 45   22 C8 25 C4 3F F5 89 40  xS....UE".%.?..@
0010: 51 07 A8 03                                        Q...
]
]

]
  Algorithm: [SHA256withRSA]
  Signature:
0000: 85 55 87 FC 28 19 D1 93   66 4D F8 85 00 E9 D6 75  .U..(...fM.....u
0010: A6 55 69 AE EF 00 53 D3   D9 86 D7 BA F9 58 60 84  .Ui...S......X`.
0020: 65 AE ED 01 FF 39 F6 31   FC 26 9F 95 B6 A3 50 AA  e....9.1.&....P.
0030: A0 1B A1 C9 F1 2B 92 41   78 69 13 35 BD DB 88 8A  .....+.Axi.5....
0040: B5 23 95 C1 F8 F9 C1 57   AD 5D A0 B6 59 DF 79 64  .#.....W.]..Y.yd
0050: E0 FE C2 49 99 8C E4 25   31 9E 38 8A 91 94 1D 47  ...I...%1.8....G
0060: 28 2A A8 78 D0 AB CA 49   1C B2 29 04 09 0D 67 0A  (*.x...I..)...g.
0070: AA F5 A8 B8 7D 0D 7B 14   81 65 56 E7 B6 16 AA F3  .........eV.....
0080: F5 3F CC 4E 3B 6F C9 F6   C8 4C 07 A9 4E 85 FB 69  .?.N;o...L..N..i
0090: 6B 0A EA 67 68 4D 1B C7   EA 30 A8 D5 56 E5 29 14  k..ghM...0..V.).
00A0: 13 51 F9 63 83 43 FA D1   46 DA 9A EA E5 36 57 C6  .Q.c.C..F....6W.
00B0: 8E 39 9C 75 40 95 24 4D   D3 7B E9 83 82 B8 61 13  .9.u@.$M......a.
00C0: ED E7 5E BE 3A 5D 6D 15   DD 7B E1 D0 37 A9 9B 4F  ..^.:]m.....7..O
00D0: D3 8B C5 26 47 3B 46 90   AC 3C 83 98 D8 69 69 51  ...&G;F..<...iiQ
00E0: EE 42 7F CA B5 83 56 09   9E FC 5C A2 BC C2 D5 67  .B....V...\....g
00F0: 7E 59 9F A8 4F DC F7 13   B5 9F F1 1C 56 F2 C2 76  .Y..O.......V..v

]

这是在 Open Java 11 上运行的完整代码:

import com.hierynomus.asn1.ASN1InputStream;
import com.hierynomus.asn1.encodingrules.der.DERDecoder;
import com.hierynomus.asn1.types.ASN1Object;
import com.hierynomus.asn1.types.constructed.ASN1Sequence;
import com.hierynomus.asn1.types.primitive.ASN1Integer;
import com.hierynomus.asn1.types.string.ASN1OctetString;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
// https://mvnrepository.com/artifact/com.hierynomus/asn-one/0.4.0
// additionally you need https://mvnrepository.com/artifact/org.slf4j/slf4j-api/1.7.30
// and https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12/1.7.30
// https://mvnrepository.com/artifact/org.eclipse.equinox/org.apache.log4j/1.2.13.v200706111418

class MainOrg3 {

    private static final String KEYSTORE_TYPE = "PKCS12";
    private static final String KEY_FACTORY_ALGORITHM = "RSA";
    private static final String CERTIFICATE_TYPE = "X.509";

    private static final Pattern CERTIFICATE_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", Pattern.DOTALL);
    private static final Pattern PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN PRIVATE KEY-----(.*?)-----END PRIVATE KEY-----", Pattern.DOTALL);
    private static final Pattern ENCRYPTED_PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN ENCRYPTED PRIVATE KEY-----(.*?)-----END ENCRYPTED PRIVATE KEY-----", Pattern.DOTALL);
    private static final Pattern RSA_PRIVATE_KEY_PATTERN = Pattern.compile("-----BEGIN RSA PRIVATE KEY-----(.*?)-----END RSA PRIVATE KEY-----", Pattern.DOTALL);

    private static final String NEW_LINE = "\n";
    private static final String EMPTY = "";

    static char[] keyPassword = "secret".toCharArray();

    public static void main(String[] args) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, InvalidKeySpecException {
        String privateKeyContent = "-----BEGIN ENCRYPTED PRIVATE KEY-----\n" +
                "MIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQI9/FSBonUacYCAggA\n" +
                "MBQGCCqGSIb3DQMHBAidGkS8wYhOpwSCBMi8JaSYOKudMNNFRpzL7QMIZgFtzDay\n" +
                "MmOroy3lW34dOa7dusqDl4d2gklKcHCpbEaTYxm5aQJ1LuiOdGtFy7HwxOvKU5xz\n" +
                "4qsJoeBIpE0eCTKjQW7/I38DzLXx2wUURqhMWOtFsWZEyR5Dqok3N9mIKKKBXAFG\n" +
                "AwNjlTRW2LyPSttiIUGN01lthjifMWoLTWB1aSGOmGeJRBdSZeqZ15xKneR4H5ja\n" +
                "yE88YcpOHCDKMIxi6ZVoKs7jDQhu8bBKqS8NsYyh1AlP9QkvWNal36jWSzhqYNzk\n" +
                "NRWUOZngfkdbMALVfRtbrC215jHGWVwosPIIs8rkoarRv8s6QWS1Rg3YfQ3qgcRf\n" +
                "s7hkDFKJf3TUXr+askfamV5hc300ZG64+ldX1YxWXY8Vd/wIvHAc/YE/lTyCgYrY\n" +
                "19Am6MNBfp8/kXvzKj+PizB8oNDO4S8sSShEEzOQ5a/+MTC6bqB0DLWYGUqRbjLc\n" +
                "PyYTC2C4i9Agx/GeGVE3c1UdtXiwwnt2XUn7Y1YGqABk0xGIY4J1NFTbSOxKl9hO\n" +
                "arwopAFrZU5nsjjFzv1DJvhfQWnYX18kPSKNHDlia019M118qZ8ERwD9tH8ix9Fa\n" +
                "R2tQdxn1aRGmvXSw+zFkbWD8aWs9n/B+QN1yllJqVoWypOld1yj+fVYYnYOtV1gK\n" +
                "eiygrtrh3JJCvLbEQl4nOgJM3PlEtfBHSaunehIXQMD1z/NDUqgBYjuDPyqRxJeH\n" +
                "Va5k72Nds5PeySKJJnICB3nZKjqgfLhNUrXa1SAQ4vqr0Ik/Lu9P7T+B1XiYwuUT\n" +
                "a20+bxi/x89ZZqwp3jnDuHup7XcO1MtqsoOKP/JgkjVMesb8Q1W8i2dXzg+l4gkk\n" +
                "l1ipreEGtT1YfFTq0DFelz6CjZFLDlGGeGWob94sW94DWTW0nsLPhQWEnwW1CcyJ\n" +
                "oJbJdDEgdiIbRJoABDkTuVXLwTlgzHSHh6zeJvNvcojI7UI3nWYCVYvD3kwghXiP\n" +
                "67sKGL3ug7PFDqLia46AudGY7CFh4+wpxyH+fidLC3FMdkDBA6xR6mGgEjRLXR9M\n" +
                "TnJ/eSYP7eqYZeKn9EarcI7v1zM2IG0/PDQCetiI0ABiHpdKyRQuuiEavp3xC5Vi\n" +
                "h7UmJNYt8Zsz3rwqAQ4FR2+Su5R34OOdRmxTaYLe96PXTpLcLef5TkYixSY7Tzgd\n" +
                "PMyRxRPrywklUEFe4KK/KOcdolxybfsIsxQnupLAMEsO7/Cs7mouNHISK51haDRc\n" +
                "vNbKQ5E4xOq1U4ThW5dHR29cGZillfmMzj05ZQh3ZX2TQJP45ahFET3v9kInWCwQ\n" +
                "8atqclVPOSnASsJZ0PxjYgKZuY8QWYM6zpfWyWnfu/CHhWbRS/qX8T1ow2SMyPBL\n" +
                "CQbZ+MhcdP0IrjoXhDFQsns16i/BPK5TTVqtEC2ywDf5P4/BOEZkySG9YNOd6THp\n" +
                "VA/dVPafzmLy3ltqH+jG8ZH2+RtWx7kwBjiDWs5cF33BFrPS7AZlzMzZoCHLXD/r\n" +
                "T/SmisybUKHMqri0x0RHeIByW0hogSByWiyIn8POabDzJV6Df9nQPziDGcSsvWfG\n" +
                "7q+hizh6+nnXOY+GZx3ptwg9mA9R4QyCiFNQradOaXSPxyEL2IC77/srFfVEIaU4\n" +
                "SRo=\n" +
                "-----END ENCRYPTED PRIVATE KEY-----";

        String certificateContent = "-----BEGIN CERTIFICATE-----\n" +
                "MIIDTjCCAjagAwIBAgIEVnHI3TANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJO\n" +
                "TDEVMBMGA1UEChMMVGh1bmRlcmJlcnJ5MRIwEAYDVQQLEwlBbXN0ZXJkYW0xDjAM\n" +
                "BgNVBAMTBUhha2FuMB4XDTIwMDgzMTA4MDczOVoXDTMwMDgyOTA4MDczOVowSDEL\n" +
                "MAkGA1UEBhMCTkwxFTATBgNVBAoTDFRodW5kZXJiZXJyeTESMBAGA1UECxMJQW1z\n" +
                "dGVyZGFtMQ4wDAYDVQQDEwVIYWthbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\n" +
                "AQoCggEBAJZ+pqirEqEk4k1ow8vryld79oCO4P/1y+v60CkLpS2MpeHE3BogTr7g\n" +
                "WWP5HdHBMU5l8yT5tXuyVZZqgyzo0q2Sdm7GGrNunHf6Z8vPQFu69sQC1utDM9u8\n" +
                "ZFKiKsTNJ+5QS6KsOtlACyhaLoaPAWVjtvueMjVwmM9hfv/Gq6VyyuBm2x1C4HTj\n" +
                "zPCLHE2G1D13EJaWsvyZLGSbl0GGXZGPhaDd/vnw5TW36mvNTWW+37ZIEk4kXANa\n" +
                "FUNsJemm8HjB/PzHs3/SXGxuD8NKobg3+cNXYwAz2s2DI0W6Xw2g5bbrMQAdBRvn\n" +
                "9/kNftMymDORw3RGwDM2ld4zQfIkNrkCAwEAAaNAMD4wHQYDVR0OBBYEFHhT7ATg\n" +
                "oVVFIsglxD/1iUBRB6gDMB0GA1UdEQEB/wQTMBGCCWxvY2FsaG9zdIcEfwAAATAN\n" +
                "BgkqhkiG9w0BAQsFAAOCAQEAhVWH/CgZ0ZNmTfiFAOnWdaZVaa7vAFPT2YbXuvlY\n" +
                "YIRlru0B/zn2Mfwmn5W2o1CqoBuhyfErkkF4aRM1vduIirUjlcH4+cFXrV2gtlnf\n" +
                "eWTg/sJJmYzkJTGeOIqRlB1HKCqoeNCrykkcsikECQ1nCqr1qLh9DXsUgWVW57YW\n" +
                "qvP1P8xOO2/J9shMB6lOhftpawrqZ2hNG8fqMKjVVuUpFBNR+WODQ/rRRtqa6uU2\n" +
                "V8aOOZx1QJUkTdN76YOCuGET7edevjpdbRXde+HQN6mbT9OLxSZHO0aQrDyDmNhp\n" +
                "aVHuQn/KtYNWCZ78XKK8wtVnflmfqE/c9xO1n/EcVvLCdg==\n" +
                "-----END CERTIFICATE-----";

        PrivateKey privateKey = parsePrivateKey(privateKeyContent);
        Certificate[] certificates = parseCertificate(certificateContent).values()
                .toArray(new Certificate[]{});

        KeyStore keyStore = createEmptyKeyStore();
        keyStore.setKeyEntry("client", privateKey, null, certificates);
        System.out.println("\nprivateKey:\n" + privateKey);
        int certificatesLength = certificates.length;
        System.out.println("\nnumber of certificates: " + certificatesLength);
        for (int i = 0; i < certificatesLength; i++) {
            System.out.println("\ncertificate: " + i + " data:\n" + certificates[i]);
        }
    }

    private static KeyStore createEmptyKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
        KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
        keyStore.load(null, null);
        return keyStore;
    }

    private static PrivateKey parsePrivateKey(String identityContent) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
        KeySpec keySpec = null;

        Matcher privateKeyMatcher = PRIVATE_KEY_PATTERN.matcher(identityContent);
        if (privateKeyMatcher.find()) {
            String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
            keySpec = new PKCS8EncodedKeySpec(decodedPrivateKeyContent);
        }

        privateKeyMatcher = RSA_PRIVATE_KEY_PATTERN.matcher(identityContent);
        if (privateKeyMatcher.find()) {
            String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
            keySpec = getKeySpecFromAsn1StructuredData(decodedPrivateKeyContent);
        }

        privateKeyMatcher = ENCRYPTED_PRIVATE_KEY_PATTERN.matcher(identityContent);
        if (privateKeyMatcher.find()) {
            String privateKeyContent = privateKeyMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedPrivateKeyContent = Base64.getDecoder().decode(privateKeyContent);
            try (ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
                ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
                ASN1Sequence asn1Sequence = stream.readObject();
                ASN1OctetString privateKeyAsOctetString = (ASN1OctetString) asn1Sequence.get(1);
                ASN1OctetString saltAsOctetString = (ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(0);
                ASN1OctetString initializationVectorAsOctec = ((ASN1OctetString) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(1)).get(1));
                ASN1Integer iterationCount = (ASN1Integer) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) ((ASN1Sequence) asn1Sequence.get(0)).get(1)).get(0)).get(1)).get(1);
                IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVectorAsOctec.getValue());
                SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1");
                EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(secretKeyFactory.getAlgorithm(), privateKeyAsOctetString.getValue());
                int keyLength = encryptedPrivateKeyInfo.getEncoded().length;
                System.out.println("original keyLength:  " + keyLength);
                keyLength = 24 * 8; // ### fixed
                System.out.println("fixed keyLength(bit): " + keyLength);
                PBEKeySpec pbeKeySpec = new PBEKeySpec(keyPassword, saltAsOctetString.getValue(), iterationCount.getValue().intValue(), keyLength);
                Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding/");

                // SecretKey secretKey = secretKeyFactory.generateSecret(pbeKeySpec); // ### old routine
                SecretKey secretKeyPbkdf = secretKeyFactory.generateSecret(pbeKeySpec); // ### new
                SecretKey secretKey = new SecretKeySpec(secretKeyPbkdf.getEncoded(), "DESede"); // ### new

                cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
                keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
                //PKCS8EncodedKeySpec keySpec = encryptedPrivateKeyInfo.getKeySpec(cipher);
            } catch (NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
                e.printStackTrace();
            }
        }

        Objects.requireNonNull(keySpec);

        KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
        return keyFactory.generatePrivate(keySpec);
    }

    private static KeySpec getKeySpecFromAsn1StructuredData(byte[] decodedPrivateKeyContent) throws IOException {
        try(ByteArrayInputStream privateKeyAsInputStream = new ByteArrayInputStream(decodedPrivateKeyContent)) {
            ASN1InputStream stream = new ASN1InputStream(new DERDecoder(), privateKeyAsInputStream);
            ASN1Sequence asn1Sequence = stream.readObject();

            if (asn1Sequence.getValue().size() < 9) {
                throw new RuntimeException("Parsed key content doesn't have the minimum required sequence size of 9");
            }

            BigInteger modulus = extractIntValueFrom(asn1Sequence.get(1));
            BigInteger publicExponent = extractIntValueFrom(asn1Sequence.get(2));
            BigInteger privateExponent = extractIntValueFrom(asn1Sequence.get(3));
            BigInteger primeP = extractIntValueFrom(asn1Sequence.get(4));
            BigInteger primeQ = extractIntValueFrom(asn1Sequence.get(5));
            BigInteger primeExponentP = extractIntValueFrom(asn1Sequence.get(6));
            BigInteger primeExponentQ = extractIntValueFrom(asn1Sequence.get(7));
            BigInteger crtCoefficient = extractIntValueFrom(asn1Sequence.get(8));

            return new RSAPrivateCrtKeySpec(
                    modulus, publicExponent, privateExponent,
                    primeP, primeQ, primeExponentP, primeExponentQ, crtCoefficient
            );
        }
    }

    private static BigInteger extractIntValueFrom(ASN1Object<?> asn1Object) {
        if (asn1Object instanceof ASN1Integer) {
            return ((ASN1Integer) asn1Object).getValue();
        } else {
            throw new RuntimeException(String.format(
                    "Unable to parse the provided value of the object type [%s]. The type should be an instance of [%s]",
                    asn1Object.getClass().getName(), ASN1Integer.class.getName())
            );
        }
    }

    private static Map<String, Certificate> parseCertificate(String certificateContent) throws IOException, CertificateException {
        Map<String, Certificate> certificates = new HashMap<>();
        Matcher certificateMatcher = CERTIFICATE_PATTERN.matcher(certificateContent);

        while (certificateMatcher.find()) {
            String sanitizedCertificate = certificateMatcher.group(1).replace(NEW_LINE, EMPTY).trim();
            byte[] decodedCertificate = Base64.getDecoder().decode(sanitizedCertificate);
            try(ByteArrayInputStream certificateAsInputStream = new ByteArrayInputStream(decodedCertificate)) {
                CertificateFactory certificateFactory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
                Certificate certificate = certificateFactory.generateCertificate(certificateAsInputStream);
                certificates.put(UUID.randomUUID().toString(), certificate);
            }
        }
        return certificates;
    }
}

【讨论】:

  • 非常感谢您指出我的错误并提供正确的调整以使其正常工作!为什么密钥长度是固定的,尤其是那个大小,或者是默认的?还有趣的是,您需要从第一个密钥中创建第二个密钥
  • 为了获得使用密钥运行代码的平静,因为我们知道密钥加密是使用“旧”TripleDES 加密完成的(而不是 Java 可能支持的 AES 加密“out的盒子”)。 TripleDES 确实需要 3 个大小为“8”的密钥,因此我将 keyLength 固定为“24”——现在代码将无法使用需要 16/24/32 密钥长度的较新的 AES 加密运行。您不需要 2 个 SecretKey,我用它来说明嵌套结果时可能会消失。
【解决方案3】:

如果您不怕事先使用 openssl 转换给定的加密私钥,您可以使用示例代码轻松解析。

为了运行我的示例,我将您的加密私钥保存到文件“so_privatekey_secret.pem”中。

在 openssl 中使用此命令行,我将(已经)加密的密钥转换为另一个密码并生成(新的)密钥文件“so_privatekey_3des_secret.pem”:

openssl pkcs8 -v1 PBE-SHA1-3DES -topk8 -in so_privatekey_secret.pem -out so_privatekey_3des_secret.pem

您的加密私钥如下所示:

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIE6jAcBgoqhkiG9w0BDAEDMA4ECAdT0CV4sPOPAgIIAASCBMi3H3WjyWEiAS/G
gw6gZTHiZFmidehU8XPh1HPYVb385u36WO16CwZVIBperQoXptCaE2LRwxmrN7T/
K2l59EHlV8i8FAfGKqvc7b1DW1fqx7jvw+/iT//3uNc3GHUNXfBrwHDi/MTghII4
gEFNUm7ZO7rAqeR7pJnZxm3giCvRHFbFcZRkz6LhCif3eYF9Lx3gSe2x6SPeNG/T
p0vrierxhHyCJk+S543J3cZ4vHUOjYzOsT1xz9hTbxAxeAEPkaCpD0P9jyth8M8k
2SRedRvR1Ia6r7sAfFz6XoHz5jH/lhd47EYpOc/A7rsgWkFLObwdFwGLRIvQsgeN
VXnwg5UoxRdjE3ZXQMFgHgzc4igmoVCjwgNZW6nAMwlcTctjsQyvRnAEnQbqLUw1
kddkjDzC1LFk+NiZ7L9oKVWAQuAQnLHMOxr8kJrTlSoS8zt5x5OEnR4I7JnUEHqs
2RythkJnoAsXaslBIkIXw4Kg6wisZjr/fL7ER0D0TBdZFPMvdMw8p4TxNxEF7v/K
G97HmCe39cVl3DkYayK2Bs4T6FxqbqUzUMOQnNHpbj82+ybEhm/5duhD5Kd/Inz7
8mTpDM08P7OMG27AiJDReQJ18XQ5y3DxQJu8UgfQEtjpVW3BQIVzYRM4JqEEv72h
YLELhtmBQom1SWadMW6jzRJRS+f+ivEApVyLcKlnPj2SY7niSmDjYChDx30fHoDt
z25UYBb8LmZ6P5rwNpMgGutBYqg721ioYt86mQsDDaLYOxjlhPfwbrQ9HqUrPCyQ
0UBeP9hNYkGbZQ+erZmyJ4hhlGGXz9v1LZ4xqNelKuUUREZaLWNVO7UwX2AgUebv
V7V3ne3ZNuLo5kDQqGz3hxwI4tcHgneNMpbxwdR7/B9QTXtirgZck9QIU6zxPhIf
agyCfpqE5pE3ln5n+FfbOik1pNcQxFr0nLy6vnlcTATLHftX0JycFc9OcJ9eRnI/
zk0ekYcw7QLxzNp/ews+Krwy+2SXaacTi+fXihCks10FZ/TEMYV0wC7OMW1yC35K
jUU12u7kSccvUk4xQftyA39XYCN6g7kxDqLjCfc6pMMWZZ9anSo7rmuQ6scoMhdr
904PcX3ne9rBJLv3a3W25TolNKZx6JofCViyIipcJY4FRyIRoIfwUTZ643KLSZ4o
nAPxtWHpbgA8ucrMKwQ0kbhT/qEvz3HxliL91N7qqMfZ3RfsgK2fUXQZ4zsU1oZ2
f7UT9+D0w/Pusond7SHPFo9G68GjsE48CQ1J/l5QreZ+bLxBohs2xM1WIz2TBJX9
ghdbo+0eatoxfq0o/H0vj66xMd8fvqePrzNw3kSqSpdQiZl95vqWM8Y7xEW0PxAU
78I0koI98v/3n9abSUNDhodPsk8I3abr7MoQsKe7VSu/x+mgMcfvNOiH6j6LuUDx
Eii7nfHhC/M69hWoh8Y+a2qPdbT2WMZSgXdBC+dCQeL4o6+flht6/24Eg+HAQMyY
3X5aH2ZM4BJAyJvdtj7btMDKq6nunyMywYhIOgKTZUg8wytxDsi2F4KyNKpXXgsj
0yR2L6EUYeQ1ZLG/NcM86sHueF1x38RznpyD4rujRGslWAKRXk4D2O2GpYS4rYRk
4r4pPXltZ472q7PGBfM=
-----END ENCRYPTED PRIVATE KEY-----

运行我的示例程序(没有异常处理)会给出输出:

pkInfo.getAlgName: PBEWithSHA1AndDESede
decryptedPrivateKey: SunRsaSign RSA private CRT key, 2048 bits
  params: null
  modulus: 18998199686208613730227071552042680566660281525162377778227794768879762338654464000476266705180956908841709177555966832423085875590556580445428066261332223955361646420713402269532603328557674453787190302285379571808527818182992417461094551685532849252957724361115461909983939654809533488667032982867230996411860328826589806804115764031480737011900431983025490936659008858316889367140757195359233313370592669605576636360123189540118611655293825465309125578750878878980002346272931570343463549347548711194000065166346537274382220211242975801253306067862461345994215674963367480607539333468674983712264803816027753559737
  private exponent: 2470690692670644289832084636740463653655882622624760758103597888447170520657826825702415751838767348735186037205082706146786714644526202528897926495648786865000645626802269367582073915029633022103975204027927584273373553784482725392082470421529769049284506228997163157587212073758970565083984956787424047216319298607371397767542633302071090323391531162434678662485530085857362259661656308472034152915312425224731842425134593468185574918070451135641322780271191791839285884885643517240131520070881951542294552185519522325857178404160441369354693465035929601010771762928436963178293113507661955562266846499005032897533

代码:

import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class ReadPkcs8PrivateKeysSo {
    public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {

        String encrypted = new String(Files.readAllBytes(Paths.get("so_privatekey_3des_secret.pem")));
        String password = "secret";

        //Create object from encrypted private key
        encrypted = encrypted.replace("-----BEGIN ENCRYPTED PRIVATE KEY-----", "");
        encrypted = encrypted.replace("-----END ENCRYPTED PRIVATE KEY-----", "");
        EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(Base64.getMimeDecoder().decode(encrypted));
        PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); // password
        System.out.println("pkInfo.getAlgName: " + pkInfo.getAlgName());
        SecretKeyFactory pbeKeyFactory = SecretKeyFactory.getInstance(pkInfo.getAlgName());
        SecretKey secretKey = pbeKeyFactory.generateSecret(keySpec);
        PKCS8EncodedKeySpec encodedKeySpec = pkInfo.getKeySpec(secretKey);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey decryptedPrivateKey = keyFactory.generatePrivate(encodedKeySpec);
        System.out.println("decryptedPrivateKey: " + decryptedPrivateKey);
    }
}

【讨论】:

  • 嗨迈克尔,谢谢你的回答。通过这种方法,我确实可以创建私钥。这将是一个非常好的选择,但是我宁愿在将其提供给我的 java 代码之前不使用 openssl 解析它。知道如何在没有 openssl 代码的情况下让它工作吗?
  • 很抱歉,我尝试在没有 Bouncy Castle 的情况下读取加密的 RSA 私钥,但没有成功,抱歉。
【解决方案4】:

我试图理解在 @MichaelFehr 的(更长的)解析 PKCS#8 包(BEGIN PRIVATE KEY 和/或 BEGIN ENCRYPTED PRIVATE KEY)的答案中完成的所有 ASN.1 解析,而且似乎更多不必要的参与。要解包加密(并使用可用的元数据,而不是硬编码的算法名称等),解包代码如下:

    byte[] keydata = ...; // These are the raw bytes of the whole base64-decoded object

    if(isEncrypted) { // You decide how to determine this
        String password = ...; // You decide where this comes from

        EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(keydata);
        SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName());
        SecretKey secretKey = secretKeyFactory.generateSecret(new PBEKeySpec(password.toCharArray()));

        Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName());
        cipher.init(Cipher.DECRYPT_MODE, secretKey, encryptedPrivateKeyInfo.getAlgParameters());

        byte[] decryptedKey = cipher.doFinal(encryptedPrivateKeyInfo.getEncryptedData());

        keydata = decryptedKey;
    }

    // Now, proceed with decoding the unencrypted key

    String algorithm = "RSA"; // You determine this; you can use ASN.1 to get it from the bundle if you want

    KeyFactory kf = KeyFactory.getInstance(algorithm);

    KeySpec keySpec = new PKCS8EncodedKeySpec(keydata);

    PrivateKey key = kf.generatePrivate(keySpec);

【讨论】:

  • 您使用的是哪个 Java 版本?在我的 Java 11 上,当使用来自海报的“原始”加密私钥时,您的代码运行到异常“java.io.IOException:PBE 参数解析错误:期望 AES 密码的对象标识符”。
  • @MichaelFehr 我明白了;在我的单元测试中,我没有在任何密钥中使用这种特殊的加密策略,并且我发现我的代码能够读取这种类型的密钥。现在它可以读取它了。 :)
猜你喜欢
  • 2021-07-31
  • 1970-01-01
  • 2011-09-16
  • 2021-09-22
  • 1970-01-01
  • 1970-01-01
  • 2023-04-04
  • 2015-11-29
  • 2022-01-16
相关资源
最近更新 更多