主頁 > 後端開發 > Java實作7種常見密碼演算法

Java實作7種常見密碼演算法

2022-10-23 06:26:38 後端開發

原創:扣釘日記(微信公眾號ID:codelogs),歡迎分享,轉載請保留出處,

簡介

前面在密碼學入門一文中講解了各種常見的密碼學概念、演算法與運用場景,但沒有介紹過代碼,因此,為作補充,這一篇將會介紹使用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密碼演算法就介紹完了,來回顧一下:
image_2022-09-04_20220904160510

整體來說,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密鑰相關類關系一覽,如下:
secret_key
image_2022-09-04_20220904160242

常見問題

密文無法解密問題

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

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

如AES演算法加密模式有ECBCBCCFBCTRGCM等,填充模式有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()禁用,

生成與讀取證書

概念

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

首先,了解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

以上面證書為例,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的常用類:
JCA

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/518921.html

標籤:Java

上一篇:SpringMVC(六):攔截器

下一篇:在IDEA上跑struts1框架

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more