如果觉得文章不错,欢迎关注、点赞和分享!
持续分享技术博文,关注微信公众号 👉🏻 前端LeBron
编码方式
hex编码
- 编码原理
将一个8位的字节数据用两个16进制数表示出来
- 将8位二进制码重新分组成两个4位的字节
- 其中一个字节的低4位是原字节的高4位,另一个字节的低4位是原数据的低4位
- 高4位都补0,然后输出这两个字节对应的十六进制数字作为编码
- 例子
ASCII码:A(65)
二进制码:0100 0001
重新分组: 00000100 00000001
十六进制: 4 1
Hex编码: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
特点
- 输出长度固定:输入长度不固定,输出长度固定(因算法而异,常见的有MD5、SHA系列)。
- 运算不可逆:已知运算结果的情况下,无法通过通过逆运算得到原始字符串。
- 高度离散:输入的微小变化,可导致运算结果差异巨大。
- 弱碰撞性:不同输入的散列值可能相同。
以MD5为例
应用场景
- 文件完整性校验:比如从网上下载一个软件,一般网站都会将软件的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后的密码,而不是用户的明文密码,保证了密码的安全性
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值的差异非常大
通过密码加盐,可以防止最初级的暴力破解,如果攻击者事先不知道”盐“值,破解的难度就会非常大
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功能
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
algorithm:加密算法,比如
aes192
- 具体有哪些可选的算法,依赖于本地
openssl
的版本 - 可以通过
openssl list-cipher-algorithms
命令查看支持哪些算法
- 具体有哪些可选的算法,依赖于本地
- password:用来生成密钥(key)、初始化向量(IV)
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
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)
// db75f3e9e78fba0401ca82527a0bbd62
const 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);
// 校验签名,如果通过,返回true
const verified = verify(content, signature);
console.log(verified);
DH(DiffieHellman)
- 原理解析
假设客户端、服务端挑选两个素数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
const crypto = require('crypto');
const primeLength = 1024; // 素数p的长度
const generator = 5; // 素数a
// 创建客户端的DH实例
const client = crypto.createDiffieHellman(primeLength, generator);
// 产生公、私钥对,Ya = a^Xa mod p
const clientKey = client.generateKeys();
// 创建服务端的DH实例,采用跟客户端相同的素数a、p
const server = crypto.createDiffieHellman(client.getPrime(), client.getGenerator());
// 产生公、私钥对,Yb = a^Xb mod p
const serverKey = server.generateKeys();
// 计算 Ka = Yb^Xa mod p
const clientSecret = client.computeSecret(server.getPublicKey());
// 计算 Kb = Ya^Xb mod p
const serverSecret = server.computeSecret(client.getPublicKey());
// 由于素数p是动态生成的,所以每次打印都不一样
// 但是 clientSecret === serverSecret
console.log(clientSecret.toString('hex'));
console.log(serverSecret.toString('hex'));
// 39edfedad4f1be731977436936ca844e50ebc90953ad208c71d7f2dc1772409962ec3eb90eaf99db5948f089e1d4951f148bd7ff76c18b53ff6be32f267fc54535928ce4acf15d923cfd0caec45db95b206e7636128210ea6813a20fb09cbfb06214b2f488716fea32788023d98cb4cb7fe39b68bd3563b3b34257e37f6b7fb7
// 39edfedad4f1be731977436936ca844e50ebc90953ad208c71d7f2dc1772409962ec3eb90eaf99db5948f089e1d4951f148bd7ff76c18b53ff6be32f267fc54535928ce4acf15d923cfd0caec45db95b206e7636128210ea6813a20fb09cbfb06214b2f488716fea32788023d98cb4cb7fe39b68bd3563b3b34257e37f6b7fb7
ECDH(Elliptic Curve Diffie-Hellma)
- 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)
扩展
- 非对称加密DSA、RSA、DH、DHE、ECDHE
- 对称加密AES、DES
相关术语
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
持续分享技术博文,关注微信公众号👇🏻