在之前的一份工作中有型参与设计了一款由 React.JS + Electron.JS + Golang 开发的跨平台桌面应用,由于软件需求的特殊性,对数据安全由特殊的需求,虽然在最终实现的方式是使用 GPG 非对称加密的方式对数据加密,但是对于如何用对称加密的方式保证数据的安全也做了一些研究,而最近才有时间用 Golang 实现一个简单 golang library。

上文提到的桌面应用的设计可以看我之前写的一篇博客 缅甸大选中的安全桌面应用设计

加密与解密

通常我们使用 AES 进行数据加密的时候,使用一个 key 来加密一整个明文后得到密文。一般情况下如果我们不知道 key(密钥)是无法解除明文的,但是随着计算机的技术的发展以及 GPU 和 CPU 运算能力的提高,计算机已经可以通过穷举的方式破解 AES-128 的加密方式了,AES-128 是指密钥长度为 16 bytes 的AES 加密方式,AES 支持 16、24、32 密钥长度加密,分别对应 AES-128、AES-196、AES-256 三种加密方式,密钥的长度越长期安全性越高,但是所需加密和解密的时间也就越长。

而单一的 key(密钥)也会带来存储的安全性问题,这时通过某种方法产生密钥就是必要的了,也就是通常说的 key derivation function,如:

dk := pbkdf2.Key([]byte(password), []byte(salt), 4096, 34, sha1.New)  

通过 KDF 方法通过密钥和一个随机值(salt),产生特定长度的数据。这时候我们用一部份的 dk[:16] 做密钥,第二部分做分组加密的密钥。

block, _ := aes.NewCipher(dk[:16])  
stream := cipher.NewCTR(block, dk[16:32])  

这时需要加密的明文通过上述的方法可以产生相同长度的密文。

密码验证与数据一致性

当我们通过密钥来解密一段密文时,我们需要通过某种方法来验证密钥的有效性,这时就需要通过我们设计的特殊密文结构来验证。

密文结构

密文主要由 x bytes 的随机值(salt)、2 bytes 的密钥验证块、明文等长的密文、10 bytes 的签名组成。当我们需要验证密码时,我们就再使用一次加密时用的 KDF 方法产生特定长度 dk,如上述的例子中,dk[32:34]就是我们的密码验证块,当解密时产生的 2 bytes 与密文中的数据一致时,我们初步判定密钥时有效的。这时我们就会执行密文的解密工作。

dk := aesf.pbkdf2Key(header[:8])  
if !bytes.Equal(header[8:], dk[32:]) {  
    return nil, ErrBadPassword
}
block, _ := aes.NewCipher(dk[:16])  
stream := cipher.NewCTR(block, dk[16:32])  

签名字段是我们在我们加密和解密明文时对整个明文进行 sha1HMAC 计算产生值的前10 个bytes,当我们在解密时如果发现密文签名用的签名块不一致时,我们就会认为密文已经被篡改,此时应该停止解密并清除解密后的数据。

结论

对于 AES 加密算法来说安全性确实比 GPG 等非对称加密要弱许多,但是当我们将其与 KDF 和 sha1HMAC 签名算法结合在一起后,也不失为一个快速简单的加密算法,golang 实现的加密库可以在我的 github 上看到 https://github.com/xeodou/aesf

refs:
1: AES - 高级加密标准
2: 分组密码工作模式
3: kdf
4: rfc2898
5: aesf