可以说的秘密-那些我们该讨论的前端加密方法

栏目: 编程工具 · 发布时间: 5年前

内容简介:作者:孙印凤随着信息安全重要性的日益凸显,如何保证用户数据的安全成为开发者重点关注的内容。目前,可供我们选择的加密方法有很多,我们需要根据实际的情况选择符合自己的安全解决方案。本文将介绍前端开发中常用的加密方法并给出其适用场景。大家经常说的是 Base64 加密,有 Base64 加密吗?真木有,只有 Base64 编码。

作者:孙印凤

随着信息安全重要性的日益凸显,如何保证用户数据的安全成为开发者重点关注的内容。目前,可供我们选择的加密方法有很多,我们需要根据实际的情况选择符合自己的安全解决方案。本文将介绍前端开发中常用的加密方法并给出其适用场景。

常用加密方法

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

文章来源于 全栈探索 微信公众号,扫描下面二维码关注:

可以说的秘密-那些我们该讨论的前端加密方法


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design for Hackers

Design for Hackers

David Kadavy / Wiley / 2011-10-18 / USD 39.99

Discover the techniques behind beautiful design?by deconstructing designs to understand them The term ?hacker? has been redefined to consist of anyone who has an insatiable curiosity as to how thin......一起来看看 《Design for Hackers》 这本书的介绍吧!

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具