TL;DR: 上一篇文章我們介紹了對稱加密算法,其最主要的特點就是加密者和解密者持有相同的密鑰,所以稱之爲對稱
。照理推想,有對稱就有非對稱。這篇文章我們來介紹另外一個重要的加密算法:非對稱加密算法 (Asymmetric Cryptography), 也稱爲公開密鑰加密算法 (Public Key Cryptography).
公開密鑰算法概要
首先,跟對稱密鑰算法一樣,非對稱密鑰算法不是指一個算法,而是一種算法。這類算法與對稱加密算法相比較,有如下的特點:
-
密鑰是一對
在上文的對稱加密算法我們看到,密鑰是一串數字或者字符串,加密者和解密者使用相同的密鑰進行加解密。公開密鑰算法則不同,它的密鑰是一對的,分成公鑰 - public key 和私鑰 - private key。一般私鑰是由密鑰對的生成者持有,比如服務器端,不能泄露。而公鑰是任何人都可以持有,是公開發布的,不怕泄露。由此這個算法而得到
公開密鑰
算法的名號。 -
功能不一樣
對稱加密算法的主要功能是加密和解密,而公開密鑰算法的功能除了加密和解密,還可用於密鑰協商,數字簽名,是數字證書, HTTPS等的最核心基礎。
-
運行速度慢
相對於對稱加密算法,公開密鑰算法由於其基礎運算是指數運算再求餘,而爲了安全,指數一般是一個比較大的數值,所以其運算非常緩慢,而且由於算法的侷限,一次加密的明文塊很小,所以如果要加密一個很大的明文,比如一個文件的話,那性能是慘不忍睹的。所以一般情況下,會根據使用場景,只用公開密鑰算法來加密對密鑰保存要求更高的數據,而不是全部都用公開密鑰算法來加密。
RSA 算法
現在我們來介紹第一個公開密鑰算法,也是比較常用而且重要的一個算法,叫RSA。 該算法是由Ron Rivest, Adi Shamir, Leenard Aldeman三個人創建的,以他們三個人的首字母來命名RSA。
RSA算法原理
RSA算法的設計用到的數學知識很多,RSA會用到質數,互質數,公約數定理,歐幾里得算法,同餘和模求解,唯一質數分解定理,歐拉函數,歐拉定理和費馬定理等。受限於時間和數學能力,這裏就不一一展開了,有興趣的可以參考這篇文章 https://www.jianshu.com/p/6aa7b59be872
公私鑰的生成
-
生成兩個不相等的大質數p和q,它們的積 ,這個n的二進制位數就是密鑰的位數,通常是1024, 2048, 4096.
-
計算p和q的乘積 , 以及歐拉公式
-
選擇一個整數e, 使得 , 且e和 是互質的, 即 . 在大多數RSA算法實現裏面,e固定位65537
-
計算 e 對於 的模反元素 d。即找到整數d,1 < d < φ,且滿足
-
把n 和e 封裝成公鑰,把n和d封裝成私鑰
n、e、d分別稱之爲:
- n : modulus 模數
- e: public exponent 公開指數
- d: private exponent 私有指數
所以我們得到RSA的公鑰 {n, e}, 私鑰 {n, d}
加解密的過程
有了公私鑰,我們就可以開始來看加解密的原理了。我們定義明文消息爲M,而加密的內容爲C,那麼加密的過程爲:計算明文消息的e次冪,然後與n求模:
而解密的過程爲: 計算密文的d次冪,然後與n求模:
具體的數學上的論證大家可以參考上面那篇文章,裏面比較詳細地介紹了論證過程。我們這裏只介紹具體的使用。
RSA算法的安全性
冪運算的逆過程是對數問題,而模運算可以認爲是離散問題,組合起來,RSA算法就是離散對數模型,只要密鑰足夠長,離散對數很難破解。密鑰的長度也就是n的二進制位數。大家都可以獲得公鑰{n,e}, 而要計算出私鑰d,那麼需要知道p和q。而想通過一個巨大的n(一般爲1024,2048 甚至4096位)獲得p和q是一個因式分解問題,也叫大素數分解問題,暴力破解很難。 因此只要密鑰足夠長,目前推薦2048位,RSA算法是很安全的。
RSA的實踐
現在我們來介紹如何具體地實踐RSA
-
公私鑰對的生成
首先是一個密碼學神奇OpenSSL,這是一個開源的軟件包,密碼學的算法基本都包了。我們現在用OpenSSL來生成一個公私鑰對:
λ openssl genrsa -out myrsa.pem 2048 Generating RSA private key, 2048 bit long modulus (2 primes) ............................+++++ ..+++++ e is 65537 (0x010001)
這時OpenSSL會生成一個pem格式的文件,裏面用ASCII的形式保存公私鑰的關鍵信息n,e,d,p,q. PEM( Privacy Enhanced Mail) 是RFCs 1421 https://tools.ietf.org/html/rfc1421 定義一個文件格式. 我們打myrsa.pem 文件,裏面顯示如下:
-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAt8rHReYR+jIr2tetc1AhrrZkfj7ewbu4K7XscVPGhlyYR8Uk s2vn6MXJwglGjN5ETJmMBJ4MMhLHaATtW2Zj9iwuyZNHJtBndFjrILNpmoF+nxVl ...... uBxc36lllGao2bN/EXcq+4yp4swQWfNVomK2kK7GQGONI9zokhJEfbRAb3Zxp0DM Fh85P7Hi50AVNWpZ2X+mCCaZt2gn8EB11G8r2BV/SPQTG6QqX+taXLQ= -----END RSA PRIVATE KEY-----
我們可以看看這裏面究竟都包含什麼信息:
openssl rsa -in myrsa.pem -noout -text
λ openssl rsa -in myrsa.pem -noout -text RSA Private-Key: (2048 bit, 2 primes) modulus: 00:b7:ca:c7:45:e6:11:fa:32:2b:da:d7:ad:73:50: 21:ae:b6:64:7e:3e:de:c1:bb:b8:2b:b5:ec:71:53: c6:86:5c:98:47:c5:24:b3:6b:e7:e8:c5:c9:c2:09: 46:8c:de:44:4c:99:8c:04:9e:0c:32:12:c7:68:04: ed:5b:66:63:f6:2c:2e:c9:93:47:26:d0:67:74:58: eb:20:b3:69:9a:81:7e:9f:15:65:5b:ae:23:91:b3: 10:0f:8a:0c:33:5b:cb:09:a1:4c:82:35:18:ea:0c: 6d:09:27:04:19:08:62:1d:5b:39:70:8f:5e:3c:55: de:01:ab:2c:33:2d:cd:ea:56:c5:81:66:7f:75:5a: 76:95:2e:29:61:28:e5:02:5d:f5:5c:37:1c:77:c5: 1b:f9:6f:36:b9:93:16:8b:de:bd:39:f1:93:58:0d: 10:34:22:5e:1f:ab:5b:fe:3f:84:e4:7d:9f:0c:06: 05:34:82:e8:fe:b8:e1:f0:6f:27:8a:fb:fe:b7:a7: e9:e7:04:e0:38:5c:41:c2:12:f9:e4:e8:3b:5e:2b: d5:30:1d:d6:7a:79:17:c0:93:f1:41:0a:9f:32:2a: 4d:37:2e:c6:5c:e0:a0:33:70:6e:41:d0:68:c3:4e: b6:c5:b1:46:fc:36:c9:3c:70:e0:95:4a:f0:83:c3: 09:81 publicExponent: 65537 (0x10001) privateExponent: 5e:00:31:81:57:95:94:40:7a:db:97:f9:d7:83:81: ... 72:c5:8d:90:bb:62:51:e1:b4:da:3d:4d:34:a6:c4: d1 prime1: 00:f0:b3:a7:ec:fe:11:ce:25:a7:b4:01:46:57:39: 09:39:a0:62:5b:06:f4:70:3b:9a:02:0c:6a:01:5e: 1f:69:16:51:7a:b4:03:34:09:99:13:5d:5e:c1:b8: 2d:92:73:6a:37:c6:66:5c:d8:02:6b:b7:41:58:3d: a3:f1:70:a3:1a:42:11:fb:dc:e4:79:61:c5:39:16: ea:d4:88:2a:f6:4c:c8:77:56:70:6e:e2:7f:14:f0: dd:46:e4:ce:5a:2b:37:fe:04:98:09:0b:be:d5:8b: 7b:18:5a:ad:2d:cc:9e:d2:0a:5f:2f:83:2e:48:6a: c8:99:3d:14:a7:46:4e:b3:6d prime2: 00:c3:79:2c:14:57:e0:db:6a:54:cd:96:ca:33:74: 47:e1:62:85:e3:15:82:3e:00:41:03:c6:a9:75:f2: 79:df:2c:86:41:1c:5a:09:ea:8b:51:45:f8:d7:0d: bf:45:c8:4d:4b:57:73:61:b1:76:cb:98:7a:f2:c9: 61:0f:c3:e9:9e:47:b1:09:76:ce:a0:4e:2f:17:e9: 81:f6:a9:d8:a5:cf:96:54:62:70:a2:17:fd:7d:ed: 09:59:09:f0:18:27:62:5a:97:59:29:b2:1f:57:fe: b3:86:55:a9:1c:a3:23:8d:33:13:76:42:55:88:c2: 88:f3:ad:37:3e:b9:40:0d:e5 exponent1: 00:db:e3:63:be:ef:03:99:0d:71:3c:d2:05:4e:5d: ... d2:08:9b:72:28:b5:e3:e3:a9 exponent2: 00:a8:11:9e:89:db:49:55:be:e6:2d:62:b2:76:6d: .... c4:d2:27:a3:f1:85:5c:82:d5 coefficient: 00:bf:a4:d2:39:09:7a:66:1a:30:8e:9b:7e:10:ea: ... 13:1b:a4:2a:5f:eb:5a:5c:b4
可以看到n, e,d 都在裏面,甚至原始數據p 和q也存在裏面。所以這個文件也是我們的私鑰,只不過裏面包含了公鑰的信息
接着我們從myrsa.pem裏面剝離出公鑰
λ openssl rsa -in myrsa.pem -pubout -out mypubkey.pem -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt8rHReYR+jIr2tetc1Ah rrZkfj7ewbu4K7XscVPGhlyYR8Uks2vn6MXJwglGjN5ETJmMBJ4MMhLHaATtW2Zj 9iwuyZNHJtBndFjrILNpmoF+nxVlW64jkbMQD4oMM1vLCaFMgjUY6gxtCScEGQhi HVs5cI9ePFXeAassMy3N6lbFgWZ/dVp2lS4pYSjlAl31XDccd8Ub+W82uZMWi969 OfGTWA0QNCJeH6tb/j+E5H2fDAYFNILo/rjh8G8nivv+t6fp5wTgOFxBwhL55Og7 XivVMB3WenkXwJPxQQqfMipNNy7GXOCgM3BuQdBow062xbFG/DbJPHDglUrwg8MJ gQIDAQAB -----END PUBLIC KEY-----
接着我們來看一下在Java中如何生成公私鑰對:
public class RSAUtil { static Logger logger = LoggerFactory.getLogger("RSAUtil"); public static void main(String[] args) throws Exception { KeyPair keyPair = generateKeyPair(1024); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); String publicKeyString = new String(Base64.getEncoder().encode(publicKey.getEncoded())); String privateKeyString = new String(Base64.getEncoder().encode(privateKey.getEncoded())); logger.info("generate {} bits public key, format {}, {}", publicKey.getModulus().bitLength(), publicKey.getFormat(), publicKeyString); logger.info("private key format {}, {}", privateKey.getFormat(), privateKeyString); public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(keySize); KeyPair keyPair = keyPairGenerator.generateKeyPair(); return keyPair; }
輸出爲:
18:11:53.604 [main] INFO RSAUtil - generate 1024 bits public key, format X.509, MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCP8+ujsUeg2hhgNSz1t8hoBHixoEy2tWVLo8Z22WC3LxqUfaGduvafIBlU9EYBU24ximn66N/AY4F9VzTKxVy3JmplIIiTptr+it5BMkJCO3YrsqPo6qKXHhpclvoc+YPfHB/8v13fmWlwI9aMCkI+mYF7m4V/gNouikBx8ZZcXQIDAQAB 18:11:53.604 [main] INFO RSAUtil - private key format PKCS#8, MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAI/z66OxR6DaGGA1LPW3yGgEeLGgTLa1ZUujxnbZYLcvGpR9oZ269p8gGVT0RgFTbjGKafro38BjgX1XNMrFXLcmamUgiJOm2v6K3kEyQkI7diuyo+jqopceGlyW+hz5g98cH/y/Xd+ZaXAj1owKQj6ZgXubhX+A2i6KQHHxllxdAgMBAAECgYAeIJqsg6nODFcVq4thUblrq6Pm6PmlM4mjrv8WWKBZNk6FzVVJwZtj6j/i+8y68k8ZpzJPBPXvOeQb62htF6kziUniqfEa78eoIwUbyPeMW6iOnPz8cSvMbDaKfR4GO6IufNajDQBG+8093+ILyU4eZiH8+UVDfGT1pdlpllkxsQJBAO+/sZXzZmjdBiP0xRQJJE9Jp6SonFxwqHXmRqK6Q84piOdAe9cnLxb+4v01UpndL8qHhtiEIOJV6+LPrb/GsW8CQQCZteacNMcYKsQqo+0vaCBTjqXYdbXbT8Pz7OMLTxctu/LuOepnl3IYLLQs+iW92n/Vw1LYBFd+7aNupNAk6xDzAkEA4DzqK3dBlNkNcjnwzsGSLXqViyONQ8S3O7bK4E7ZNo2Ql8KvUdg7agWyZuQlwvWnSoWiMQa7/xYgD77xIssDjwJAEdgEBW47DpsoWqrdBfvYhNqydgZ0Lhl8bfy5/r4Xur9u3CjtBUmXfSbzY6VGbFvJK0+ZdmpKnfmIV3fake6X8QJBANxWIz8EHPm0TWJAS6DSFLbo8XYbz5hcheUvRfEseE0TJoQDxW9RN5ikkpJusue3NaeeCuQe7RMZw4F440TuVy4=
- 公私鑰保存的標準
公開密鑰算法有一套標準叫Public Key Cryptgraphy Standards, 簡稱PKCS。 這套標準最早由RSA公司制定維護,目前交由標準化組織IETF(Internet Engineering Task Force)的 PKIX工作組來維護。這套標準從PKCS#1 到PKCS#14. 我們在對稱加密算法的補位中就用到PKCS#5 和PKCS#7的部分標準。
RSA的公鑰是一般是以X.509標準的格式進行保存的,如上面的Java例子,
publicKey.getFormat()
的結果是X.509
.而私鑰一般是以PKCS#8 (Private Key Information Syntax Standard)的格式保存. 如上面的例子中,
privateKey.getFormat()
的值爲 PKCS#8. -
加解密
Java中security包已經帶了RSA的實現,所以我們直接用Cipher類進行加載就行:
public static void main(String[] args) throws Exception { String publicKeyString = " MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCP8+ujsUeg2hhgNSz1t8hoBHixoEy2tWVLo8Z22WC3LxqUfaGduvafIBlU9EYBU24ximn66N/AY4F9VzTKxVy3JmplIIiTptr+it5BMkJCO3YrsqPo6qKXHhpclvoc+YPfHB/8v13fmWlwI9aMCkI+mYF7m4V/gNouikBx8ZZcXQIDAQAB" String privateKeyString = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAI/z66OxR6DaGGA1LPW3yGgEeLGgTLa1ZUujxnbZYLcvGpR9oZ269p8gGVT0RgFTbjGKafro38BjgX1XNMrFXLcmamUgiJOm2v6K3kEyQkI7diuyo+jqopceGlyW+hz5g98cH/y/Xd+ZaXAj1owKQj6ZgXubhX+A2i6KQHHxllxdAgMBAAECgYAeIJqsg6nODFcVq4thUblrq6Pm6PmlM4mjrv8WWKBZNk6FzVVJwZtj6j/i+8y68k8ZpzJPBPXvOeQb62htF6kziUniqfEa78eoIwUbyPeMW6iOnPz8cSvMbDaKfR4GO6IufNajDQBG+8093+ILyU4eZiH8+UVDfGT1pdlpllkxsQJBAO+/sZXzZmjdBiP0xRQJJE9Jp6SonFxwqHXmRqK6Q84piOdAe9cnLxb+4v01UpndL8qHhtiEIOJV6+LPrb/GsW8CQQCZteacNMcYKsQqo+0vaCBTjqXYdbXbT8Pz7OMLTxctu/LuOepnl3IYLLQs+iW92n/Vw1LYBFd+7aNupNAk6xDzAkEA4DzqK3dBlNkNcjnwzsGSLXqViyONQ8S3O7bK4E7ZNo2Ql8KvUdg7agWyZuQlwvWnSoWiMQa7/xYgD77xIssDjwJAEdgEBW47DpsoWqrdBfvYhNqydgZ0Lhl8bfy5/r4Xur9u3CjtBUmXfSbzY6VGbFvJK0+ZdmpKnfmIV3fake6X8QJBANxWIz8EHPm0TWJAS6DSFLbo8XYbz5hcheUvRfEseE0TJoQDxW9RN5ikkpJusue3NaeeCuQe7RMZw4F440TuVy4="; logger.info("generate {} bits public key, format {}, {}", publicKey.getModulus().bitLength(), publicKey.getFormat(), publicKeyString); logger.info("private key format {}, {}", privateKey.getFormat(), privateKeyString); String message = "I am Coco Cola!"; String cipherText = encrypt(publicKeyString, message); logger.info("plainText '{}' encrypted as: {}", message, cipherText); String plainText = decrypt(privateKeyString, cipherText); logger.info("cipherText '{}' decrypted as: {}", cipherText, plainText); } public static String encrypt(String publicKeyString, String message) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyString)); RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(publicKeySpec); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); byte[] encrypted = cipher.doFinal(message.getBytes()); return new String(Base64.getEncoder().encode(encrypted)); } public static String decrypt(String privateKeyString, String cipherText) throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyString)); PrivateKey priKey = KeyFactory.getInstance("RSA").generatePrivate(privateKeySpec); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, priKey); byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText.getBytes())); return new String(decrypted); }
運行可得到如下結果:
18:11:55.448 [main] INFO RSAUtil - plainText 'I am Coco Cola!' encrypted as: cnql6UxIv+4TFY8pjnfc+XhiOwzKZFeiTCvugDuKE23S22FFlil7WMlyBkwzw5lYHFjRAAEpwmlWUF5zoGoohEiUc9obcBPwWrwTc1YQ71Fe1vO9Xt5GzO0Dz+EcjocKMxCQ5JiRcbG/X3AGDgIWb/8J8tqn+NGh154y4tBeMk4=
18:11:55.464 [main] INFO RSAUtil - cipherText 'cnql6UxIv+4TFY8pjnfc+XhiOwzKZFeiTCvugDuKE23S22FFlil7WMlyBkwzw5lYHFjRAAEpwmlWUF5zoGoohEiUc9obcBPwWrwTc1YQ71Fe1vO9Xt5GzO0Dz+EcjocKMxCQ5JiRcbG/X3AGDgIWb/8J8tqn+NGh154y4tBeMk4=' decrypted as: I am Coco Cola!
RSA的使用及常見問題
- RSA的使用
由於RSA的原理是對明文計算e次冪(一般是65537次),或者對密文進行d次冪的運算,再求模。所以性能回是一個很大的問題,特別當大數據量的運算的時候,性能確實不敢恭維。所以一般是用於比較重要的信息才採用RSA算法,比如對於對稱密鑰的加密,HTTPS RSA密碼套件在進行3次握手的時候,客戶端會生成一個臨時密碼,並用服務器端的公鑰進行加密傳給服務端,服務端收到加密的密鑰之後,用它的私鑰進行解密,從而得到對稱加密的密鑰。
還有一種就是對要發送內容計算hash1,並對hash的內容進行用自己的RSA私鑰進行加密得到encryptedHash,然後把內容跟encryptedHash發給對方,接收方收到信息之後,統一對內容計算hash2值,同時用發送方的公鑰解密接收到的encryptedHash值,再將解密得到的decryptedHash值跟計算得到的hash2值進行比較,如果相同就認爲內容沒有篡改過,而且是認定的發送方發的。這也就是一種簽名算法的本質。
- 明文的大小限制
從RSA的加解密公式我們可以看出, .所以解密的時候,算出的值是要去mod n,既然是n的餘數,那就不能大於n。 如果明文的大於n,進行硬算,那麼解密就會算錯,算成求餘後的值,比如n是91,而明文是95的話,那麼解密後的值是4. 也就是所謂的迴繞問題。所以明文必須小於n。 非對稱密鑰算法跟對稱密鑰算法一樣,當明文內容小於n的時候,比如進行補位,而PKCS#1 (RSA Cryptograhy Standard)建議的補位是11個字節,所以 明文< n - 11*8。 比如公鑰1024位,那麼它能加密的明文大小爲 (1024 - 88 )/ 8 = 117字節。