# 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

最後會得到三個檔案:
> 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`