一文詳解非對稱加密算法之RSA算法

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=pqn = pq ,這個n的二進制位數就是密鑰的位數,通常是1024, 2048, 4096.

  • 計算p和q的乘積n=pqn=pq , 以及歐拉公式 φ(n)=(p1)(q1)\varphi(n)= (p-1)(q-1)

  • 選擇一個整數e, 使得 1<e<φ(n)1 < e< \varphi(n), 且e和 φ(n)\varphi(n) 是互質的, 即 gcd(e,φ(n))=1gcd(e, \varphi(n))=1. 在大多數RSA算法實現裏面,e固定位65537

  • 計算 e 對於 φ(n)\varphi(n) 的模反元素 d。即找到整數d,1 < d < φ,且滿足 ed1(modφ(n))ed≡1(mod \varphi (n))

  • 把n 和e 封裝成公鑰,把n和d封裝成私鑰

n、e、d分別稱之爲:

  • n : modulus 模數
  • e: public exponent 公開指數
  • d: private exponent 私有指數

所以我們得到RSA的公鑰 {n, e}, 私鑰 {n, d}

加解密的過程

有了公私鑰,我們就可以開始來看加解密的原理了。我們定義明文消息爲M,而加密的內容爲C,那麼加密的過程爲:計算明文消息的e次冪,然後與n求模:

C=Memodn C= M^e\quad mod\quad n
而解密的過程爲: 計算密文的d次冪,然後與n求模:
M=Cdmodn M= C^d\quad mod\quad 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的加解密公式我們可以看出C=MemodnC= M^e\quad mod\quad nM=CdmodnM= C^d\quad mod\quad n .所以解密的時候,算出的值是要去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字節。

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