可以說的秘密-那些我們該討論的前端加密方法

作者:孫印鳳

隨著信息安全重要性的日益凸顯,如何保證用戶數據的安全成為開發者重點關注的內容。目前,可供我們選擇的加密方法有很多,我們需要根據實際的情況選擇符合自己的安全解決方案。本文將介紹前端開發中常用的加密方法并給出其適用場景。

常用加密方法

1. Base64 編碼

大家經常說的是 Base64 加密,有 Base64 加密嗎?真木有,只有 Base64 編碼。

Base64 是一種基于 64 個可打印字符來表示二進制數據的表示方法,詳見 [1]。常用于在通常處理文本數據的場合,表示、傳輸、存儲一些二進制數據,包括 MIME 的 email,email via MIME,在 XML 中存儲復雜數據;主要用來解決把不可打印的內容塞進可打印內容的需求。很多編程語言中都內置了該編碼方法,比如 Python 內置的 base64 方法使用如下:

import base64;
base64.b64encode('binary\x00string');
//'YmluYXJ5AHN0cmluZw=='
 
base64.b64decode('YmluYXJ5AHN0cmluZw==');
//'binary\x00string'

因此,Base64 適用于小段內容的編碼,比如數字證書簽名、Cookie的內容等;而且 Base64 也是一種通過查表的編碼方法,不能用于加密,如果需要加密,請使用專業的加密算法。

2. 哈希算法(Hash)

哈希(Hash)是將目標文本轉換成具有固定長度的字符串(或叫做消息摘要)。當輸入發生改變時,產生的哈希值也是完全不同的。從數學角度上講,一個哈希算法是一個多對一的映射關系,對于目標文本 T,算法 H 可以將其唯一映射為 R,并且對于所有的 T,R 具有相同的長度,所以 H 不存在逆映射,也就是說哈希算法是不可逆的。

基于哈希算法的特性,其適用于該場景:被保護數據僅僅用作比較驗證且不需要還原成明文形式。比較常用的哈希算法是 MD5 和 SHA1,詳見 [2][3]。

我們比較熟悉的使用哈希存儲數據的例子是:當我們登錄某個已注冊網站時,在忘記密碼的情況下需要重置密碼,此時網站會給你發一個隨機的密碼或者一個郵箱激活鏈接,而不是將之前的密碼發給你,這就是因為哈希算法是不可逆的。

需要注意的是:在 Web 應用中,在瀏覽器中使用哈希加密的同時也要在服務端上進行哈希加密。

現在,對于簡單的哈希算法的攻擊方法主要有:尋找碰撞法和窮舉法。所以,為了保證數據的安全,可以在哈希算法的基礎上進一步的加密,常見的方法有:加鹽、慢哈希、密鑰哈希、XOR 等。

3. 加鹽(Adding Salt)

加鹽加密是一種對系統登錄口令的加密方式,它實現的方式是將每一個口令同一個叫做“鹽”(salt)的 n 位隨機數相關聯。以 sha1-hex 的使用方法為例:

let salt = self.getCookie('salt') ? self.getCookie('salt') : uuid;
let sign = Sha1hex(`${token}${taxpayerId}${self.state.submitParams.companyName}${uuid}${salt}`);//salt為鹽值

鹽值其實就是我們添加的一串隨機字符串,如上所示,salt 值是隨機的,

${token}${taxpayerId}${self.state.submitParams.companyName}${uuid}

這樣處理之后,相同的字符串每次都會被加密為完全不同的字符串。

使用加鹽加密時需要注意以下兩點:

(1)短鹽值(Short Slat)

如果鹽值太短,攻擊者可以預先制作針對所有可能的鹽值的查詢表。例如,如果鹽值只有三個 ASCII 字符,那么只有 95x95x95=857,375 種可能性,加大了被攻擊的可能性。還有,不要使用可預測的鹽值,比如用戶名,因為針對某系統用戶名是唯一的且被經常用于其他服務。

(2)鹽值復用(Salt Reuse)

在項目開發中,有時會遇到將鹽值寫死在程序里或者只有第一次是隨機生成的,之后都會被重復使用,這種加鹽方法是不起作用的。以登錄密碼為例,如果兩個用戶有相同的密碼,那么他們就會有相同的哈希值,攻擊者就可以使用反向查表法對每個哈希值進行字典攻擊,使得該哈希值更容易被破解。

所以正確的加鹽方法如下:

(1)鹽值應該使用加密的安全偽隨機數生成器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )產生,比如 C 語言的 rand() 函數,這樣生成的隨機數高度隨機、完全不可預測;

(2)鹽值混入目標文本中,一起使用標準的加密函數進行加密;

(3)鹽值要足夠長(經驗表明:鹽值至少要跟哈希函數的輸出一樣長)且永不重復;

(4)鹽值最好由服務端提供,前端取值使用。

4. 慢哈希函數(Slow Hash Function)

顧名思義,慢哈希函數是將哈希函數變得非常慢,使得攻擊方法也變得很慢,慢到足以令攻擊者放棄,而往往由此帶來的延遲也不會引起用戶的注意。降低攻擊效率用到了密鑰擴展( key stretching)的技術,而密鑰擴展的實現使用了一種 CPU 密集型哈希函數( CPU-intensive hash function)。看起來有點暈~還是關注下該函數怎么用吧!

如果想在一個 Web 應用中使用密鑰擴展,則需要設定較低的迭代次數來降低額外的計算成本。我們一般直接選擇使用標準的算法來完成,比如 PBKDF2 或 bcrypt 。PHP、斯坦福大學的 JavaScript 加密庫都包含了 PBKDF2 的實現,瀏覽器中則可以考慮使用 JavaScript 完成,否則這部分工作應該由服務端進行計算。

5. 密鑰哈希

密鑰哈希是將密鑰添加到哈希加密,這樣只有知道密鑰的人才可以進行驗證。目前有兩種實現方式:使用 ASE 算法對哈希值加密、使用密鑰哈希算法 HMAC 將密鑰包含到哈希字符串中。為了保證密鑰的安全,需要將其存儲在外部系統(比如一個物理上隔離的服務端)。

即使選擇了密鑰哈希,在其基礎上進行加鹽或者密鑰擴展處理也是很有必要。目前密鑰哈希用于服務端比較多,例如來應對常見的 SQL 注入攻擊。

6. XOR

XOR [4] 大家都不陌生,它指的是邏輯運算中的 “異或運算”。兩個值相同時,返回 false,否則返回 true,用來判斷兩個值是否不同。

JavaScript 語言的二進制運算,有一個專門的 XOR 運算符,寫作^。

1 ^ 1 // 0
0 ^ 0 // 0
1 ^ 0 // 1
0 ^ 1 // 1

XOR 運算有一個特性:如果對一個值連續做兩次 XOR,會返回這個值本身。這也是其可以用于信息加密的根本。

message XOR key // cipherText
cipherText XOR key // message

目標文本 message,key 是密鑰,第一次執行 XOR 會得到加密文本;在加密文本上再用 key 做一次 XOR 就會還原目標文本 message。為了保證 XOR 的安全,需要滿足以下兩點:

(1)key 的長度大于等于 message ;

(2)key 必須是一次性的,且每次都要隨機產生。

下面以登錄密碼加密為例介紹下 XOR 的使用:

第一步:使用 MD5 算法,計算密碼的哈希;

const message = md5(password);

第二步:生成一個隨機 key 值;

第三步:進行 XOR 運算,求出加密后的 message。

function getXOR(message, key) {
  const arr = [];
  //假設 key 是32位的
  for (let i = 0; i < 32; i++) {
    const  m = parseInt(message.substr(i, 1), 16);
    const k = parseInt(key.substr(i, 1), 16);
    arr.push((m ^ k).toString(16));
  }
  return arr.join('');
}

如上所示,使用 XOR 和一次性的密鑰 key 對密碼進行加密處理,只要 key 沒有泄露,目標文本就不會被破解。

上面說了那么多,問題就來了:我們應該使用什么樣的哈希算法呢?

(1)選擇經過驗證的成熟算法,如 PBKDF2 等 ;

(2)crypt 的安全版本;

(3)避免使用自己設計的加密算法。

有關哈希等算法介紹完了,下面來說下 加密(Encrypt)[5] 算法。

7. 加密(Encrypt)

不同于哈希,加密(Encrypt)是將目標文本轉換成具有不同長度的、可逆的密文。也就是說加密算法是可逆的,而且其加密后生成的密文長度和明文本身的長度有關。從數學角度上講的話: 一個加密算法是一個一對一的映射,其中第二個參數叫做加密密鑰,E 可以將給定的明文 T 結合 Ke 唯一映射為密文 R,反過來,Kd 結合密文 R 也可以唯一映射為對應明文 T,其中 Kd 叫做解密密鑰。

因此,如果被保護數據在以后需要被還原成明文,則需要使用加密。目前,我們用的比較多的是 crypto [5] 模塊,它提供了安全相關的功能,如摘要運算、加密、電子簽名等。在最近的 PC 端系統以及小程序項目中我們都用到了該模塊里的加密算法。

例如,在最近開發的 PC 端的系統中,前端需要對用戶的手機號、郵箱等敏感數據進行加密、解密處理,和后端協商之后選擇了 AES 算法,封裝了相應的加密、解密算法,如下所示:

encrypt: function (data) {
    let encrypted = CryptoJS.AES.encrypt(data, CryptoJS.enc.Utf8.parse(MAP.AuthTokenKey), {
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7,
        iv: CryptoJS.enc.Hex.parse(MAP.iv)
    });
    return encrypted.toString();
}
/**
* AES 解密
**/
decrypt: function (data) {
    let decrypted = CryptoJS.AES.decrypt(data, CryptoJS.enc.Utf8.parse(MAP.AuthTokenKey), {
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7,
        iv: CryptoJS.enc.Hex.parse(MAP.iv)
    });
    return decrypted.toString(CryptoJS.enc.Utf8);
}

接下來分以下幾方面簡要介紹下 crypto 模塊的內容:

(1)對稱加密、非對稱加密

根據加密、解密所用的密鑰是否相同,可以將加密算法分為對稱加密、非對稱加密。

對稱加密

密鑰是相同的,即 encryptKey===decryptKey 。常見的對稱加密算法有 DES、AES 等,偽代碼如下:

encryptedText = encrypt(plainText, key); // 加密
plainText = decrypt(encryptedText, key); // 解密

非對稱加密

加密、解密所用的密鑰是不同的,分為公鑰和私鑰,即 encryptKey!==decryptKey 。常見的非對稱加密算法有 RSA、DSA 等,偽代碼如下:

encryptedText = encrypt(plainText, publicKey); // 加密
plainText = decrypt(encryptedText, priviteKey); // 解密

對稱加密與非對稱加密除了密鑰的不同之外,還有以下不同點:

① 對稱加密的速度更快; ② 對稱加密適用于加密長文本,非對稱加密通常用于加密短文本。

(2)數字簽名

數字簽名主要用于確認信息來源于特定的主體且信息完整、未被篡改,發送方生成簽名,接收方驗證簽名。

發送方首先計算目標文本的摘要(哈希值),通過私鑰對摘要進行簽名,將目標文本和電子簽名發送給接收方。偽代碼如下所示:

digest = hash(message); // 計算摘要
digitalSignature = sign(digest, priviteKey); // 計算數字簽名

接收方驗證簽名的步驟如下:

① 通過公鑰破解電子簽名,得到摘要 D1 (如果失敗,則信息來源主體校驗失敗); ② 計算目標文本摘要 D2; ③ 若 D1 === D2,則說明目標文本完整、未被篡改。

偽代碼如下:

digest1 = verify(digitalSignature, publicKey); // 獲取摘要
digest2 = hash(message); // 計算原始信息的摘要
digest1 === digest2 // 驗證是否相等

看起來和非對稱加密有點像對不對,它們不一樣!

① 非對稱加密(加密/解密):公鑰加密,私鑰解密。 ② 數字簽名(簽名/驗證):私鑰簽名,公鑰驗證。

(3)crypto 中常用API

大多數對稱加密算法都采用了分組加密模式,比如上面我們用到的 AES。接下里有三個概念需要我們著重了解:模式、填充、初始化向量。

分組加密模式

分組加密指的是將(較長的)明文拆分成固定長度的塊,然后對拆分的塊按照特定的模式進行加密。常見的分組加密模式有ECB、CBC(最常用)、CFB等。

填充

假設定義每個塊的長度為 128 位,那么采用分組拆分之后,最后一個數據塊的長度可能小于 128 ,如果采用的是 ECB 或 CBC 模式,此時需要進行填充來滿足長度要求。常用的填充方式是 PKCS7。

初始化向量

初始化向量(IV)主要是為了增強算法的安全性,部分分組加密模式里引入了 IV,這樣可以使得加密結果隨機化。以 CBC 為例,每一個數據塊都與前一個加密塊進行 XOR 運算后再加密(第一個數據塊與 IV 進行 XOR 運算)。IV 的大小與數據塊大小有關,如下圖所示:

以上談到的 API 在封裝后的 AES 加密/解密算法都使用到了,crypto 模塊中 還有很多沒有提及的內容,有興趣的同學可以自己學習。

小結

以上我們所介紹的都是相應算法的簡要知識點,因為密碼學本身是一門非常深奧的數學分支,作為開發者的我們無需太深入的學習,我們只需要了解每種算法的特性及適用場景,在有需要的時候靈活使用就可以了。值得注意的是,上面列出的都是在項目開發實踐中學習 [7][8] 和用到的一些算法,內容有限;而且雖然前端在開發過程中做了一些加密,其實更重要的是服務端的加密處理,所以選擇何種加密方法需要兩端溝通決定。

敲黑板:以上說到的MD5、AES 等資源包都可以在我們的組件庫 POPUI [9] 資源庫中找到,歡迎大家使用。好了,關于前端開發中可以選擇的加密方法就介紹到這里了,如有任何疑問,歡迎留言。

擴展閱讀

[1] https://zh.wikipedia.org/zh/Base64

[2] https://en.wikipedia.org/wiki/MD5

[3] https://en.wikipedia.org/wiki/SHA-1

[4] https://en.wikipedia.org/wiki/XOR_gate

[5] https://www.drupal.org/project/encrypt

[6] https://nodejs.org/api/crypto.html

[7] http://www.cnblogs.com/chyingp/p/nodejs-learning-crypto-theory.html

[8] http://www.infoq.com/cn/articles/how-to-encrypt-the-user-password-correctly

[9] http://popui.jd.com/#/introduce

文章來源于 全棧探索 微信公眾號,掃描下面二維碼關注:

我來評幾句
登錄后評論

已發表評論數()

相關站點

熱門文章
河北20选5平台