网络传输中 XML 敏感信息的加密

网络传输中 XML 敏感信息的加密

假设有如下场景: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 后的数据结构图
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 保证了加密数 据本身的安全性,三重保护为敏感数据提供了一个强大的安全保障。而且经实践证明,本方法性能良好。

 

参考资料

学习

获得产品和技术

  • 下载 IBM 软件试用版,体验强大的 DB2®,Lotus®,Rational®,Tivoli®和 WebSphere®软件。

讨论

关于作者

郑栋辉,毕业于上海交通大学,自 2010 年 4 月加入 IBM CSTL,目前任职于ToolsCenter 部门。他熟悉 C/C++, JAVA, XML。

发表评论

电子邮件地址不会被公开。 必填项已用*标注