顯示具有 加密 標籤的文章。 顯示所有文章
顯示具有 加密 標籤的文章。 顯示所有文章

2022年3月1日 星期二

使用 RS256 簽名演算法對 JWT 做簽章 - Java

JWT (JSON Web Token) 可以用於儲存不敏感的訊息來讓 client 和 server 端做溝通,

其支援不同的簽名加密演算法,例如對稱加密的 HS256 或 非對稱加密的 RS256 等,
在這篇文章裡,我記錄了一下使用了 RS256 的 JWT 驗證過程。

RS256 結合了 RSASSA-PKCS1-v1_5 和 SHA-256,
RSASSA-PKCS1-v1_5 是一種非對稱加密演算法、
而 SHA-256 是一種雜湊演算法,參考 Digital Signature with RSASSA-PKCS1-v1_5

RS256 跟 HS256 不同的地方是,RS256使用了非對稱加密的RSA演算法,所以擁有一對
Public Key (公鑰) 和 Private Key (私錀),而 HS256 只有一把鑰匙。
在安全上,HS256 需要發佈方(例如提供 API 的 server 端)和驗證方(例如呼叫 API 的 client 端)都要擁有同一把錀匙,如果這把錀匙流露出去讓不法的第三方得知,
第三方將可使用這把錀匙偽造出假訊息的 JWT token 讓 server, client 端無法查覺。

跟只使用了一個密碼的 HS256 不同,RS256 使用了一對公錀與密錀,
提供 API 的 server 方可以以不公開的密錀對 JWT 做加密雜溱演算,
client 端及 server 端都可以用 server 公開出來的公錀來驗證 JWT 的合法性,能確認 JWT 確實為
server 端所發出且沒有被遭到竄改

Note:
 https://jwt.io/ 是一個提供線上 JWT token 產生、驗證範例的網站,想要做測試時還蠻好用的,其也提供了不同的簽名演算法可供選擇。

在這篇文中,我示範了:

  1. 如何產生 RSA 的公錀及私錀。
  2. 如何使用 RSA 的私錀進行 RS256 JWT 的簽發。
  3. 如何使用 RSA 的公錀進行 RS256 JWT 的驗證。
下面直接上程式碼:

首先是有用到的 library dependency, 
pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>test</groupId>
  <artifactId>jwtTest</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  
  <dependencies>
  	<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
	<dependency>
	    <groupId>javax.xml.bind</groupId>
	    <artifactId>jaxb-api</artifactId>
	    <version>2.3.0</version>
	</dependency>
	
	<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
	<dependency>
	    <groupId>io.jsonwebtoken</groupId>
	    <artifactId>jjwt</artifactId>
	    <version>0.9.1</version>
	</dependency>
  </dependencies>
  
</project>

JwtTest.java:

package main;

import java.io.UnsupportedEncodingException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Base64;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClaims;

public class JwtTest {

	public static void main(String[] args) throws Exception {
		KeyPair keyPair = generateRsaKeyPair();
        
		String publicKeyStr = "-----BEGIN PUBLIC KEY-----" + new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded()), "UTF-8") + "-----END PUBLIC KEY-----";
		String privateKeyStr = "-----BEGIN PRIVATE KEY-----" + new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded()), "UTF-8") + "-----END PRIVATE KEY-----";
		
		System.out.println("Public Key: " + publicKeyStr);
		System.out.println("Private Key: " + privateKeyStr);
        
        String someProperyValue = "someName";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");				
		Claims claims = new DefaultClaims();
        claims.setIssuer("issuer")
                .setSubject("test")
                .setExpiration(sdf.parse("2200-01-01"))
                .put("name", someProperyValue);
        
        String generatedRsaJwt = generateRsaJwt(claims, privateKeyStr);
		Claims parsedClaims = parseRsaJwt(generatedRsaJwt, publicKeyStr);
		System.out.println("JWT Token: " + generatedRsaJwt);
		//verify content
		System.out.println(someProperyValue.equals(parsedClaims.get("name", String.class))); //true
	}
	
	static String generateRsaJwt(Claims claims, String privateKeyStr) throws InvalidKeySpecException, UnsupportedEncodingException, NoSuchAlgorithmException {
		String generatedRsaJwt = null;
		
		privateKeyStr = privateKeyStr.replace("-----BEGIN PRIVATE KEY-----", "");
        privateKeyStr = privateKeyStr.replace("-----END PRIVATE KEY-----", "");
        privateKeyStr = privateKeyStr.replaceAll("\r\n", "");
        privateKeyStr = privateKeyStr.replaceAll("\\s+", "");
        
        PKCS8EncodedKeySpec keySpec_private = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr.getBytes("UTF-8")));
        KeyFactory keyFactory_private = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory_private.generatePrivate(keySpec_private);
        
		generatedRsaJwt = Jwts.builder()
        .setHeaderParam("typ", "JWT")
        .setClaims(claims)
        .signWith(SignatureAlgorithm.RS256, privateKey)               
        .compact();
		
		return generatedRsaJwt;
	}
	
	static Claims parseRsaJwt(String jwtToParse, String publicKeyStr) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException {
		Claims parsedRsaJwtClaims = null;
		
		publicKeyStr = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", "");
		publicKeyStr = publicKeyStr.replace("-----END PUBLIC KEY-----", "");
		publicKeyStr = publicKeyStr.replaceAll("\r\n", "");
		publicKeyStr = publicKeyStr.replaceAll("\\s+", "");
		
		X509EncodedKeySpec keySpec_public = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr.getBytes("UTF-8")));
        KeyFactory keyFactory_public = KeyFactory.getInstance("RSA");
        PublicKey publicKey_public = keyFactory_public.generatePublic(keySpec_public);
        
        Jws<Claims> jws = Jwts.parser()
        					  .setSigningKey(publicKey_public)
        					  .parseClaimsJws(jwtToParse);
        
        parsedRsaJwtClaims = jws.getBody();
		
		return parsedRsaJwtClaims;
	}
	
	static KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException, UnsupportedEncodingException {
		KeyPair keyPair = null;
		
		SecureRandom secureRandom = new SecureRandom();
		secureRandom.setSeed("test".getBytes("UTF-8"));
		
		KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");	
		keyPairGenerator.initialize(2048, secureRandom); // secureRandom is not necessary,
														 // you can also use keyPairGenerator.initialize(2048); for random seed
														 // note: if use same seed for secureRandom, will get same public key and private key.
		keyPair = keyPairGenerator.generateKeyPair();
		
		return keyPair;
	}

}

說明:

  1. generateRsaKeyPair() 會產生一對 RSA Public Key 及 Private Key,SecureRandom 並非必須,只是做個示範,要注意的是 SecureRandom 如果給予一樣的種子 (Seed) ,可能會造成每次產生出一樣的公錀及密錀 (但也有可能不會,看SecureRandom、KeyGenerator 等底層演算法而定)。
  2. generateRsaJwt() 使用私錀為 JWT 做簽章,parseRsaJwt() 使用公錀對 JWT 做驗證,如果有驗證問題的話,例如有人用了另一個錯吳的私錀 (真正的私錀不可流露出去,理論上其他第三方應無法知道真正的私錀) 做假訊息的簽章,在用公錀驗證時就會出現驗證錯的訊息:
    Exception in thread "main" io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
    

參考資料:

  1. Generating a JWT using an existing private key and RS256 algorithm
  2. 常見簽名算法之SHA256withRSA
  3. 在 Java 使用加密演算法(一):產生與儲存 RSA 的金鑰(2017-07-19 調整)
  4. Create jwt in java using Public key rsa
  5. Digital Signature with RSASSA-PKCS1-v1_5

2020年11月18日 星期三

AES 加密 - JAVA 程式碼範例

紀錄一下 Java 使用 AES (Advanced Encryption Standard)  加解密的範例程式碼:

import java.io.UnsupportedEncodingException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class AESTest {

	public static void main(String[] args) {
		try {
			String seedForRandom = "TestSeed";
			String plaintext = "TestPlaintext";
			
			KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
			// 設定特定的隨機數種子,
			// 如隨機數種子不變,就會得到相同的隨機數,
			// 這裡相當於每次都使用同樣的金鑰
			SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
			secureRandom.setSeed(seedForRandom.getBytes("UTF-8"));
			//產生 AES 金鑰
			keyGenerator.init(256, secureRandom);
			SecretKey secretKey = keyGenerator.generateKey();
			SecretKey AES_secretKey = new SecretKeySpec(secretKey.getEncoded(), "AES");
			// 取得 AES 加解密器
			Cipher cipher = Cipher.getInstance("AES");
			
			//進行加密
			cipher.init(Cipher.ENCRYPT_MODE, AES_secretKey);
			byte[] encryptedText_byteArray = cipher.doFinal(plaintext.getBytes("UTF-8"));
			String encryptedText = new String(encryptedText_byteArray, "UTF-8");
			System.out.println(encryptedText); // W?\?? ???	U]?
			
			//對加密後的 byte[] 進行解密 (再取得一個新的 AES 加解密器來做)
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			byte[] decryptedText_byteArray = cipher.doFinal(encryptedText_byteArray);
			String decryptedText = new String(decryptedText_byteArray, "UTF-8");
			System.out.println(decryptedText); // TestPlaintext
			
			//注意!
			//加密後的 byte[] (decryptedText_byteArray) 不能與 String 互轉,
			//會流失一些東西,即上述的 encryptedText 會有轉不回 decryptedText_byteArray 的情況,
			//如果直接拿 encryptedText 來解密會出錯,例如以下例子會產生如下錯誤: 
			//javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
			//cipher = Cipher.getInstance("AES");
			//cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			//byte[] decryptedText_byteArray2 = cipher.doFinal(encryptedText.getBytes("UTF-8"));
			// ==> Execption : javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
			
			//如上所述,替代方案可使用 base64 編碼,
			//因為加密後的 byte[] (即 decryptedText_byteArray) 可用 base64 編碼來在 byte[] 和 String 之間互轉,
			//如下所示:
			String base64Encoded_encryptedTextcryptedText = Base64.encodeBase64String(encryptedText_byteArray);
			System.out.println(base64Encoded_encryptedTextcryptedText); // V79c5vQIIP/R2wcWCVVdqA==
			
			cipher = Cipher.getInstance("AES");
			cipher.init(Cipher.DECRYPT_MODE, AES_secretKey);
			byte[] decryptedText_byteArray2 = cipher.doFinal(Base64.decodeBase64(base64Encoded_encryptedTextcryptedText));
			String decryptedText2 = new String(decryptedText_byteArray2, "UTF-8");
			System.out.println(decryptedText2); // TestPlaintext
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (BadPaddingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

}


參考:

  1. 進階加密標準(Advanced Encryption Standard,AES)
  2. JAVA AES加密與解密
  3. Input length must be multiple of 16 when decrypting with padded cipher