還記得上初二的那年夏天,班裡來了一個新同學,他就住在我家對面的樓裡,於是我們一起上學放學,很快便成了最要好的朋友。我們決定發明一套神祕的溝通方式,任何人看到都不可能猜到它的真實含義。我們第一個想到的就是漢語拼音,但很顯然光把一個句子變成漢語拼音是不夠的,於是我們把26個英文字母用簡譜的方式從低音到高音排起來,就得到了一個簡單的密碼本:
把“我們都是好朋友
”用這個密碼本變換之後就得到了這樣的結果:
小時候玩這個遊戲樂此不疲,覺得非常有趣。上大學後,有幸聽盧開澄教授講《計算機密碼學》,才知道原來我們小時候玩的這個遊戲遠遠不能稱之為加密。那麼到底什麼是加密呢?
什麼是加密?
把字串123456
經過base64
變換之後,得到了MTIzNDU2
,有人說這是base64
加密。
把字串123456
經過md5
變換之後,得到了E10ADC3949BA59ABBE56E057F20F883E
,有人說這是md5
加密。
從嚴格意義上來說,不管是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-ECB
AES-ECB的Javascript加密
為了完成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
,所以不用特別設定。執行它之後你就會得到正確的加密結果。
AES-ECB的Java解密
接下來我們看看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-CBC
談完了不安全的AES-ECB
,我們來做一下相對安全一些的AES-CBC
模式。
AES-CBC的Javascript加密
直接上程式碼:
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
。
AES-CBC的Java解密
下面是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
模式。
RSA
但是以上兩種做法都明顯是非常不安全的,因為我們把加密用的金鑰和iv
引數都直接暴露在了前端,為此我們需要一種更加安全的加密方法——RSA
。因為RSA
是非對稱加密,即使我們把加密用的公鑰完全暴露在前端也不必擔心,別人即使截獲了我們的密文,但因為他們沒有解密金鑰,是無法解出我們的明文的。
生成金鑰對
要用RSA
加密,首先我們需要生成一個公鑰和一個私鑰,我們可以直接執行命令ssh-keygen
。它會問我們金鑰檔案儲存的資料夾,注意一定要單獨找一個資料夾存放,不要放在預設資料夾下,否則你日常使用的ssh
公鑰和私鑰就都被覆蓋了。
得到公鑰檔案之後,由於這個公鑰檔案是rfc4716
格式的,而我們的forge
庫要求一個pkcs1
格式的公鑰,所以這裡我們需要把它轉換成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
的私鑰轉換為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
端解密的過程,感興趣的朋友可以在這裡下載完整的程式碼親自驗證一下: