如果感觉文章不错,欢送关注、点赞和分享!
继续分享技术博文,关注微信公众号 前端LeBron
互联网时代,网络上的数据量每天都在以惊人的速度增长。同时,各类网络安全问题层出不穷。在信息安全重要性日益凸显的明天,作为一名开发者,须要增强对平安的意识,并通过技术手段加强服务的安全性。crypto模块的目标是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些性能不是不可能,但速度会十分慢。Nodejs用C/C++实现这些算法后,通过cypto这个模块裸露为JavaScript接口,这样用起来不便,运行速度也快。
编码方式
为什么信息传输须要编码?
在开发加密解密数据的时候碰到须要把加密好的字节数组转换成 String 对象用于网络传输的需要,如果把字节数组间接转换成 UTF-8 等编码方式的话必定会存在某些编码没有对应的字符(8bit只能示意128个字符),在编码和解析过程中会出错,不能正确地表白信息。这时就能够通过罕用的二进制数据编码形式 Base64 编码或者 Hex 编码来实现。
hex编码
- 编码原理
将一个8位的字节数据用两个16进制数示意进去
- 将8位二进制码从新分组成两个4位的字节
- 其中一个字节的低4位是原字节的高4位,另一个字节的低4位是原数据的低4位
- 高4位都补0,而后输入这两个字节对应的十六进制数字作为编码
- 例子
ASCII码:A(65)二进制码:0100 0001从新分组: 00000100 00000001十六进制: 4 1Hex编码:41
就算原文件是纯英文内容,编码后内容也和原文齐全不一样,普通人难以浏览但因为只有16个字符,据说一些程序员大牛可能记下他们的映射关系,从而达到读hex编码和读原文一样的成果。另外,数据在通过hex编码后,空间占用变成了原来的2倍。
base64编码
- 编码原理
Base64编码是通过64个字符来示意二进制数据,64个字符示意二进制数据只能示意6位,所以它能够通过4个 Base64字符来示意3个字节,如下是Base64的字符编码表
- 举个Base64编码的例子,图就很浅显易懂了
- 字符串长度不是3的倍数时补0,也就是“=”
由64个字符组成,比hex编码更难浏览,但因为每3个字节会被编码为4个字符。
所以,空间占用会是原来的4/3,比hex要节俭空间。另外要留神的是,尽管Base64编码后的数据难以浏览,但不能将其作为加密算法应用,因为它解码都不须要你提供密钥啊
urlencode编码
- 编码原理
urlencode编码,看名字就就晓得是设计给url编码的对于a-z
,A-Z
,0-9
,.
,-
和_
,urlencode都不会做任何解决原样输入,而其它字节会被编码为%xx
(16进制)的模式,其中xx
就是这个字节对应的hex编码。 因为英文字符原样保留,对于以英文为主的内容,可读性最好,空间占用简直不变,而对于非英文内容,每个字节会被编码为%xx的3个字符,空间占用是原来的3倍,所以urlencode是一个对英文敌对的编码方案。
Hash
摘要:将不固定长度的音讯作为输出Hash函数,生成固定长度的输入,这段输入称之为摘要
实用场景:敏感信息的校验和存储、验证音讯残缺 & 未被篡改
特点
- 输入长度固定:输出长度不固定,输入长度固定(因算法而异,常见的有MD5、SHA系列)。
- 运算不可逆:已知运算后果的状况下,无奈通过通过逆运算失去原始字符串。
- 高度离散:输出的渺小变动,可导致运算后果差别微小。
- 弱碰撞性:不同输出的散列值可能雷同。
以MD5为例
MD5(Message-Digest Algorithm)是计算机平安畛域宽泛应用的散列函数(又称哈希算法、摘要算法),次要用来确保音讯的残缺和一致性。
常见的利用场景:密码保护、下载文件校验等。
利用场景
- 文件完整性校验:比方从网上下载一个软件,个别网站都会将软件的md5值附在网页上,用户下载完软件后,可对下载到本地的软件进行md5运算,而后跟网站上的md5值进行比照,确保软件的完整性
- 密码保护:将md5后的明码保留到数据库,而不是保留明文明码,防止拖库等事件产生后,明文明码透露。
- 防篡改:比方数字证书的防篡改,就用到了摘要算法。(当然还要联合数字签名等伎俩)
简略的md5运算
- hash.digest([encoding])
计算摘要。encoding能够是hex
、base64
或其余。如果申明了encoding,那么返回字符串。否则,返回Buffer实例。留神,调用hash.digest()后,hash对象就作废了,再次调用就会报错。
- hash.update(data[, input_encoding])
input_encoding能够是utf8
、ascii
或者其余。如果data是字符串,且没有指定 input_encoding,则默认是utf8
。留神,hash.update()办法能够调用屡次。
const crypto = require('crypto');const fs = require('fs');const FILE_PATH = './index.txt'const ENCODING = 'hex';const md5 = crypto.createHash('md5');const content = fs.readFileSync(FILE_PATH);const result = md5.update(content).digest(ENCODING);console.log(result);// f62091d58876a322864f5a522eb05052
密码保护
后面提到,将明文明码保留到数据库是很不平安的
最不济也要进行md5后进行保留
比方用户明码是
123456
,md5运行后,失去输入:e10adc3949ba59abbe56e057f20f883e
这样至多有两个益处:
- 防外部攻打:网站开发者也不晓得用户的明文明码,防止开发者拿着用户明文明码干坏事,以这种模式来爱护用户的隐衷
- 防内部攻打:如网站被黑客入侵,黑客也只能拿到md5后的明码,而不是用户的明文明码,保障了明码的安全性
const crypto = require('crypto');const cryptPwd = (password) => { const md5 = crypto.createHash('md5'); return md5.update(password).digest('hex');}const password = '123456';const cryptPassword = cryptPwd(password);console.log(cryptPassword);// e10adc3949ba59abbe56e057f20f883e
后面提到,通过对用户明码进行md5运算来进步安全性。
- 但实际上,这样的安全性是很差的,为什么呢?
- 略微批改下下面的例子,可能你就明确了。雷同的明文明码,md5值也是雷同的。
- 也就是说当攻击者晓得算法是md5,且数据库里存储的明码值为
e10adc3949ba59abbe56e057f20f883e
时,实践上能够能够猜到,用户的明文明码就是123456
。 - 事实上,彩虹表就是这么进行暴力破解的:当时将常见明文明码的md5值运算好存起来,而后跟网站数据库里存储的明码进行匹配,就可能疾速找到用户的明文明码。
那么有什么方法能够进一步晋升安全性呢?
答案是:明码加盐。
明码加盐
“加盐”这个词看上去很玄乎,其实原理很简略
就是在明码特定地位插入特定字符串后,再对批改后的字符串进行md5运算。
同样的明码,当“盐”值不一样时,md5值的差别十分大
通过明码加盐,能够避免最高级的暴力破解,如果攻击者当时不晓得”盐“值,破解的难度就会十分大
const crypto = require('crypto');const cryptPwd = (password, salt) => { const saltPassword = `${password}:${salt}`; console.log(`原始明码:${password}`); console.log(`加盐明码:${saltPassword}`); const md5 = crypto.createHash('md5'); const result = md5.update(password).digest('hex'); console.log(`加盐明码的MD5值:${result}`)}const password = '123456';const salt = 'abc'cryptPwd(password, salt);/*原始明码:123456加盐明码:123456:abc加盐明码的MD5值:e10adc3949ba59abbe56e057f20f883e*/
明码加盐:随机盐值
通过明码加盐,明码的安全性曾经进步了不少
但其实下面的例子存在不少问题
- 假如字符串拼接算法、盐值已外泄,下面的代码至多存在上面问题:
- 短盐值:须要穷举的可能性较少,容易暴力破解,个别采纳长盐值来解决。
- 盐值固定:相似的,攻击者只须要把罕用明码+盐值的hash值表算进去。
短盐值自不必说,应该防止
- 对于为什么不应该应用固定盐值,这里须要多解释一下。很多时候,咱们的盐值是硬编码到咱们的代码里的(比方配置文件),一旦攻击者通过某种伎俩获知了盐值,那么,只须要针对这串固定的盐值进行暴力穷举就行了
- 比方下面的代码,当你晓得盐值是
abc
时,立即就能猜到51011af1892f59e74baf61f3d4389092
对应的明文明码是123456
。
那么,该怎么优化呢?答案是:随机盐值。
能够看到,明码同样是123456,因为采纳了随机盐值,前后运算得出的后果是不同的
这样带来的益处是,多个用户,同样的明码,攻击者须要进行屡次运算才可能齐全破解
同样是纯数字3位短盐值,随机盐值破解所需的运算量 >> 固定盐值
示例代码如下
const crypto = require('crypto');const getRandomSalt = () => { return Math.random().toString().slice(2,5);}const cryptPwd = (password, salt) => { const saltPassword = `${password}:${salt}`; console.log(`原始明码:${password}`); console.log(`加盐明码:${saltPassword}`); const md5 = crypto.createHash('md5'); const result = md5.update(saltPassword).digest('hex'); console.log(`加盐明码的MD5值:${result}`)}const password = '123456';cryptPwd(password, getRandomSalt());/*原始明码:123456加盐明码:123456:126加盐明码的MD5值:3aeb1848ff63aa32b262bc3f8dd5bd82*/cryptPwd(password, getRandomSalt());/*原始明码:123456加盐明码:123456:232加盐明码的MD5值:21a427268a5094322146e18e47b135fb*/
HMAC性能
HMAC的全称是Hash-based Message Authentication Code,也即在hash的加盐运算。
具体到应用的话,跟hash模块差不多,选定hash算法,指定“盐”即可。
和下面的例子的区别是一个是手动拼盐值,一个是利用HMAC模块
const crypto = require("crypto")const fs = require("fs")const FILE_PATH = "./index.txt"const SECRET = 'secret'const content = fs.readFileSync(FILE_PATH,{encoding:'utf8'})const hmac = crypto.createHmac('sha256', SECRET);hmac.update(content)const output = hmac.digest('hex')console.log(`Hmac: ${output}`)// Hmac: 6f438ef66d3806ae14d6692d9610e55c41ebb4eb3ee73911a4d512bd1cade976
注:大文件可流式解决
加密 / 解密
加解密次要用到上面两组办法:
加密:
- crypto.createCipher(algorithm, password)
- crypto.createCipheriv(algorithm, key, iv)
解密:
- crypto.createDecipher(algorithm, password)
- crypto.createDecipheriv(algorithm, key, iv)
crypto.createCipher / crypto.createDecipher
先来看下 crypto.createCipher(algorithm, password),两个参数别离是加密算法、明码
algorithm:加密算法,比方
aes192
- 具体有哪些可选的算法,依赖于本地
openssl
的版本 - 能够通过
openssl list-cipher-algorithms
命令查看反对哪些算法
- 具体有哪些可选的算法,依赖于本地
- password:用来生成密钥(key)、初始化向量(IV)
crypto.createDecipher(algorithm, password)能够看作 crypto.createCipher(algorithm, password) 逆向操作
const crypto = require("crypto")const SECRET = 'secret'const ALGORITHM = 'aes192'const content = 'Hello Node.js'const encoding = 'hex'// 加密const cipher = crypto.createCipher(ALGORITHM, SECRET)cipher.update(content)const output = cipher.final(encoding)console.log(output)// 944e6e3c21d6eb8568bd6a9716631e、e// 解密const decipher = crypto.createDecipher(ALGORITHM, SECRET)decipher.update(output, encoding)const input = decipher.final('utf8')console.log(input)// Hello Node.js
crypto.createCipheriv / crypto.createDecipheriv
绝对于 crypto.createCipher() 来说,crypto.createCipheriv() 须要提供
key
和iv
,而 crypto.createCipher() 是依据用户提供的 password 算进去的key、iv 能够是Buffer,也能够是utf8编码的字符串,这里须要关注的是它们的长度:
key:依据抉择的算法无关
- 比方 aes128、aes192、aes256,长度别离是128、192、256位(16、24、32字节)
- iv:初始化向量,都是128位(16字节),也能够了解为明码盐的一种
const crypto = require("crypto")const key = crypto.randomBytes(192 / 8)const iv = crypto.randomBytes(128 / 8)const algorithm = 'aes192'const encoding = 'hex'const encrypt = (text) => { const cipher = crypto.createCipheriv(algorithm, key, iv) cipher.update(text) return cipher.final(encoding)}const decrypt = (encrypted) => { const decipher = crypto.createDecipheriv(algorithm, key, iv) decipher.update(encrypted, encoding) return decipher.final('utf8')}const content = 'Hello Node.js'const crypted = encrypt(content)console.log(crypted)// db75f3e9e78fba0401ca82527a0bbd62const decrypted = decrypt(crypted)console.log(decrypted)// Hello Node.js
数字签名 / 签名校验
假如:
- 服务端原始信息为M,摘要算法为Hash,Hash(M)得出的摘要是H
- 公钥为Pub,私钥为Piv,非对称加密算法为Encrypt,非对称解密算法为Decrypt
- Encrypt(H)失去的后果是S
- 客户端拿到的信息为M1,利用Hash(M1)得出的后果是H1
数字签名的产生、校验步骤别离如下:
数字签名的产生步骤:
- 利用摘要算法Hash算出M的摘要,即Hash(M) == H
- 利用非对称加密算法对摘要进行加密Encrypt( H, Piv ),失去数字签名S
数字签名的校验步骤:
- 利用解密算法D对数字签名进行解密,即Decrypt(S) == H
- 计算M1的摘要 Hash(M1) == H1,比照 H、H1,如果两者雷同,则通过校验
私钥如何生成不是这里的重点,这里采纳网上的服务来生成。
理解了数字签名产生、校验的原理后,置信上面的代码很容易了解:
const crypto = require('crypto');const fs = require('fs');const privateKey = fs.readFileSync('./private-key.pem'); // 私钥const publicKey = fs.readFileSync('./public-key.pem'); // 公钥const algorithm = 'RSA-SHA256'; // 加密算法 vs 摘要算法const encoding = 'hex'// 数字签名function sign(text){ const sign = crypto.createSign(algorithm); sign.update(text); return sign.sign(privateKey, encoding);}// 校验签名function verify(oriContent, signature){ const verifier = crypto.createVerify(algorithm); verifier.update(oriContent); return verifier.verify(publicKey, signature, encoding);}// 对内容进行签名const content = 'hello world';const signature = sign(content);console.log(signature);// 校验签名,如果通过,返回trueconst verified = verify(content, signature);console.log(verified);
DH(DiffieHellman)
DiffieHellman:Diffie–Hellman key exchange,缩写为D-H,是一种平安协定,罕用于密钥替换,让通信单方在事后没有对方信息的状况下,通过不平安通信信道,创立一个密钥。这个密钥能够在后续的通信中,作为对称加密的密钥加密传递的信息。
- 原理解析
假如客户端、服务端筛选两个素数a、p(都公开),而后
- 客户端:抉择自然数Xa,Ya = a^Xa mod p,并将Ya发送给服务端;
- 服务端:抉择自然数Xb,Yb = a^Xb mod p,并将Yb发送给客户端;
- 客户端:计算 Ka = Yb^Xa mod p
- 服务端:计算 Kb = Ya^Xb mod p
Ka = Yb^Xa mod p
= (a^Xb mod p)^Xa mod p
= a^(Xb * Xa) mod p
= (a^Xa mod p)^Xb mod p
= Ya^Xb mod p
= Kb
能够看到,只管客户端、服务端彼此不晓得对方的Xa、Xb,但算出了相等的secret
const crypto = require('crypto');const primeLength = 1024; // 素数p的长度const generator = 5; // 素数a// 创立客户端的DH实例const client = crypto.createDiffieHellman(primeLength, generator);// 产生公、私钥对,Ya = a^Xa mod pconst clientKey = client.generateKeys();// 创立服务端的DH实例,采纳跟客户端雷同的素数a、pconst server = crypto.createDiffieHellman(client.getPrime(), client.getGenerator());// 产生公、私钥对,Yb = a^Xb mod pconst serverKey = server.generateKeys();// 计算 Ka = Yb^Xa mod pconst clientSecret = client.computeSecret(server.getPublicKey());// 计算 Kb = Ya^Xb mod pconst serverSecret = server.computeSecret(client.getPublicKey());// 因为素数p是动静生成的,所以每次打印都不一样// 然而 clientSecret === serverSecretconsole.log(clientSecret.toString('hex'));console.log(serverSecret.toString('hex'));// 39edfedad4f1be731977436936ca844e50ebc90953ad208c71d7f2dc1772409962ec3eb90eaf99db5948f089e1d4951f148bd7ff76c18b53ff6be32f267fc54535928ce4acf15d923cfd0caec45db95b206e7636128210ea6813a20fb09cbfb06214b2f488716fea32788023d98cb4cb7fe39b68bd3563b3b34257e37f6b7fb7// 39edfedad4f1be731977436936ca844e50ebc90953ad208c71d7f2dc1772409962ec3eb90eaf99db5948f089e1d4951f148bd7ff76c18b53ff6be32f267fc54535928ce4acf15d923cfd0caec45db95b206e7636128210ea6813a20fb09cbfb06214b2f488716fea32788023d98cb4cb7fe39b68bd3563b3b34257e37f6b7fb7
ECDH(Elliptic Curve Diffie-Hellma)
ECDH和DH原理相似,都是平安密钥协商协定。
绝对于DH协定,联合椭圆曲线密码学ECC减速,运算更节俭CPU资源
- ECDH(Elliptic Curve Diffie-Hellman )原理如下
const crypto = require('crypto');const G = 'secp521r1';const encoding = 'hex'const server = crypto.createECDH(G);const serverKey = server.generateKeys();const client = crypto.createECDH(G);const clientKey = client.generateKeys();const serverSecret = server.computeSecret(clientKey);const clientSecret = client.computeSecret(serverKey);console.log(serverSecret.toString(encoding));console.log(clientSecret.toString(encoding));// 01c418be1b479f936397d4c1653ad77fa28fade67ff058dc18264a72bd1fc208ea6cac4dad996fda55bf271e84f0faef085173257b67bf21f95b09acee4d0a204517// 01c418be1b479f936397d4c1653ad77fa28fade67ff058dc18264a72bd1fc208ea6cac4dad996fda55bf271e84f0faef085173257b67bf21f95b09acee4d0a204517
ECDHE(Elliptic Curve Diffie-Hellma Ephemeral)
一般的ECDH算法也存在肯定缺点,比方密钥协商的时候有一方的私钥总是一样的,个别都是Server方固定,Client方私钥随机生成。随着工夫的缩短,黑客能够截获到海量的密钥协商过程(有些数据是公开的),黑客就能够根据这些数据暴力破解出Server的私钥,而后就能够计算出会话密钥了,加密的数据也会随之被破解。固定一方的私钥会有被破解的危险,那么就让单方的私钥在每次密钥替换通信时,都是随机生成的、长期的,这个算法就是ECDH的增强版:ECDHE, E 全称是 Ephemeral(临时性的)。
扩大
学习这块儿常识的同时也学习了很多密码学相干常识,发现越挖越深快陷进去了,感兴趣的同学能够持续开展看看相干加密算法和他们之间的区别以及利用场景,例如:
- 非对称加密DSA、RSA、DH、DHE、ECDHE
- 对称加密AES、DES
RSA算法原理(二) - 阮一峰的网络日志
图解 ECDHE 密钥替换算法 - 小林coding
材料加密规范(DES) - 维基百科](https://zh.wikipedia.org/wiki/資料加密標準)
高级加密规范(AES) - 维基百科
相干术语
SPKAC:Signed Public Key and Challenge
MD5:Message-Digest Algorithm 5,信息-摘要算法。
SHA:Secure Hash Algorithm,平安散列算法。
HMAC:Hash-based Message Authentication Code,密钥相干的哈希运算音讯认证码。
对称加密:比方AES、DES
非对称加密:比方RSA、DSA
AES:Advanced Encryption Standard(高级加密规范),密钥长度能够是128、192和256位。
DES:Data Encryption Standard,数据加密规范,对称密钥加密算法(当初认为不平安)。
DiffieHellman:Diffie–Hellman key exchange,缩写为D-H,是一种平安协定,让通信单方在事后没有对方信息的状况下,通过不平安通信信道,创立一个密钥。这个密钥能够在后续的通信中,作为对称加密的密钥加密传递的信息。(备注,是应用协定的发明者命名)
密钥替换算法
常见的密钥替换算法有 RSA,ECDHE,DH,DHE 等算法。它们的个性如下:
- RSA:算法实现简略,诞生于 1977 年,历史悠久,通过了长时间的破解测试,安全性高。毛病就是须要比拟大的素数(目前罕用的是 2048 位)来保障平安强度,很耗费 CPU 运算资源。RSA 是目前惟一一个既能用于密钥替换又能用于证书签名的算法。
- DH:diffie-hellman 密钥替换算法,诞生工夫比拟早(1977 年),然而 1999 年才公开。毛病是比拟耗费 CPU 性能。
- ECDHE:应用椭圆曲线(ECC)的 DH 算法,长处是能用较小的素数(256 位)实现 RSA 雷同的安全等级。毛病是算法实现简单,用于密钥替换的历史不长,没有通过长时间的平安攻打测试。
- ECDH:不反对 PFS,安全性低,同时无奈实现 false start。
- DHE:不反对 ECC。十分耗费 CPU 资源 。
倡议优先反对 RSA 和 ECDH_RSA 密钥替换算法。起因是:
- ECDHE 反对 ECC 减速,计算速度更快。反对 PFS,更加平安。反对 false start,用户访问速度更快。
目前还有至多 20% 以上的客户端不反对 ECDHE,咱们举荐应用 RSA 而不是 DH 或者 DHE,因为 DH 系列算法十分耗费 CPU(相当于要做两次 RSA 计算)。
继续分享技术博文,欢送关注!
- 掘金:前端LeBron
- 知乎:前端LeBron
继续分享技术博文,关注微信公众号