日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

簡介

前面在密碼學入門一文中講解了各種常見的密碼學概念、算法與運用場景,但沒有介紹過代碼,因此,為作補充,這一篇將會介紹使用JAVA語言如何實現使用這些算法,并介紹一下使用過程中可能遇到的坑。

Java加密體系JCA

Java抽象了一套密碼算法框架JCA(Java Cryptography Architecture),在此框架中定義了一套接口與類,以規范Java平臺密碼算法的實現,而Sun,SunRsaSign,SunJCE這些則是一個個JCA的實現Provider,以實現具體的密碼算法,這有點像List與ArrayList、LinkedList的關系一樣,Java開發者只需要使用JCA即可,而不用管具體是怎么實現的。

JCA里定義了一系列類,如Cipher、MessageDigest、mac、Signature等,分別用于實現加密、密碼學哈希、認證碼、數字簽名等算法,一起來看看吧!

對稱加密

對稱加密算法,使用Cipher類即可,以廣泛使用的AES為例,如下:

public byte[] encrypt(byte[] data, Key key) {
    try {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        byte[] iv = SecureRandoms.randBytes(cipher.getBlockSize());
        //初始化密鑰與加密參數iv
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
        //加密
        byte[] encryptBytes = cipher.doFinal(data);
        //將iv與密文拼在一起
        ByteArrayOutputStream baos = new ByteArrayOutputStream(iv.length + encryptBytes.length);
        baos.write(iv);
        baos.write(encryptBytes);
        return baos.toByteArray();
    } catch (Exception e) {
        return ExceptionUtils.rethrow(e);
    }
}

public byte[] decrypt(byte[] data, Key key) {
    try {
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        //獲取密文前面的iv
        IvParameterSpec ivSpec = new IvParameterSpec(data, 0, cipher.getBlockSize());
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        //解密iv后面的密文
        return cipher.doFinal(data, cipher.getBlockSize(), data.length - cipher.getBlockSize());
    } catch (Exception e) {
                return ExceptionUtils.rethrow(e);
    }
}

如上,對稱加密主要使用Cipher,不管是AES還是DES,Cipher.getInstance()傳入不同的算法名稱即可,這里的Key參數就是加密時使用的密鑰,稍后會介紹它是怎么來的,暫時先忽略它。
另外,為了使得每次加密出來的密文不同,我使用了隨機的iv向量,并將iv向量拼接在了密文前面。

注:如果某個算法名稱,如上面的AES/CBC/PKCS5Padding,你不知道它在JCA中的標準名稱是什么,可以到 https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html 中查詢即可。

非對稱加密

非對稱加密同樣是使用Cipher類,只是傳入的密鑰對象不同,以RSA算法為例,如下:

public byte[] encryptByPublicKey(byte[] data, PublicKey publicKey){
    try{
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return cipher.doFinal(data);
    }catch (Exception e) {
        throw Errors.toRuntimeException(e);
    }
}

public byte[] decryptByPrivateKey(byte[] data, PrivateKey privateKey){
    try{
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        return cipher.doFinal(data);
    }catch (Exception e) {
        throw Errors.toRuntimeException(e);
    }
}

一般來說應使用公鑰加密,私鑰解密,但其實反過來也是可以的,這里的PublicKey與PrivateKey也先忽略,后面會介紹它怎么來的。

密碼學哈希

密碼學哈希算法包括MD5、SHA1、SHA256等,在JCA中都使用MessageDigest類即可,如下:

public static String sha256(byte[] bytes) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    digest.update(bytes);
    return Hex.encodeHexString(digest.digest());
}

消息認證碼

消息認證碼使用Mac類實現,以常見的HMAC搭配SHA256為例,如下:

public byte[] digest(byte[] data, Key key) throws InvalidKeyException, NoSuchAlgorithmException{
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(key);
    return mac.doFinal(data);
}

數字簽名

數字簽名使用Signature類實現,以RSA搭配SHA256為例,如下:

public byte[] sign(byte[] data, PrivateKey privateKey) {
    try {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data);
        return signature.sign();
    } catch (Exception e) {
        return ExceptionUtils.rethrow(e);
    }
}

public boolean verify(byte[] data, PublicKey publicKey, byte[] sign) {
    try {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(data);
        return signature.verify(sign);
    } catch (Exception e) {
        return ExceptionUtils.rethrow(e);
    }
}

密鑰協商算法

在JCA中,使用KeyAgreement來調用密鑰協商算法,以ECDH協商算法為例,如下:

public static void testEcdh() {
    KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC");
    ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1");
    keyGen.initialize(ecSpec);
    // A生成自己的私密信息
    KeyPair keyPairA = keyGen.generateKeyPair();
    KeyAgreement kaA = KeyAgreement.getInstance("ECDH");
    kaA.init(keyPairA.getPrivate());
    // B生成自己的私密信息
    KeyPair keyPairB = keyGen.generateKeyPair();
    KeyAgreement kaB = KeyAgreement.getInstance("ECDH");
    kaB.init(keyPairB.getPrivate());

    // B收到A發送過來的公用信息,計算出對稱密鑰
    kaB.doPhase(keyPairA.getPublic(), true);
    byte[] kBA = kaB.generateSecret();

    // A收到B發送過來的公開信息,計算對對稱密鑰
    kaA.doPhase(keyPairB.getPublic(), true);
    byte[] kAB = kaA.generateSecret();
    Assert.isTrue(Arrays.equals(kBA, kAB), "協商的對稱密鑰不一致");
}

基于口令加密PBE

通常,對稱加密算法需要使用128位字節的密鑰,但這么長的密鑰用戶是記不住的,用戶容易記住的是口令,也即password,但與密鑰相比,口令有如下弱點:

  1. 口令通常較短,這使得直接使用口令加密的強度較差。
  2. 口令隨機性較差,因為用戶一般使用較容易記住的東西來生成口令。

為了使得用戶能直接使用口令加密,又能最大程度避免口令的弱點,于是PBE(Password Based Encryption)算法誕生,思路如下:

  1. 既然密碼算法需要密鑰,那在加解密前,先使用口令生成密鑰,然后再使用此密鑰去加解密。
  2. 為了彌補口令隨機性較差的問題,生成密鑰時使用隨機鹽來混淆口令來產生準密鑰,再使用散列函數對準密鑰進行多次散列迭代,以生成最終的密鑰。

因此,使用PBE算法進行加解密時,除了要提供口令外,還需要提供隨機鹽(salt)與迭代次數(iteratorCount),如下:

public static byte[] encrypt(byte[] plainBytes, String password, byte[] salt, int iteratorCount) {
    try {
        PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
        SecretKey key = SecretKeyFactory.getInstance("PBEWithMD5AndTripleDES").generateSecret(keySpec);

        Cipher cipher = Cipher.getInstance("PBEWithMD5AndTripleDES");
        cipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(salt, iteratorCount));
        byte[] encryptBytes = cipher.doFinal(plainBytes);
        byte[] iv = cipher.getIV();
        ByteArrayOutputStream baos = new ByteArrayOutputStream(iv.length + encryptBytes.length);
        baos.write(iv);
        baos.write(encryptBytes);
        return baos.toByteArray();
    } catch (Exception e) {
        throw Errors.toRuntimeException(e);
    }
}

public static byte[] decrypt(byte[] secretBytes, String password, byte[] salt, int iteratorCount) {
    try {
        PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray());
        SecretKey key = SecretKeyFactory.getInstance("PBEWithMD5AndTripleDES").generateSecret(keySpec);

        Cipher cipher = Cipher.getInstance("PBEWithMD5AndTripleDES");
        IvParameterSpec ivParameterSpec = new IvParameterSpec(secretBytes, 0, cipher.getBlockSize());
        cipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(salt, iteratorCount, ivParameterSpec));
        return cipher.doFinal(secretBytes, cipher.getBlockSize(), secretBytes.length - cipher.getBlockSize());
    } catch (Exception e) {
        throw Errors.toRuntimeException(e);
    }
}

public static void main(String[] args) throws Exception {
    byte[] content = "hello".getBytes(StandardCharsets.UTF_8);
    byte[] salt = Base64.decode("QBadPOP6/JM=");
    String password = "password";
    byte[] encoded = encrypt(content, password, salt, 1000);
    System.out.println("密文:" + Base64.encode(encoded));
    byte[] plainBytes = decrypt(encoded, password, salt, 1000);
    System.out.println("明文:" + new String(plainBytes, StandardCharsets.UTF_8));
}

注意,雖然使用PBE加解密數據,都需要使用相同的password、salt、iteratorCount,但這里面只有password是需要保密的,salt與iteratorCount不需要,可以保存在數據庫中,比如每個用戶注冊時給他生成一個隨機鹽。

到此,JCA密碼算法就介紹完了,來回顧一下:

 

整體來說,JCA對密碼算法相關的類設計與封裝還是非常清晰簡單的!

但使用密碼算法時,依賴SecretKey、PublicKey、PrivateKey對象提供密鑰信息,那這些密鑰對象是怎么來的呢?

密鑰生成與讀取

密碼學隨機數

密碼學隨機數算法在安全場景中使用廣泛,如:生成對稱密鑰、鹽、iv等,因此相比普通的隨機數算法(如線性同余),它需要更高強度的不可預測性,在Java中,使用SecureRandom來生成更安全的隨機數,如下:

public class SecureRandoms {
 public static byte[] randBytes(int len) throws NoSuchAlgorithmException {
  byte[] bytes = new byte[len];
  SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
  secureRandom.nextBytes(bytes);
  return bytes;
 }
}

SecureRandom使用了更高強度的隨機算法,同時會讀取機器本身的隨機熵值,如/dev/urandom,因此相比普通的Random,它具有更強的隨機性,因此,對于需要生成密鑰的場景,該用哪個要擰得清。

對稱密鑰

在JCA中對稱密鑰使用SecretKey表示,若要生成一個新的SecretKey,可使用KeyGenerator,如下:

//生成新的密鑰
public static SecretKey genSecretKey() {
    KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
    keyGenerator.init(SecureRandom.getInstance("SHA1PRNG"));
    SecretKey secretKey = keyGenerator.generateKey();
}

而如果是從文件中讀取密鑰的話,則可以借助SecretKeyFactory將其轉換為SecretKey,如下:

//讀取密鑰
public static SecretKey getSecretKey() {
    byte[] keyBytes = readKeyBytes();
    String alg = "AES";
    SecretKey secretKey = SecretKeyFactory.getInstance(alg).generateSecret(new SecretKeySpec(keyBytes, alg));
}

非對稱密鑰

在JCA中,對于非對稱密鑰,公鑰使用PublicKey表示,私鑰使用PrivateKey表示,若要生成一個新的公私鑰對,可使用KeyPairGenerator,如下:

//生成新的公私鑰對
public static void genKeyPair() {
    KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
    keyPairGen.initialize(2048);
    KeyPair keyPair = keyPairGen.generateKeyPair();
    PublicKey publicKey = keyPair.getPublic();
    PrivateKey privateKey = keyPair.getPrivate();
}

而如果是從文件中讀取公私鑰的話,一般公鑰是X509格式,而私鑰是PKCS8格式,分別對應JCA中的X509EncodedKeySpec與PKCS8EncodedKeySpec,如下:

//讀取私鑰
public static PrivateKey getPrivateKey() {
    byte[] privateKeyBytes = readPrivateKeyBytes();
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
    PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec);
}

//讀取公鑰
public static PublicKey getPublicKey() {
    byte[] publicKeyBytes = readPublicKeyBytes();
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKeyBytes);
    PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
}

注意,KeyGenerator、KeyPairGenerator與KeyFactory從命名上看起來有點相似,但它們實現的功能是完全不同的,KeyGenerator、KeyPairGenerator用于生成新的密鑰,而KeyFactory則用于將KeySpec轉換為對應的Key密鑰對象。

JCA密鑰相關類關系一覽,如下:

 


 

常見問題

密文無法解密問題

有時,在使用密碼算法時,會發現別人提供的密文使用正確的密鑰卻無法解密出來,特別容易發生在跨語言的情況下,如加密方使用的C#語言,而解密方卻使用的Java。

遇到這種情況,你需要和對方認真確認加密時使用的加密模式、填充模式以及IV等密碼參數是否完全一致。

如AES算法加密模式有ECB、CBC、CFB、CTR、GCM等,填充模式有PKCS#5, ISO 10126, ANSI X9.23等,以及對方是使用了固定的IV向量還是將IV向量拼在了密文中,這些都需要確認清楚并與對方保持一致才能正確解密。

簽名失敗問題

簽名失敗也是使用密碼算法時常見的情況,比如對方生成的MD5值與你生成的MD5不一致,常見有2種原因,如下:
1. 使用的字符編碼不一致導致
密碼算法為了通用性,操作對象都是字節數組,而你要簽名的對象一般是字符串,因此你需要將字符串轉為字節數組之后再做md5運算,如下:

  • 調用方:md5(str.getBytes())
  • 服務方:md5(str.getBytes())

看起來兩邊的代碼一模一樣,但問題就在getBytes()函數中,getBytes()函數默認會使用操作系統的字符編碼將字符串轉為字節數組,而中文windows默認字符編碼是GBK,而linux默認是UTF-8,這就導致當str中有中文時,調用方與服務方獲取到的字節數組是不一樣的,那生成的MD5值當然也不一樣了。

因此,強烈推薦在使用getBytes()函數時,傳入統一的字符編碼,如下:

  • 調用方:md5(str.getBytes("UTF-8"))
  • 服務方:md5(str.getBytes("UTF-8"))
    這樣就能有效地避過這個非常隱晦的坑了。

2. json的escape功能導致
有些json框架,做json序列化時會默認做一些轉義操作,如把&字符轉義為u0026,但如果服務端做json反序列化時沒有做反轉義,這會導致兩邊計算的簽名值不一樣,如下:

  • 調用方:md5("&")
  • 服務方:md5("\u0026")
    這也是一個非常隱晦的坑,如Gson默認就會有這種行為,可使用new GsonBuilder().disableHtmlEscaping()禁用。

生成與讀取證書

概念

隨著對密碼學了解的深入,會發現有特別多奇怪的名詞出現,讓人迷惑不已,如PKCS8、X.509、ASN.1、DER、PEM等,接下來就來澄清下這些名詞是什么,以及它們之間的關系。

首先,了解3個概念,如下:

  • 密鑰:包括對稱密鑰與非對稱密鑰等。
  • 證書:包含用戶或網站的身份信息、公鑰,以及CA的簽名。
  • 密鑰庫:用于存儲密鑰與證書的倉庫。

ASN.1語法

ASN.1抽象語法標記(Abstract Syntax Notation One),和XML、JSON類似,用于描述對象結構,可以把它看成一種描述語言,簡單的示例如下:

Report ::= SEQUENCE {
author OCTET STRING,
title OCTET STRING,
body OCTET STRING,
}

這個語法描述了一個結構體,它包含3個屬性author、title、body,且都是字符串類型。

DER與PEM

DER是ASN.1的一種序列化編碼方案,也就是說ASN.1用來描述對象結構,而DER用于將此對象結構編碼為可存儲的字節數組。

PEM(Privacy Enhanced Mail)是一種將二進制數據,以文本形式進行存儲或傳輸的方案,早期主要用于郵件中交換證書,它的文本內容常以-----BEGIN XXX-----開頭,并以-----END XXX-----結尾,而中間 Body 部分則為 Base64 編碼后的數據,如下是一個證書的PEM樣例。

 

以上面證書為例,PEM與DER的關系大概如下:

PEM = "-----BEGIN CERTIFICATE-----" + base64(DER) +  "-----END CERTIFICATE-----"

X.509、PKCS8、PKCS12等

X.509、PKCS8、PKCS12等都是公鑰密碼學標準(PKCS)組織制定的各種密碼學規范,該組織使用ASN.1語法為密鑰、證書、密鑰庫等定義了標準的對象結構,常見的如下:

  • X.509規范:用于描述證書與公鑰的標準格式。
  • PKCS7規范:可描述的對象很多,不過一般也是用于描述證書的。
  • PKCS8規范:用于描述私鑰的標準格式。
  • PKCS12規范:用于描述密鑰庫的標準格式。
  • PKCS1規范:用于描述RSA算法及其公私鑰的標準格式。

這些規范都有相應的RFC文檔,感興趣的可以前往查看:

PEM:https://www.rfc-editor.org/rfc/rfc7468   
X.509:https://datatracker.ietf.org/doc/html/rfc5280  
PKCS7:https://datatracker.ietf.org/doc/html/rfc2315  
PKCS8:https://datatracker.ietf.org/doc/html/rfc8351  
PKCS12:https://datatracker.ietf.org/doc/html/rfc7292  
PKCS1:https://datatracker.ietf.org/doc/html/rfc8017#Appendix-A  

類比一下,如果把ASN.1比作Java,那X.509就是使用Java定義的一個名叫X509的類,這個類里面包含身份信息、公鑰信息等相關字段,而DER就是一種Java對象序列化方案,用于將X509這個類的對象序列化為字節數組,字節數組保存為文件后,這個文件就是我們常說的證書或密鑰文件。

常見證書文件

由于PKCS組織并未給證書文件定下標準的文件名后綴,所以證書文件有非常多的后綴名,如下:

  • .der: DER編碼的證書,一般是X.509規范的,無法用文本編輯器直接打開
  • .pem: PEM編碼的證書,一般是X.509規范的
  • .crt: 常見于unix類系統,一般是X.509規范的,可能是DER編碼或PEM編碼
  • .cer: 常見于windows系統,一般是X.509規范的,可能是DER編碼或PEM編碼
  • .p7b: 常見于windows系統,PKCS7規范證書,可能是DER編碼或PEM編碼
  • .pfx:PKCS12規范的密鑰庫文件,也有取名為.p12的
  • .jks:java專用的密鑰庫文件格式,在java技術棧內使用較多,非java一般使用.pfx

證書概念小結

 

Certificate

生成證書與密鑰庫

openssl命令提供了大量的工具,用以生成密鑰、證書與密鑰庫文件,如下,是一個典型的生成密鑰與證書的過程:

# 生成pkcs1 rsa私鑰
openssl genrsa -out rsa_private_key_pkcs1.key 2048
# 生成pkcs1 rsa公鑰
openssl rsa -in rsa_private_key_pkcs1.key -RSAPublicKey_out -out rsa_public_key_pkcs1.key

# 生成證書申請文件cert.csr
openssl req -new -key rsa_private_key_pkcs1.key -out cert.csr
# 自簽名(演示時使用,生產環境一般不用自簽證書)  
openssl x509 -req -days 365 -in cert.csr -signkey rsa_private_key_pkcs1.key -out cert.crt
# ca簽名(將證書申請文件提交給ca機構簽名)
openssl x509 -req -days 365 -in cert.csr -CA ca_cert.crt -CAkey ca_private_key.pem -CAcreateserial -out cert.crt

# 生成p12密鑰庫文件
openssl pkcs12 -export -in cert.crt -inkey rsa_private_key_pkcs1.key -name demo -out keystore.p12

有時別人發來的密鑰或證書文件無法讀取,也可使用openssl確認一下,如果openssl能讀出來,那大概率是自己程序有問題,如果openssl讀不出來,那大概率是別人發的文件有問題,如下:

# 查看pkcs1 rsa私鑰
openssl rsa -in rsa_private_key_pkcs1.key -text -noout
# 查看pkcs1 rsa公鑰
openssl rsa -RSAPublicKey_in -in rsa_public_key_pkcs1.key -text -noout

# 查看x.509證書
openssl x509 -in cert.crt -text -nocert

# 查看pkcs12密鑰庫文件
openssl pkcs12 -in keystore.p12
keytool -v -list -storetype pkcs12 -keystore keystore.p12

由于密鑰、證書、密鑰庫文件,其實都是使用ASN.1語法描述的,所以它們都能按ASN.1語法解析出來,如下:

openssl asn1parse -i -inform pem -in cert.crt

證書格式轉換

某些情況下,我們需要在不同格式的密鑰或證書文件之間轉換,也可使用openssl命令來完成。
密鑰格式轉換,如下:

# rsa公鑰轉換為X509公鑰
openssl rsa -RSAPublicKey_in -in rsa_public_key_pkcs1.key -pubout -out public_key_x509.key
# rsa私鑰轉換為PKCS8格式
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key_pkcs1.key -outform PEM -nocrypt -out private_key_pkcs8.key
# pkcs8轉rsa私鑰
openssl pkcs8 -inform PEM -nocrypt -in private_key_pkcs8.key -traditional -out rsa_private_key_pkcs1.key

證書格式轉換,如下:

# 證書DER轉PEM
openssl x509 -inform der -in cert.der -outform pem -out cert.pem -noout
# x509證書轉pkcs7證書
openssl crl2pkcs7 -nocrl -certfile cert.crt -out cert.p7b
# 查看pkcs7證書
openssl pkcs7 -print_certs -in cert.p7b -noout

由于密鑰庫中包含證書與私鑰,故可以從密鑰庫文件中提取出證書與私鑰,如下:

# 從pkcs12密鑰庫中提取證書
openssl pkcs12 -in keystore.p12 -clcerts -nokeys -out cert.crt
# 從pkcs12密鑰庫中提取私鑰
openssl pkcs12 -in keystore.p12 -nocerts -nodes -out private_key.key
# pkcs12轉jks
keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -srcalias demo -destkeystore keystore.jks -deststoretype jks -deststorepass 123456 -destalias demo
# 從jks中提取證書
keytool -export -alias demo -keystore keystore.jks -file cert.crt

讀取密鑰或證書文件

使用JCA來讀取密鑰或證書文件,也是非常方便的。

PEM轉DER

若要將PEM格式文件轉換為DER,只需要把---BEGIN XXX---與---END XXX---去掉,然后使用Base64解碼即可,如下:

private static byte[] pemFileToDerBytes(String pemFilePath) throws IOException {
    InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(pemFilePath);
    String pemStr = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
    //去掉---BEGIN XXX---與---END XXX---
    pemStr = pemStr.replaceAll("---+[^-]+---+", "")
            .replaceAll("\s+", "");
    //base64解碼為DER二進制內容
    return Base64.getDecoder().decode(pemStr);
}

讀取PKCS8私鑰

在JCA中,使用PKCS8EncodedKeySpec解析PKCS8私鑰文件,如下:

public static void testPkcs8PrivateKeyFile() {
    byte[] derBytes = pemFileToDerBytes("cert/private_key_pkcs8.key");
    PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(derBytes);
    RSAPrivateCrtKey rsaPrivateCrtKey = (RSAPrivateCrtKey)KeyFactory.getInstance("RSA").generatePrivate(pkcs8EncodedKeySpec);
    BigInteger n = rsaPrivateCrtKey.getModulus();
    BigInteger e = rsaPrivateCrtKey.getPublicExponent();
    BigInteger d = rsaPrivateCrtKey.getPrivateExponent();
    System.out.printf(" n: %X n e: %X n d: %X n", n, e, d);
    BigInteger plain = BigInteger.valueOf(new Random().nextInt(1000000000));
    // RSA加密
    long t1 = System.nanoTime();
    BigInteger secret = plain.modPow(e, n);
    long t2 = System.nanoTime();
    // RSA解密
    BigInteger plain2 = secret.modPow(d, n);
    long t3 = System.nanoTime();
    System.out.printf(" plain: %d n plain2: %d n", plain, plain2);
    System.out.printf("enc time: %d n", (t2 - t1));
    System.out.printf("dec time: %d n", (t3 - t2));
}

讀取X.509公鑰

在JCA中,使用X509EncodedKeySpec解析X.509公鑰文件,如下:

public static void testX509PublicKeyFile() {
    byte[] derBytes = pemFileToDerBytes("cert/public_key_x509.key");
    X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(derBytes);
    RSAPublicKey rsaPublicKey = (RSAPublicKey)KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec);
    BigInteger e = rsaPublicKey.getPublicExponent();
    BigInteger n = rsaPublicKey.getModulus();
    System.out.printf(" e: %X n n: %X n", e, n);
}

讀取X.509證書

讀取X.509證書文件,可使用CertificateFactory類,如下:

public static void testX509CertFile() {
    byte[] derBytes = pemFileToDerBytes("cert/cert.crt");
    Collection<? extends Certificate> certificates = CertificateFactory.getInstance("X.509")
            .generateCertificates(new ByteArrayInputStream(derBytes));
    for(Certificate certificate : certificates){
        X509Certificate x509Certificate = (X509Certificate)certificate;
        System.out.printf("SubjectDN: %s n", x509Certificate.getSubjectDN());
        System.out.printf("IssuerDN: %s n", x509Certificate.getIssuerDN());
        System.out.printf("SigAlgName: %s n", x509Certificate.getSigAlgName());
        System.out.printf("Signature: %s n", Hex.encodeHexString(x509Certificate.getSignature()));
        System.out.printf("PublicKey: %s n", x509Certificate.getPublicKey());
    }
}

讀取PKCS12密鑰庫文件

讀取PKCS12規范的密鑰庫文件,可使用KeyStore類,如下:

public static void testPkcs12File() {
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("cert/keystore.p12");
    char[] password = "123456".toCharArray();
    keyStore.load(is, password);
    //獲取證書
    X509Certificate x509Certificate = (X509Certificate)keyStore.getCertificate("demo");
    System.out.println("X509Certificate: ");
    System.out.printf("SubjectDN: %s n", x509Certificate.getSubjectDN());
    System.out.printf("IssuerDN: %s n", x509Certificate.getIssuerDN());
    System.out.printf("SigAlgName: %s n", x509Certificate.getSigAlgName());
    System.out.printf("Signature: %s n", Hex.encodeHexString(x509Certificate.getSignature()));
    System.out.printf("PublicKey: %s n", x509Certificate.getPublicKey());
    //獲取私鑰
    Key key = keyStore.getKey("demo", password);
    System.out.printf("PrivateKey: %s n", key);
}

如果要讀取.jks文件,只需要將KeyStore.getInstance("PKCS12")中的PKCS12更換為JKS即可,其它部分保持不變,不過由于JKS是java專有格式,目前java也不推薦使用了,所以能不用的話,就盡量不要用了。

常見問題

證書信任問題

證書的絕大多數應用場景是Https協議,但在訪問https接口時,有時會由于證書信任問題導致https握手失敗,主要有以下2點原因:

  1. 有些公司會自建CA,使用自簽證書,如早期的12306,而jdk只信任它預置的根證書,所以https握手時這種證書會認證失敗。
  2. 新成立的根CA機構證書,沒預置在舊的jdk里面,導致這些CA機構簽發的證書不被信任。

要解決這種證書信任問題,有兩種方法,如下:
1. 將證書導致到jdk的預置證書庫中

# 將cert.crt導入jdk預置密鑰庫文件,密鑰庫文件密碼默認是changeit
sudo keytool -importcert -file cert.crt -alias demo -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit

# 查看密鑰庫文件,檢查是否導入成功
keytool -list -v -alias demo -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit

2. 以編碼的方式信任證書
以jdk自帶的https sdk為例,可在代碼中手動將問題證書添加到信任列表中,如下:

public String testReqHttpsTrustCert() throws Exception {
    // 讀取jdk預置證書
    KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    try(InputStream ksIs = new FileInputStream(System.getProperty("java.home") + "/lib/security/cacerts")) {
        keyStore.load(ksIs, "changeit".toCharArray());
    }

    // 讀取證書文件
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    try(InputStream certIs = this.getClass().getResourceAsStream("/cert/cert.crt")) {
        Certificate c = cf.generateCertificate(certIs);
        keyStore.setCertificateEntry("demo", c);
    }

    // 生成信任管理器
    TrustManagerFactory tmFact = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmFact.init(keyStore);

    // 生成SSLSocketFactory
    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
    sslContext.init(null, tmFact.getTrustManagers(), new SecureRandom());
    SSLSocketFactory ssf = sslContext.getSocketFactory();

    // 發送https請求
    URL url = new URL("https://www.demo.com/user/list");
    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
    connection.setHostnameVerifier((hostname, session) -> hostname.endsWith("demo.com"));
    connection.setSSLSocketFactory(ssf);

    String result;
    try(InputStream inputStream = connection.getInputStream()){
        result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
    }
    connection.disconnect();
    return result;
}

注:雖然2種方法都可以解決問題,但第1種方法使得java程序對環境形成了依賴,一旦部署環境發生變化,java程序可能就報錯了,因此更推薦使用第2種方法。

總結

到這里,JCA相關類的使用就介紹完了,如下表格中總結了JCA的常用類:

 

本篇花了近一周時間整理,內容較多,對這塊不太熟悉的同學,可以先關注收藏起來當示例手冊,待需要時再參閱即可。

分享到:
標簽:Java
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定