假設有如下場景:XML 文件需要在客戶端和服務器端進行傳輸,文件的內容分為兩類:敏感信息 ( 如密碼,證書內容 ) 和普通信息。客戶端和服務器端都可以獨立獲得應用程序的 USERID 和 PASSWord。傳輸方式不限 (SSL 通常建立在 TCP 之上 )。
AES 加密算法
數據的安全性跟一個好的密碼算法是緊密相連的。一般的,加密算法有兩種:對稱加密算法和非對稱加密算法。前者使用同一個密鑰進行加解密,而後者則使用不同的加解密鑰。根據我們的場景,客戶端和服務器端都可以獲得類似的信息,再通過計算可以得到相同的加密密鑰,而無需通過不安全的網絡進行直接傳輸,這樣入侵者就無法直接利用網絡監聽來破獲加密密鑰,所以本文使用對稱加密算法。
在過去的 20 多年中,國際上最常用的對稱加密算法是數據加密標准 (data encryption standard,DES), 該算法已經被美國國家標准和技術協會 (NIST) 采用。但是,到 2001 年 4 月為止,DES 在許多應用領域中被認為是不安全的,因為它采用的密鑰長度為 56 位,許多擁有中等計算資源的計算機就可以用窮盡法搜索出正確的密鑰。因此,NIST 采用了另一種新的加密算法來代替 DES,該算法被稱為高級加密標准 (advanced encryption standard,AES)。AES 一般具有 128 位的分組長度,密鑰長度可分別為 128,192,256 位, 單就安全性而言,AES 的 128 位密鑰比 DES 的 56 位密鑰強 1021 倍還多。AES 是基於代替 - 置換網絡設計的,不管用軟 件還是硬件實現都非常高效。AES 算法的具體實現並不是本文的重點,互聯網上已經存在了很多用各種語言實現的可用代碼。 關於各種對稱加密算法的比較分析可以參見參考文獻中秦志光教授的《密碼算法的現狀和發展研究》。
敏感信息的加密方案
需要傳輸的 XML 文件如下,其中包含了敏感信息和普通信息。為了簡單易懂,這裡敏感信息僅以 passWord 為例。
清單 1. 需要傳輸的 XML 內容
<?XML version="1.0" encoding="UTF-8"?>
<app>
<!-- non sensitive data-->
<userid>USERID</userid>
<!-- sensitive data-->
<password>PASSW0RD</passWord>
</app>
清單 1是我們需要傳輸的原始數據。如果直接傳輸敏感信息,是非常危險的,所以需要采用加密機制。為了在客戶端和服務器端達成統一,我們在 xml 文件中需要指定目前雙方所采用的加密算法。同時,我們設計這樣的加密密鑰:其只在一次傳輸操作中有效,而一旦離開當前會話立即失效。這樣保證了密鑰的安全,使數據得到進一步保障。經過加密機制處理後,XML 將呈現為如 清單 2所示的形式。其中,SessionID 用於標識本次會話,cipher 屬性表示客戶端和服務器端一致認可的加密算法。
清單 2. 加密機制處理後的 XML 文件
<?XML version="1.0" encoding="UTF-8"?>
<app SessionID="App-001e3751a6e6-00000d34-000000004ba7589e-0">
<!-- non sensitive data-->
<userid>USERID</userid>
<!-- sensitive data-->
<passWord cipher="aes128">78DFC347E201F24742030E4E03B8A034C83A4F072EA78DF6C6
3A9AF8DF06E57D42D73DC00D3A01773D1AB8A9DBCE759CACC324BD23D141A0CE4F68FA
E6332970FD272250014A1C1CC82EB1637487A430</passWord>
</app>
這樣,經過加密機制的處理,敏感信息 passWord 的內容以加密後的形式呈現。 現在開始,將分步介紹本文對敏感信息的加密和解密機制,以了解上述 XML 文件中敏感信息 passWord 的加密值是如何得到的。主要步驟可分為數據對齊及初步保護,加密算法的密鑰選取,加密內容的十六進制字符串呈現,以及相應的解密方法。下面以客戶端加密、服務器端解密為例進行說明。因為加密和加密過程在客戶端和服務器端是一致的 , 所以逆向加解密,即服務器端加密、客戶端解密的過程與此類似。
加密過程
Step 1.1. 敏感信息的 Pack 操作,對齊字節並對數據進行初步保護
因為 AES 算法是一種分組算法,而且每個分組必須是 32 位的整數倍,並且最小值為 128 位,我們需要對加密的數據進行 數據對齊,這裡采用 16 字節即 128 位對齊。並且,為了保證數據的完整性,對數據加入頭部和尾部,其中頭部和尾部都有 標識符來表征數據的完整性。以上整個過程我們稱之為 Pack 操作。
一旦該數據被意外截斷,就可以通過頭尾部檢測到,無論發送方還是接收方都會丟棄此類數據,以保證系統安全。以下是 Pack 操作的具體步驟:
讀取用戶提供的需要加密的內容,對 password 的實際值 PASSWord 進行 Pack 操作,使用到的數據結構參見 清單 3中的示例。數據在 Pack 操作後呈現的結構如 圖 1所示:
清單 3. 頭部結構的定義
struct DataHeader {
#pragma pack(push, 1) // 使結構體 1 字節對齊
UINT32 canary1; // 標識,用於檢測數據是否截斷,設置值為 (0xDEADC0DE)
UINT8 version; // 頭部版本(以 0 開始)
UINT8 reserved1; // 保留位
UINT16 offset; // 從開始到有效負載數據的偏移量(頭部長度)
UINT32 size; // 有效負載數據的長度
UINT16 reserved2; // 保留位
UINT8 reserved3; // 保留位 .
UINT8 UIDsize; // UID 的長度
#pragma pack(pop)
} header;
// 備注 : 接下來的兩個字段可以根據實際需要設置
// 文件的其余部分結構如下設置
// char[] UID; // UID 指每次會話的 SessionID(不含結尾的 NULL 符)
// UINT8[] payload; // 需要傳輸的數據,即有效負載數據
// UINT32 canary2; // 標識,用於檢測數據是否截斷,設置值為 (0xDEADC0DE)
圖 1. Pack 後的數據結構圖
為什麼我們在敏感數據的頭尾部加上標識 canary 呢?這出自這樣一個傳說。大家知道 canary 是金絲雀的意思。古代挖煤的工人由於沒有像現在 這麼高級的瓦斯探測儀,所以往往不知道前方是不是危險區域,該不該進入。所以有一個工人想到了一個妙招,在挖煤的地點放一只金絲雀,如果 發現金絲雀站立不穩,就知道這裡是危險區域,應馬上離開。所以我們在數據的頭尾部加上這樣的標識,一旦數據發生異動,就可以及時丟棄異常數據, 報告錯誤。
對上述頭部結構體的內容進行賦值之後,需要把特定內容轉換為網絡序以便網絡傳輸, 如 清單 4所示:
清單 4. 設置頭部結構的網絡序
// 從主機序轉換為網絡序,以便網絡傳輸 .
header.canary1 = htonl(header.canary1);
header.offset = htons(header.offset);
header.size = htonl(header.size);
Canary2 = header.canary1;
然後,對整個數據進行 16 字節的對齊操作,如 清單 5所示。
清單 5. 數據的 16 字節對齊
UINT32 size = sizeof(header) + UID.size() + payload.size() + sizeof(header.canary1);
if(size % 16)
size += 16 - (size % 16);
// 原數據需 copy 到上述 size 的結構中
這樣就會產生 0 到 15 個字節的空位,在圖 1 中用 Padding dummy 表示。 Padding dummy 的長度在 0 到 15 個字節,其取決於 DataHeader, UID, Payload 和 Canary2 的長度和。 padding dummy 的值可以用空值填充。
由此可以看到,數據進行打包後,在數據的頭部和尾部都含由一個符合網絡序的校驗碼 0xDEADC0DE,並且在頭部結構體中包含了整 個數據各部分內容的偏移量等信息,這個機制保證了,一旦數據在傳輸過程中被截斷或者被更改就可以很容易的檢測到,對數據提供了初步的保護。
Step 1.2 AES 加密算法密鑰的獲取
下面我們考慮 AES 的密鑰如何選取。一般的,可以用用戶密碼的 MD5 值作為 AES 密鑰,但是一旦入侵者監聽到散列後的密碼值,則比較危險。 而且,目前 MD5 算法已被破解。所以直接采用 MD5 來對用戶密碼進行散列得到密鑰的做法是不太安全的。
如果我們在散列算法中加入特殊的密鑰,來結合用戶密碼產生 AES 密鑰,雙重保證可以大大提高安全性。HMAC(Keyed-Hash Message Authentication Code) 剛好可以實現此策略。 HMac 是一種經加密的散列消息驗證碼,是一種使用加密散列函數和密鑰計算出來的一種消息驗證碼(MAC)。就像任何 MAC 一樣,它也可以對信息數據的完 整性和真實性進行同步檢查。查看參考資料中的“HMAC”獲得更多關於 HMac 的信息。
在我們的應用場景中,客戶端每次跟服務器端進行通信時會產生一個隨機字符串 , 即 XML 文件中的 SessionID,然後以此為密鑰,生成用戶密碼的 HMAC 值,為了 進一步加強安全,我們把 HMac 值轉換為十六進制字符串後作為 AES 算法的初始密鑰。
隨機字符串一般由信息的發送方來產生。隨機字符串的生成規則如 圖 2所示。
圖 2. 隨機字符串的組成
對 mac address,process id, time stamp 進行十六進制顯示和寬度對齊,便可以得到相應的隨機字符串。比如 App-001e3751a6e6-00000d34-000000004ba7589e0. 在每次的會話過程中,隨機字符串都各不相同,從而使得生成的 HMac 值也只在當前會話中有效。
這裡簡單對 HMAC 的計算做簡單的介紹。計算 HMac 需要一個散列函數 hash(這裡采用 MD5)和一個密鑰 key( 這裡采用隨機字符串 SessionID)。用 L 表示 hash 函數的輸出字符串長(MD5 是 16 字節),用 B 表示數據塊的長度(使用 MD5 分割的數據塊長度是 64 字節)。密鑰 key 的長度應該小於等於數據塊長度 B,如果大於 數據塊長度,可以使用 hash 函數對 key 進行轉換,結果是一個長度為 L 的 key,這樣就滿足了長度小於等於數據塊長度 B 的條件。計算過程如 清單 6所示。
清單 6. 用戶密碼的 HMac 值計算
// 計算過程中需要如下兩個長度為 B 的不同字符串:
o_key_pad = [0x5c * B] ⊕ key // ⊕ 為異或操作
i_key_pad = [0x36 * B] ⊕ key
password( 即用戶密碼 ) 的 HMac 值等於該表達式的值:hash(o_key_pad, hash(i_key_pad , passWord));
這樣,對得到的 passWord 的 HMac 值轉換成十六進制字符串形式就可以作為我們 AES 加密算法的密鑰了。
Step 1.3. AES 加密和數據的十六進制字符串形式轉換
把上個步驟中,由 passWord HMac 值轉換得到的十六進制字符串作為 AES 的密鑰,調用標准的 AES 實現算法即可得到加密後的二進制數據。為了便於二進制形 式的數據便於通過 XML 等格式進行網絡傳輸,我們把把加密後的二進制數據也轉換到十六進制的字符串形式。轉換到十六進制字符串的示例如 清單 7所示。
清單 7. 數據的十六進制字符串轉換
加密後文件的十六進制表示 :
1234567890abcdef
轉換後的十六進制字符串形式
31323334353637383930616263646566
password 的值"PASSWord"經過 Pack、AES 加密之後,轉換為十六進制字符串形式為
78DFC347E201F24742030E4E03B8A034C83A4F072EA78DF6C63A9AF8DF06
E57D42D73DC00D3A01773D1AB8A9DBCE759CACC324BD23D141A0CE4F68
FAE6332970FD272250014A1C1CC82EB1637487A430
這樣的轉化是可逆的。
敏感信息的解密方案
解密過程
由於對稱加密算法的特性,解密過程基本上就是加密的逆過程。
Step 2.1 把加密後十六進制字符串形式轉換為二進制形式
按照加密部分 Step 1.3 介紹的轉換方法,我們就可以把需要解密的十六進制字符串值逆向轉換到十六進制值。
Step 2.2 使用 AES 算法進行解密
同樣的,跟 AES 的加密過程一樣,在服務器端本身就保存著該用戶的用戶密碼,並且可以獲得客戶端以明文形式傳送過來的隨機字符串即 SessionID, 由 此便可以以同樣的 HMac 計算方式得到 AES 算法的加密密鑰。調用 AES 算法的標准解密過程便可以得到解密後的數據。
Step 2.3 對解密後的數據進行去頭操作
解密後的數據還包含我們附加的頭部結構,所以我們需要 unPack 操作,以便得到原始數據。具體操作如 清單 8所示。
清單 8. 對解密後的數據進行 unPack
// 獲得解密後,數據體的頭部結構 DataHeader header
// 從網絡序轉換到主機序
header.canary1 = ntohl(header.canary1);
header.offset = ntohs(header.offset);
header.size = ntohl(header.size);
canary2 = ntohl(canary2);
const UINT32 canary_constant = 0xDEADC0DE;
// 驗證識別碼和頭部版本
if((header.canary1 != canary_constant) || (canary2 != canary_constant)){
// 標識位錯誤,直接丟棄或報錯
LOG << "Header is invalid." << endl;
return;
}
if(header.version != 0){
// 檢測版本,如果不是相應版本也做丟棄處理
LOG<< "Unrecognized header version : " << (unsigned) header.version << endl;
return;
}
// 驗證 UID( 即隨機字符串 ), 若不一致則報錯
UINT32 UID_offset = sizeof(header);
UINT32 payload_offset = UID_offset + header.UIDsize;
UID.assign(data.begin()+UID_offset, data.begin()+payload_offset);
if (UID != SessionID){
LOG << "UID did not match the SessionID." << endl;
return;
}
// 抽取原始數據
UINT32 payload_end_offset = payload_offset + header.size;
payload.assign(data.begin()+payload_offset, data.begin()+payload_end_offset);
unPack 之後,我們得到的 payload 內容就是本次傳輸過程中真正的負載數據值。
小結
本文沒有采用成本較高而且較復雜的 SSL (Secure Sockets Layer ) 協議進行加密通訊,而是介紹了一套針對敏感信息的加密機制,對數據進行了多重 安全考量。其中包括對數據的 Pack 操作保證數據的完整性;結合利用會話隨機字符串 SessionID 和用戶密碼 passWord 的 HMac 值做為加密算法的密鑰大大增強了加密過程的安全性;利用目前最好的對稱加密算法 AES 保證了加密數 據本身的安全性,三重保護為敏感數據提供了一個強大的安全保障。而且經實踐證明,本方法性能良好。