# RSA 加解密及簽名驗證 商業邏輯中常需要 OpenSSL 來驗證 API 的溝通。通常由被訪問端發行金鑰,而訪問端用程式進行溝通。由於金鑰有不同版本及未加密/已加密之分,所以 Java 中的做法也會有所不同 ## OpenSSL on Linux 4.1 的產鑰範例 > 1. 輸入指令 openssl,看到提示符號 OpenSSL> > 2. 輸入生成 private-key 指令: > genrsa -out myPrivateKeyPKSC1 1024 > 3. 輸入指令將私鑰轉換為 PKSC8,過程中詢問的密碼只在建立時用到故可隨意指定,指令: > pkcs8 -topk8 -inform PEM -in myPrivateKeyPKSC1 -outform PEM -out myPrivateKeyPKSC8 –nocrypt > 4. 輸入生成public-key 指令: > rsa -in myPrivateKeyPKSC8 -pubout -out myPublicKey > 5. 輸入 exit 以退出 openssl ![](https://i.imgur.com/OOyZjeW.png) 最後會得到三個檔案: > PrivateKeyPKSC1 是私鑰,在步驟 2 產生,用於加密明文、產生秘文 > PrivateKeyPKSC8 是步驟 3 在轉換時順便產出的 PKSC8 私鑰 > PubPKSC8 是公鑰,特意在步驟 4 轉換完後產生,留在被訪問端,用來驗證明文與秘文 總結來說,這個範例想產 1 個 PKSC1 格式的私鑰、1 個 PKSC8 格式的對應公鑰。所以產完私鑰後故意先做轉換成 PKSC8,再把公鑰產出 這是因為各個單位的商業邏輯考量不同,所以才有這種作法 --- ## 困難點整理 ### Java 不支援 PKSC1,支援 PKSC8 Java 內建的 Package java.security.spec 支援 PKSC8,它有 PKCS8EncodedKeySpec 可以做處理,但無法處理 PKSC1 要強行支援 PKSC1,需要引入第三方資源如 [bouncycastle](http://www.bouncycastle.org/)、[Spongy Castle](http://rtyley.github.io/spongycastle/),用的人相對有限且較熱門的版本有點舊 如果最一開始在伺服器端產金鑰時有轉成 PKSC8 會比較好處理,例如: > openssl pkcs8 **-topk8** -inform PEM -outform DER -in private_key_file -nocrypt > pkcs8_key [How to Load RSA Private Key From File](https://stackoverflow.com/questions/3243018/how-to-load-rsa-private-key-from-file) ### Java 不支援 PEM 格式金鑰檔,支援 DER 上面的檔案產出的金鑰是 PEM 格式,內文是稍具閱讀性的亂碼,而 Java 不支援辨識這樣的金鑰,需要另外用 Base64 解碼或倚賴 [bouncycastle](http://www.bouncycastle.org/)、[Spongy Castle](http://rtyley.github.io/spongycastle/) 這些資源來解讀 如果最一開始在伺服器端產金鑰時有轉成 DER 如下: > openssl pkcs8 -topk8 -inform **PEM** -outform **DER** -in private_key_file -nocrypt > pkcs8_key 由於產出是 binary 二進位碼,Java 就能直接讀,例如: ```java= public PrivateKey genPrivateKey(byte[] key) { KeySpec keySpec = new **PKCS8EncodedKeySpec**(key); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePrivate(keySpec); } ``` ==警告: 有需要作 base64 decode 時別用 com.ibm.misc.BASE64Decoder,有機率出現肉眼難以辨識的 bug。請改用 java.util.Base64 或 org.springframework.util.Base64Utils== [JAVA解析各种编码密钥对(DER、PEM、openssh公钥)](https://blog.csdn.net/hzzhoushaoyu/article/details/8627952) ### 辨識 PKSC1(SSLeay)、PKSC8、OpenSSH PKSC1 PEM:開頭結尾為 -----BEGIN RSA PRIVATE KEY-----、-----END RSA PRIVATE KEY----- PKSC8 PEM:未加密的開頭結尾為 -----BEGIN PRIVATE KEY-----、-----END PRIVATE KEY----- PKSC8 PEM:加密的開頭結尾為 -----BEGIN ENCRYPTED PRIVATE KEY-----、-----END ENCRYPTED PRIVATE KEY----- PKCS8 DER:( binary 二進位碼) OpenSSH:開頭 ssh-rsa、結尾 RSA-1024 [Getting RSA private key from PEM BASE64 Encoded private key file](https://stackoverflow.com/questions/7216969/getting-rsa-private-key-from-pem-base64-encoded-private-key-file) --- ## 常用方法 ### 從 PKSC1 PEM 字串產生 PrivateKey (String -> Base64 -> PrivateKey) ```java= public static PrivateKey genPKfromStr(String pemKeyString) { byte[] bytes = java.util.Base64.getDecoder().decode(pemKeyString); DerInputStream derReader = new DerInputStream(bytes); DerValue[] seq = derReader.getSequence(0); BigInteger modulus = seq[1].getBigInteger(); BigInteger publicExp = seq[2].getBigInteger(); BigInteger privateExp = seq[3].getBigInteger(); BigInteger prime1 = seq[4].getBigInteger(); BigInteger prime2 = seq[5].getBigInteger(); BigInteger exp1 = seq[6].getBigInteger(); BigInteger exp2 = seq[7].getBigInteger(); BigInteger crtCoef = seq[8].getBigInteger(); RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef); KeyFactory factory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = factory.generatePrivate(keySpec); ``` ### 用 PrivateKey 簽署明文 作加密 (String -> byte[] -> sign "SHA256WithRSA" -> Base64 encode -> String) ```java= public static String signWithS256RSA(String text, PrivateKey privateKey) { byte[] msgBytes = text.getBytes(); Signature sig = Signature.getInstance("SHA256WithRSA"); sig.initSign(privateKey); sig.update(msgBytes); byte[] signatureBytes = sig.sign(); String signedText = Base64.byteArrayToBase64(signatureBytes); ``` 最後一句將 byte 序列 encode 回 Base64 字串的 [Base64 class](https://gitlab.com/bynum5566/hackmdresource/-/raw/master/Base64.java) 請利用本連結獲得 ### 從 PKSC8 字串取得 RSAPublicKey ```java= public static RSAPublicKey loadPublicKeyByStr(String publicKeyStr){ byte[] buffer = Base64.decode(publicKeyStr); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); return (RSAPublicKey) keyFactory.generatePublic(keySpec); ``` ### 用 RSAPrivateKey 加密字串 (利用 Cipher) ```java= public static byte[] encrypt(RSAPrivateKey privateKey, byte[] plainTextData) // 使用預設RSA Cipher cipher = Cipher.getInstance("RSA"); // 初始化演算法:私鑰加密 cipher.init(Cipher.ENCRYPT_MODE, privateKey); // 加密後的密文資料,以字元陣列的形式存在 byte[] output = cipher.doFinal(plainTextData); ``` *plainTextData 用 String.toBytes() 得來 ### 用 RSAPublicKey 解密 bytes[] (利用 Cipher) ```java= public static byte[] decrypt(RSAPublicKey publicKey, byte[] cipherData) // 使用預設RSA // 此處或可使用 cipher= Cipher.getInstance("RSA", new BouncyCastleProvider()); Cipher cipher = Cipher.getInstance("RSA"); // 初始化加密演算法:公鑰解密 cipher.init(Cipher.DECRYPT_MODE, publicKey); byte[] output = cipher.doFinal(cipherData); // 以字元陣列形式存在的明文資料 return output; ``` 用 PublicKey 校驗成對明文及秘文是否合法 ```java= public static boolean doCheck(String content, String sign, String publicKey) { KeyFactory keyFactory = KeyFactory.getInstance("RSA"); byte[] encodedKey = Base64.decode(publicKey); PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey)); // 使用 SHA1WithRSA 簽名演算法 java.security.Signature signature = java.security.Signature.getInstance(SIGN_ALGORITHMS); // 用公鑰初始化簽名物件準備作校驗 signature.initVerify(pubKey); // 原始字串轉 byte[] 注入簽名物件中 signature.update(content.getBytes()); // 簽署過的明文以 Base64 解碼,注入簽名物件進行校驗,產生驗證結果 boolean bverify = signature.verify(Base64.decode(sign)); ``` [Java實現RSA的加密解密簽名和驗證](https://www.mdeditor.tw/pl/pS7G/zh-tw) ###### tags: `Java`