作者:張京 鏈接:https://segmentfault.com/a/1190000019437132 還記得上初二的那年夏天,班里來了一個新同學,他就住在我家對面的樓里,于是我們一起上學放學,很快便成了最要好的朋友。我們決定發(fā)明一套神秘的溝通方式,任何人看到都不可能猜到它的真實含義。我們第一個想到的就是漢語拼音,但很顯然光把一個句子變成漢語拼音是不夠的,于是我們把26個英文字母用簡譜的方式從低音到高音排起來,就得到了一個簡單的密碼本: 
把“ 我們都是好朋友”用這個密碼本變換之后就得到了這樣的結(jié)果: 
小時候玩這個游戲樂此不疲,覺得非常有趣。上大學后,有幸聽盧開澄教授講《計算機密碼學》,才知道原來我們小時候玩的這個游戲遠遠不能稱之為加密。那么到底什么是加密呢? 什么是加密?把字符串 123456經(jīng)過 base64變換之后,得到了 MTIzNDU2,有人說這是 base64加密。
把字符串 123456經(jīng)過 md5變換之后,得到了 E10ADC3949BA59ABBE56E057F20F883E,有人說這是 md5加密。 從嚴格意義上來說,不管是 base64還是 md5甚至更復雜一些的 sha256都不能稱之為加密。 一句話,沒有密鑰的算法都不能叫加密。 編碼(Encoding)是把字符集中的字符編碼為指定集合中某一對象(例如:比特模式、自然數(shù)序列、8位字節(jié)或者電脈沖),以便文本在計算機中存儲和通過通信網(wǎng)絡的傳遞的方法,常見的例子包括將拉丁字母表編碼成摩爾斯電碼和 ASCII。 base64只是一種編碼方式。 雜湊(Hashing)是電腦科學中一種對資料的處理方法,通過某種特定的函數(shù)/算法(稱為雜湊函數(shù)/算法)將要檢索的項與用來檢索的索引(稱為雜湊,或者雜湊值)關聯(lián)起來,生成一種便于搜索的資料結(jié)構(gòu)(稱為雜湊表)。雜湊算法常被用來保護存在資料庫中的密碼字符串,由于雜湊算法所計算出來的雜湊值具有不可逆(無法逆向演算回原本的數(shù)值)的性質(zhì),因此可有效的保護密碼。常用的雜湊算法包括 md5, sha1, sha256等。 加密(Encryption)是將明文信息改變?yōu)殡y以讀取的密文內(nèi)容,使之不可讀的過程。只有擁有解密方法的對象,經(jīng)由解密過程,才能將密文還原為正??勺x的內(nèi)容。加密分為對稱加密和非對稱加密,對稱加密的常用算法包括 DES, AES等,非對稱加密算法包括 RSA,橢圓曲線算法等。
在古典加密算法當中,加密算法和密鑰都是不能公開的,一旦泄露就有被破解的風險,我們可以用詞頻推算等方法獲知明文。 1972年美國 IBM公司研制的 DES算法( Data Encryption Standard)是人類歷史上第一個公開加密算法但不公開密鑰的加密方法,后來成為美國軍方和政府機構(gòu)的標準加密算法。 2002年升級成為 AES算法( AdvancedEncryption Standard),我們今天就從 AES開始入手學習加密和解密。 準備工具通常情況下,加解密都只需要在服務端完成就夠了,這也是網(wǎng)上大多數(shù)教程和樣例代碼的情況,但在某種特殊情況下,你需要用一種語言加密而用另一種語言解密的時候,最好有一個中立的公正的第三方結(jié)果集來驗證你的加密結(jié)果,否則一旦出錯,你都不知道是加密算法出錯了,還是解密算法出錯了,對此我們是有慘痛教訓的,特別是如果一個公司里,寫加密的是前端,用的是 js語言,而寫解密的是后端,用的是 java語言或者 php語言或者 go語言,則雙方更需要有這樣一個客觀公正的平臺,否則你們之間必然會陷入永無休止的互相指責的境地,前端說自己沒有錯,是后端解密解錯了,后端說解密沒有錯,是前端加密寫錯了,而事實上是雙方都是菜鳥,對密碼學一知半解,在這種情況下浪費的時間就更多。
在線AES加密解密就是這樣的一個工具網(wǎng)站,你可以在上面驗證你的加密結(jié)果,如果你加密得到的結(jié)果和它的結(jié)果完全一致,就說明你的加密算法沒有問題,否則你就去調(diào)整,直到和它的結(jié)果完全一致為止。反之亦然,如果它能從一個密文解密解出來,而你的代碼解不出來,那么一定是你的算法有問題,而不可能是數(shù)據(jù)的問題。 我們先在這個網(wǎng)站上對一個簡單的字符串 123456進行加密。 
下面我們對網(wǎng)站上的所有選項逐個解釋一下: AES加密模式:這里我們選擇的是 ECB( ee cc block)模式。這是 AES所有模式中最簡單也是最不被人推薦的一種模式,因為它的固定的明文對應的是固定的密文,很容易被破解。但是既然是練習的話,就讓我們先從最簡單的開始。
填充:在這里我們選擇 pkcs標準的 pkcs7padding。 數(shù)據(jù)塊:我們選擇 128位,因為 java端解密算法目前只支持 AES128,所以我們先從 128位開始。 密鑰:因為我們前面選擇了 128位的數(shù)據(jù)塊,所以這里我們用 128 / 8 = 16個字節(jié)來處理,我們先簡單地填入 16個0,其實你也可以填寫任意字符,比如 abcdefg1234567ab或者其它,只要是 16個字節(jié)即可。理論上來說,不是16個字節(jié)也可以用來當密鑰,優(yōu)秀的算法會自動補齊,但是為了簡單起見,我們先填入 16個 0。 偏移量:置空。因為是 ECB模式,不需要 iv偏移量。 輸出:我們選擇 base64編碼方式。 字符集:這里因為我們只加密英文字母和阿拉伯數(shù)字,所以選擇 utf-8和 gb2312都是一樣的。
好了,現(xiàn)在我們知道按照以上選項設置好之后的代碼如果加密 123456的話,應該輸出 DoxDHHOjfol/2WxpaXAXgQ==,如果不是這個結(jié)果,那就是加密端的問題。 AES-ECBAES-ECB的Javascript加密
為了完成 AES加密,我們并不需要自己手寫一個 AES算法,不需要去重復造輪子。但如何選擇 js的加密庫是個很有意思的挑戰(zhàn)。我們嘗試了很多方法,一開始我們嘗試了aes-js這個庫,但它不支持 RSA算法,后來我們看到Web Crypto API這種瀏覽器自帶的加密庫,原生支持 AES和 RSA,但它的 RSA實現(xiàn)和 Java不兼容,最終我們還是選擇了Forge這個庫,它天生支持 AES的各種子集,并且它的 RSA也能和 Java完美配合。 使用 forge編寫的 js代碼實現(xiàn) AES-ECB加密的代碼就是下面這些: const cipher = forge.cipher.createCipher('AES-ECB', '這里是16字節(jié)密鑰'); cipher.start(); cipher.update(forge.util.createBuffer('這里是明文')); cipher.finish(); const result = forge.util.encode64(cipher.output.getBytes())
forge的 AES缺省就是 pkcs7padding,所以不用特別設置。運行它之后你就會得到正確的加密結(jié)果。
AES-ECB的Java解密接下來我們看看Java端的解密代碼該如何寫: try { Cipher cipher = Cipher.getInstance('AES/ECB/PKCS5Padding'); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec('這里是16字節(jié)密鑰'.getBytes(), 'AES')); String plaintext = new String(cipher.doFinal(Base64.getDecoder().decode('這里是明文'.getBytes())), 'UTF-8'); System.out.println(plaintext); } catch (Exception e) { System.out.println('解密出錯:' + e.toString()); }
注意這里我們用到的是 PKCS5Padding,上面加密的時候不是用的是 pkcs7padding嗎?怎么這里變成 5了呢? 我們先來了解一下什么是 pkcs。 pkcs的全稱是 Public Key Cryptography Standards(公鑰加密標準),這是 RSA實驗室制定的一系列的公鑰密碼編譯標準,比較著名的有 pkcs1, pkcs5, pkcs7, pkcs8這四個,它們分別管理的是不同的內(nèi)容。在這里我們只是用它來填充,所以我們只關注 pkcs5和 pkcs7就夠了。那么 pkcs5和 pkcs7有什么區(qū)別呢?其實在填充方面它們兩個的算法是一樣的, pkcs5是 pkcs7的一個子集,區(qū)別在于 pkcs5是 8字節(jié)固定的,而 pkcs7可以是 1到 255之間的任意字節(jié)。但用在 AES算法上,因為 AES標準規(guī)定塊大小必須是 16字節(jié)或者 24字節(jié)或者 32字節(jié),不可能用 pkcs5的 8字節(jié),所以 AES算法只能用 pkcs7填充。但是由于 java早期工程師犯的一個命名上的錯誤,他們把 AES填充算法的名稱設定為 pkcs5,而實際實現(xiàn)中實現(xiàn)的是 pkcs7,所以我們在 java端開發(fā)解密的時候需要使用 pkcs5。 AES-CBC談完了不安全的 AES-ECB,我們來做一下相對安全一些的 AES-CBC模式。
AES-CBC的Javascript加密直接上代碼: const cipher = forge.cipher.createCipher('AES-CBC', '這里是16字節(jié)密鑰'); cipher.start({ iv: '這里是16字節(jié)偏移量' }); cipher.update(forge.util.createBuffer('這里是明文')); cipher.finish(); const result = forge.util.encode64(cipher.output.getBytes());
跟上面的 AES-ECB差不多,唯一區(qū)別只是在 start函數(shù)里定義了一個 iv。 AES-CBC的Java解密下面是 Java代碼: try { Cipher cipher = Cipher.getInstance('AES/CBC/PKCS5Padding'); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec('這里是16字節(jié)密鑰'.getBytes(), 'AES'), new IvParameterSpec('這里是16字節(jié)偏移量'.getBytes())); String plaintext = new String(cipher.doFinal(Base64.getDecoder().decode('這里是明文'.getBytes())), 'UTF-8'); System.out.println(plaintext); } catch (Exception e) { System.out.println('解密出錯:' + e.toString()); }
也是同樣,跟上面用 AES-ECB時的模式幾乎一模一樣,只是增加了一個 IvParameterSpec,用來生成 iv,在 cipher.init里面增加了一個 iv參數(shù),除此之外完全相同,就這樣我們就已經(jīng)實現(xiàn)了一個簡單的 CBC模式。 RSA但是以上兩種做法都明顯是非常不安全的,因為我們把加密用的密鑰和 iv參數(shù)都直接暴露在了前端,為此我們需要一種更加安全的加密方法—— RSA。因為 RSA是非對稱加密,即使我們把加密用的公鑰完全暴露在前端也不必擔心,別人即使截獲了我們的密文,但因為他們沒有解密密鑰,是無法解出我們的明文的。
生成密鑰對要用 RSA加密,首先我們需要生成一個公鑰和一個私鑰,我們可以直接執(zhí)行命令 ssh-keygen。它會問我們密鑰文件保存的文件夾,注意一定要單獨找一個文件夾存放,不要放在缺省文件夾下,否則你日常使用的 ssh公鑰和私鑰就都被覆蓋了。 得到公鑰文件之后,由于這個公鑰文件是 rfc4716格式的,而我們的 forge庫要求一個 pkcs1格式的公鑰,所以這里我們需要把它轉(zhuǎn)換成 pem格式(也就是 pkcs1格式): ssh-keygen -f 公鑰文件名 -m pem -e
RSA的Javascript加密得到 pem格式的公鑰之后,我們來看一下 js的代碼: forge.util.encode64(forge.pki.publicKeyFromPem('-----BEGIN RSA PUBLIC KEY-----MIIBCfdsafasfasfafsdaafdsaAB-----END RSA PUBLIC KEY-----').encrypt('這里是明文', 'RSA-OAEP', { md: forge.md.sha256.create(), mgf1: { md: forge.md.sha1.create() } });
一句話就完成整個加密過程了,這就是 forge的強大之處。 RSA的Java解密接下來我們看解密。 對于私鑰,因為 Java只支持 PKCS8,而我們用 ssh-keygen生成的私鑰是 pkcs1的,所以還需要用以下命令把 pkcs1的私鑰轉(zhuǎn)換為 pkcs8的私鑰: openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in 私鑰文件名 -out 導出文件名
得到 pkcs8格式的私鑰之后,我們把這個文件的頭和尾去掉,然后放入以下 Java代碼: try { Cipher cipher = Cipher.getInstance('RSA/ECB/OAEPWithSHA-256AndMGF1Padding'); cipher.init(Cipher.DECRYPT_MODE, KeyFactory.getInstance('RSA').generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode('這里是私鑰')))); String plaintext = new String(cipher.doFinal(Base64.getDecoder().decode('這里是密文'.getBytes())), 'UTF-8'); System.out.println(plaintext); } catch (Exception e) { System.out.println('解密出錯:' + e.toString()); }
和上面的 AES解密類似,只是增加了 KeyFactory讀取 PKCS8格式私鑰的部分,這樣我們就完成了 Java端的 RSA解密。 以上我們用最簡單的方式實現(xiàn)了 js端加密, java端解密的過程,感興趣的朋友可以在這里下載完整的代碼親自驗證一下: https://github.com/fengerzh/encdec
●編號891,輸入編號直達本文
●輸入m獲取文章目錄
|