RSA算法,加密/解密和簽名/驗籤

1、概述

非對稱加密算法

加密和解密用的密鑰是不同的,這種加密方式是用數學上的難解問題構造的,通常加密解密的速度比較慢,適合偶爾發送數據的場合。優點是密鑰傳輸方便。常見的非對稱加密算法爲RSA、ECC和EIGamal。

具體詳細分析參考文章Java 實現 RSA 非對稱加密算法

2、工具類

public class RSAUtils {

	private static final String PROVIDER_NAME = "BC";

	public final static String MAP_KEY_PUBLIC_KEY = "publicKey";
	public final static String MAP_KEY_PRIVATE_KEY = "privateKey";

	/** 數字簽名算法:SHA1withRSA **/
	public final static String SIGNATURE_ALGORITHM_SHA1WITHRSA = "SHA1withRSA";
	/** 數字簽名算法:MD5withRSA **/
	public final static String SIGNATURE_ALGORITHM_MD5WITHRSA = "MD5withRSA";

	/** 加密算法RSA **/
	public static final String KEY_ALGORITHM = "RSA";

	public static final String TRANSFORMATION_PKCS1PADDING = "RSA/ECB/PKCS1Padding";
	public static final String TRANSFORMATION_NOPADDING = "RSA/ECB/NOPADDING";

	/**
	 * 生成RSA公私鑰對
	 * Map.keySet() 
	 */
	public static Map<String, String> generateRSAKeyPlain() {
		Map<String, String> map = new HashMap<String, String>();
		try {
			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);
			KeyPair keyPair = keyPairGenerator.generateKeyPair();
			map.put(MAP_KEY_PUBLIC_KEY, getPublicKeyPlain(keyPair.getPublic()));
			map.put(MAP_KEY_PRIVATE_KEY, getPrivateKeyPlain(keyPair.getPrivate()));
		} catch (NoSuchAlgorithmException e) {
			System.err.println("無此算法");
			e.printStackTrace();
		}
		return map;
	}

	/**
	 * RSA加密
	 * @param key  公鑰
	 * @param data 數據
	 */
	public static byte[] encrypt(Key key, byte[] data) throws Exception {
		return encrypt(key, "RSA/ECB/PKCS1Padding", data);
	}

	/**
	 * RSA加密
	 * @param key  加密
	 * @param   算法
	 * @param data 數據
	 */
	public static byte[] encrypt(Key key, String transformation, byte[] data) throws Exception {
		try {
			Cipher cipher = Cipher.getInstance(transformation, PROVIDER_NAME);
			cipher.init(Cipher.ENCRYPT_MODE, key);
			int blockSize = cipher.getBlockSize();
			int outputSize = cipher.getOutputSize(data.length);
			int leavedSize = data.length % blockSize;
			int blocksSize = leavedSize != 0 ? data.length / blockSize + 1
					: data.length / blockSize;
			byte[] raw = new byte[outputSize * blocksSize];
			int i = 0;
			while (data.length - i * blockSize > 0) {
				if (data.length - i * blockSize > blockSize) {
					cipher.doFinal(data, i * blockSize, blockSize, raw, i
							* outputSize);
				} else {
					cipher.doFinal(data, i * blockSize, data.length - i
							* blockSize, raw, i * outputSize);
				}
				i++;
			}
			return raw;
		} catch (Exception e) {
			throw e;
		}
	}

	/**
	 * RSA解密
	 */
	public static byte[] decrypt(Key key, byte[] data) throws Exception {
		return decrypt(key, "RSA/ECB/PKCS1Padding", data);
	}

	/**
	 * RSA解密
	 */
	public static byte[] decrypt(Key key, String transformation, byte[] data) throws Exception {
		try {
			Cipher cipher = Cipher.getInstance(transformation, PROVIDER_NAME);
			cipher.init(Cipher.DECRYPT_MODE, key);
			int blockSize = cipher.getBlockSize(); // 密文數據分塊
			if (data.length % blockSize != 0)
				throw new AppBizException("RSA descrypt:unexpected data length:" + data.length);
			int blocks = data.length / blockSize; // 獲得分塊數據
			int inputOffset = 0;
			int outputOffset = 0;
			byte[] buffer = new byte[blocks * blockSize];
			for (int i = 0; i < blocks; i++) {
				int hasDone = cipher.doFinal(data, inputOffset, blockSize,
						buffer, outputOffset);
				outputOffset += hasDone;
				inputOffset += blockSize;
			}
			return ArrayUtils.subarray(buffer, 0, outputOffset);
		} catch (Exception e) {
			throw e;
		}
	}

	/**
	 * 數據簽名驗籤
	 * @param publicKey     公鑰
	 * @param algorithm    數字簽名算法 "MD5withRSA","SHA1withRSA" 等
	 * @param data   數據
	 * @param signData 驗籤數據
	 */
	public static boolean verify(PublicKey publicKey, String algorithm, byte[] data, byte[] signData) throws NoSuchAlgorithmException,
            InvalidKeyException, SignatureException {
		Signature signature = Signature.getInstance(algorithm);
		signature.initVerify(publicKey);
		signature.update(data);
		return signature.verify(signData);
	}

	/**
	 * 進行數字簽名
	 * @param privateKey    私鑰
	 * @param data     簽名數據
	 * @param algorithm    數字簽名算法 "MD5withRSA","SHA1withRSA" 等
	 */
	public static byte[] sign(PrivateKey privateKey, byte[] data,String algorithm) throws NoSuchAlgorithmException,
            InvalidKeyException, SignatureException {
		Signature signature = Signature.getInstance(algorithm);
		signature.initSign(privateKey);
		signature.update(data);
		return signature.sign();
	}

	/**
	 * 獲取公鑰Base64編碼的字符串
	 */
	public static String getPublicKeyPlain(PublicKey publicKey) {
		byte[] pbk = publicKey.getEncoded();
		return Base64.encodeBase64String(pbk);
	}

	/**
	 * 獲取私鑰Base64編碼的字符串
	 */
	public static String getPrivateKeyPlain(PrivateKey privateKey) {
		byte[] prk = privateKey.getEncoded();
		return Base64.encodeBase64String(prk);
	}

	/**
	 * Base64編碼的字符串 轉 公鑰對象
	 */
	public static PublicKey loadPublicKey(String publicKeyStr) {
		byte[] buffer = Base64.decodeBase64(publicKeyStr);
		PublicKey publicKey = null;
		try {
			KeyFactory keyFactory = KeyFactory.getInstance("RSA");
			X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer);
			publicKey = keyFactory.generatePublic(keySpec);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("無此算法");
			e.printStackTrace();
		} catch (InvalidKeySpecException e) {
			System.err.println("非法公鑰");
			e.printStackTrace();
		}
		return publicKey;
	}

	/**
	 * Base64編碼的字符串 轉 私鑰對象
	 */
	public static PrivateKey loadPrivateKey(String privateKeyStr) {
		PrivateKey privateKey = null;
		try {
			byte[] buffer = Base64.decodeBase64(privateKeyStr);
			PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
			KeyFactory keyFactory = KeyFactory.getInstance("RSA");
			privateKey = keyFactory.generatePrivate(keySpec);
		} catch (NoSuchAlgorithmException e) {
			System.err.println("無此算法");
			e.printStackTrace();
		} catch (InvalidKeySpecException e) {
			System.err.println("非法私鑰");
			e.printStackTrace();
		}
		return privateKey;
	}
}

3、RSA 加密與解密數據

RSA 非對稱加密在使用中通常公鑰公開,私鑰保密,使用公鑰加密,私鑰解密。例如 客戶端 給 服務端 加密發送數據:

  • 客戶端從服務端獲取公鑰;
  • 客戶端用公鑰先加密要發送的數據,加密後發送給服務端;
  • 服務端拿到加密後的數據,用私鑰解密得到原文。

公鑰加密後的數據,只有用私鑰才能解,只有服務端纔有對應的私鑰,因此只有服務端能解密,中途就算數據被截獲,沒有私鑰依然不知道數據的原文內容,因此達到數據安全傳輸的目的

  Map<String,String> keyMap = RSAUtils.generateRSAKeyPlain();
  keyMap.get(RSAUtils.MAP_KEY_PRIVATE_KEY)
  keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY)
  
  String data = "你好, World!";
  // 客戶端: 加密
  byte[] cipherData = RSAUtils.encrypt(keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY), data.getBytes());
 
  // 服務端: 解密
  byte[] plainData = RSAUtils.decrypt(keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY), cipherData);

4、簽名與驗籤

RSA 非對稱加密算法,除了用來加密/解密數據外,還可以用於對數據(文件)的 簽名 和 驗籤,可用於確認數據或文件的完整性與簽名者(所有者)

RSA 密鑰對在使用時通常:

  • 加密/解密:通常使用 公鑰加密,私鑰解密。
  • 簽名/驗籤:通常使用 私鑰簽名,公鑰驗籤。

Android 安裝包 APK 文件的簽名,是 RSA 簽名驗籤的典型應用:Android 打包後,用私鑰對 APK 文件進行簽名,並把公鑰和簽名結果放到 APK 包中。下次客戶端升級 APK 包時,根據新的 APK 包和包內的簽名信息,用 APK 包內的公鑰驗籤校驗是否和本地已安裝的 APK 包使用的是同一個私鑰簽名,如果是,則允許安裝升級。

 Map<String,String> keyMap = RSAUtils.generateRSAKeyPlain();
 keyMap.get(RSAUtils.MAP_KEY_PRIVATE_KEY)
 keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY)
 
 File file = new File("demo.jpg");

// 私鑰簽名(文件): 對文件進行簽名, 返回簽名結果
byte[] fileSignInfo = RSAUtils.sign(file.getBytes(), keyMap.get(RSAUtils.MAP_KEY_PRIVATE_KEY),"SHA1withRSA");
System.out.println("文件簽名信息:" + new BASE64Encoder().encode(fileSignInfo));

// 公鑰驗籤(文件): 用公鑰校驗文件的簽名是否來自公鑰對應的私鑰
boolean fileVerify = RSAUtils.verify(keyMap.get(RSAUtils.MAP_KEY_PUBLIC_KEY),"SHA1withRSA",file.getBytes(), signInfo);
System.out.println("文件驗簽結果:" + fileVerify);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章