還記得上初二的那年夏天,班裏來了一個新同窗,他就住在我家對面的樓裏,因而咱們一塊兒上學放學,很快便成了最要好的朋友。咱們決定發明一套神祕的溝通方式,任何人看到都不可能猜到它的真實含義。咱們第一個想到的就是漢語拼音,但很顯然光把一個句子變成漢語拼音是不夠的,因而咱們把26個英文字母用簡譜的方式從低音到高音排起來,就獲得了一個簡單的密碼本:php
把「咱們都是好朋友
」用這個密碼本變換以後就獲得了這樣的結果:前端
小時候玩這個遊戲樂此不疲,以爲很是有趣。上大學後,有幸聽盧開澄教授講《計算機密碼學》,才知道原來咱們小時候玩的這個遊戲遠遠不能稱之爲加密。那麼到底什麼是加密呢?java
把字符串123456
通過base64
變換以後,獲得了MTIzNDU2
,有人說這是base64
加密。git
把字符串123456
通過md5
變換以後,獲得了E10ADC3949BA59ABBE56E057F20F883E
,有人說這是md5
加密。github
從嚴格意義上來講,不論是base64
仍是md5
甚至更復雜一些的sha256
都不能稱之爲加密。算法
一句話,沒有密鑰的算法都不能叫加密。後端
編碼(Encoding)是把字符集中的字符編碼爲指定集合中某一對象(例如:比特模式、天然數序列、8位字節或者電脈衝),以便文本在計算機中存儲和經過通訊網絡的傳遞的方法,常見的例子包括將拉丁字母表編碼成摩爾斯電碼和ASCII
。base64
只是一種編碼方式。雜湊(Hashing)是電腦科學中一種對資料的處理方法,經過某種特定的函數/算法(稱爲雜湊函數/算法)將要檢索的項與用來檢索的索引(稱爲雜湊,或者雜湊值)關聯起來,生成一種便於搜索的資料結構(稱爲雜湊表)。雜湊算法常被用來保護存在資料庫中的密碼字符串,因爲雜湊算法所計算出來的雜湊值具備不可逆(沒法逆向演算回本來的數值)的性質,所以可有效的保護密碼。經常使用的雜湊算法包括
md5
,sha1
,sha256
等。瀏覽器加密(Encryption)是將明文信息改變爲難以讀取的密文內容,使之不可讀的過程。只有擁有解密方法的對象,經由解密過程,才能將密文還原爲正常可讀的內容。加密分爲對稱加密和非對稱加密,對稱加密的經常使用算法包括
DES
,AES
等,非對稱加密算法包括RSA
,橢圓曲線算法等。安全
在古典加密算法當中,加密算法和密鑰都是不能公開的,一旦泄露就有被破解的風險,咱們能夠用詞頻推算等方法獲知明文。1972
年美國IBM
公司研製的DES
算法(Data Encryption Standard
)是人類歷史上第一個公開加密算法但不公開密鑰的加密方法,後來成爲美國軍方和政府機構的標準加密算法。2002
年升級成爲AES
算法(Advanced Encryption Standard
),咱們今天就從AES
開始入手學習加密和解密。網絡
一般狀況下,加解密都只須要在服務端完成就夠了,這也是網上大多數教程和樣例代碼的狀況,但在某種特殊狀況下,你須要用一種語言加密而用另外一種語言解密的時候,最好有一箇中立的公正的第三方結果集來驗證你的加密結果,不然一旦出錯,你都不知道是加密算法出錯了,仍是解密算法出錯了,對此咱們是有慘痛教訓的,特別是若是一個公司裏,寫加密的是前端,用的是js
語言,而寫解密的是後端,用的是java
語言或者php
語言或者go
語言,則雙方更須要有這樣一個客觀公正的平臺,不然大家之間必然會陷入永無休止的互相指責的境地,前端說本身沒有錯,是後端解密解錯了,後端說解密沒有錯,是前端加密寫錯了,而事實上是雙方都是菜鳥,對密碼學只知其一;不知其二,在這種狀況下浪費的時間就更多。
在線AES加密解密就是這樣的一個工具網站,你能夠在上面驗證你的加密結果,若是你加密獲得的結果和它的結果徹底一致,就說明你的加密算法沒有問題,不然你就去調整,直到和它的結果徹底一致爲止。反之亦然,若是它能從一個密文解密解出來,而你的代碼解不出來,那麼必定是你的算法有問題,而不多是數據的問題。
咱們先在這個網站上對一個簡單的字符串123456
進行加密。
下面咱們對網站上的全部選項逐個解釋一下:
AES
加密模式:這裏咱們選擇的是ECB
(ee cc block
)模式。這是AES
全部模式中最簡單也是最不被人推薦的一種模式,由於它的固定的明文對應的是固定的密文,很容易被破解。可是既然是練習的話,就讓咱們先從最簡單的開始。pkcs
標準的pkcs7padding
。128
位,由於java
端解密算法目前只支持AES128
,因此咱們先從128
位開始。128
位的數據塊,因此這裏咱們用128 / 8 = 16
個字節來處理,咱們先簡單地填入16
個0
,其實你也能夠填寫任意字符,好比abcdefg1234567ab
或者其它,只要是16
個字節便可。理論上來講,不是16
個字節也能夠用來當密鑰,優秀的算法會自動補齊,可是爲了簡單起見,咱們先填入16
個0
。ECB
模式,不須要iv
偏移量。base64
編碼方式。utf-8
和gb2312
都是同樣的。好了,如今咱們知道按照以上選項設置好以後的代碼若是加密123456
的話,應該輸出DoxDHHOjfol/2WxpaXAXgQ==
,若是不是這個結果,那就是加密端的問題。
爲了完成AES
加密,咱們並不須要本身手寫一個AES
算法,不須要去重複造輪子。但如何選擇js
的加密庫是個頗有意思的挑戰。咱們嘗試了不少方法,一開始咱們嘗試了aes-js這個庫,但它不支持RSA
算法,後來咱們看到Web Crypto API這種瀏覽器自帶的加密庫,原生支持AES
和RSA
,但它的RSA
實現和Java
不兼容,最終咱們仍是選擇了Forge這個庫,它天生支持AES
的各類子集,而且它的RSA
也能和Java
完美配合。
使用forge
編寫的js
代碼實現AES-ECB
加密的代碼就是下面這些:
const cipher = forge.cipher.createCipher('AES-ECB', '這裏是16字節密鑰'); cipher.start(); cipher.update(forge.util.createBuffer('這裏是明文')); cipher.finish(); const result = forge.util.encode64(cipher.output.getBytes())
forge
的AES
缺省就是pkcs7padding
,因此不用特別設置。運行它以後你就會獲得正確的加密結果。
接下來咱們看看Java端的解密代碼該如何寫:
try { Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec("這裏是16字節密鑰".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
這四個,它們分別管理的是不一樣的內容。在這裏咱們只是用它來填充,因此咱們只關注pkcs5
和pkcs7
就夠了。那麼pkcs5
和pkcs7
有什麼區別呢?其實在填充方面它們兩個的算法是同樣的,pkcs5
是pkcs7
的一個子集,區別在於pkcs5
是8
字節固定的,而pkcs7
能夠是1
到255
之間的任意字節。但用在AES
算法上,由於AES
標準規定塊大小必須是16
字節或者24
字節或者32
字節,不可能用pkcs5
的8
字節,因此AES
算法只能用pkcs7
填充。可是因爲java
早期工程師犯的一個命名上的錯誤,他們把AES
填充算法的名稱設定爲pkcs5
,而實際實現中實現的是pkcs7
,因此咱們在java
端開發解密的時候須要使用pkcs5
。
談完了不安全的AES-ECB
,咱們來作一下相對安全一些的AES-CBC
模式。
直接上代碼:
const cipher = forge.cipher.createCipher('AES-CBC', '這裏是16字節密鑰'); cipher.start({ iv: '這裏是16字節偏移量' }); cipher.update(forge.util.createBuffer('這裏是明文')); cipher.finish(); const result = forge.util.encode64(cipher.output.getBytes());
跟上面的AES-ECB
差很少,惟一區別只是在start
函數裏定義了一個iv
。
下面是Java
代碼:
try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec("這裏是16字節密鑰".getBytes(), "AES"), new IvParameterSpec("這裏是16字節偏移量".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
參數,除此以外徹底相同,就這樣咱們就已經實現了一個簡單的CBC
模式。
可是以上兩種作法都明顯是很是不安全的,由於咱們把加密用的密鑰和iv
參數都直接暴露在了前端,爲此咱們須要一種更加安全的加密方法——RSA
。由於RSA
是非對稱加密,即便咱們把加密用的公鑰徹底暴露在前端也沒必要擔憂,別人即便截獲了咱們的密文,但由於他們沒有解密密鑰,是沒法解出咱們的明文的。
要用RSA
加密,首先咱們須要生成一個公鑰和一個私鑰,咱們能夠直接執行命令ssh-keygen -m PEM
。它會問咱們密鑰文件保存的文件夾,注意必定要單獨找一個文件夾存放,不要放在缺省文件夾下,不然你平常使用的ssh
公鑰和私鑰就都被覆蓋了。
獲得公鑰文件以後,因爲這個公鑰文件是rfc4716
格式的,而咱們的forge
庫要求一個pkcs1
格式的公鑰,因此這裏咱們須要把它轉換成pem
格式(也就是pkcs1
格式):
ssh-keygen -f 公鑰文件名 -m pem -e
獲得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
的強大之處。
接下來咱們看解密。
對於私鑰,由於Java
只支持PKCS8
,而咱們用ssh-keygen
生成的私鑰是pkcs1
的,因此還須要用如下命令把pkcs1
的私鑰轉換爲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
解密。
以上咱們用最簡單的方式實現了js
端加密,java
端解密的過程,感興趣的朋友能夠在這裏下載完整的代碼親自驗證一下: