四种场景
- 密码加密
- 数据加密
- 对称签名
- 授权认证
密码加密
选择要素
安全性、不可逆、防彩虹表
算法的安全性通常依赖于算法速度足够慢,通常为 300ms ~1000ms,视场景
防彩虹表通常依赖于salt的随机性和长度
主流推荐
scrypt、bcrypt、pbkdf2
流行但不再推荐
sha1、sha1+salt、sha1+salt+ 多次哈希、sha256+salt
// 等效于 sha1+salt 方案,且 salt 随机性长度都很弱
algo = 'sha1'
salt = sha1(Math.random() + '' + Math.random())
hsh = sha1(salt[0..5] + raw_password)
array = [algo, salt[0..5], hsh]
return array.join('$')
sha1 算法因为被攻破而不再推荐,目前推荐的是 sha256
salt 可以有效的抵御彩虹表攻击,能力取决于 salt 的随机性和长度
简单的多次哈希不会带来任何好处,通常会循环万次或更多来保证算法足够慢,也就是 pbkdf2 的方案
bcrypt vs pbkdf2
pbkdf2 是目前 NIST 官方推荐的标准。它约等于哈希函数 +salt+ 多次哈希。nodejs 也源生支持了这个算法。比对密码需要用原参数再计算一遍,所以需要保存 salt 和哈希次数。下面有一个 nodejs 的简单加密代码:
var crypto = require('crypto')
var salt = crypto.randomBytes(16).toString('base64')
// 哈希算法通常选择 sha256
// 第三个参数是哈希次数,选择依据应该是保证算法耗时在 300ms 以上
var hash = crypto.pbkdf2Sync('hello world', salt, 12000, 64, 'sha256').toString('hex')
bcrypt 算法是基于 1993 提出的 Blowfish 算法优化而来。它是一种块加密算法。·也是目前大多数人认为最安全的密码加密算法。它的主要优势在于引入了一个代价因子,用来控制加密速度。算法 salt 会包含代价因子信息并被混入密文,比对密码不需要除密文外其他信息,所以用户可以随机更新代价因子调整加密速度,而不用做任何兼容处理。
nodejs 实现 github.com/dcodeIO/bcr…
数据加密
选择要素
加密性能 、防篡改(通常取决于使用的签名算法)
主流推荐
chacha20poly1305, aes-gcm
流行但不再推荐
aes-cbc + sha1 签名(微信、钉钉等回调推送使用的模式)
chacha20poly1305 vs aes-gcm
chacha20-poly1305 = ChaCha20(流密码) + Poly1305(消息认证码)
chacha20poly1305 是流密码,以字节为单位进行加密,安全性的关键体现在密钥流生成的过程,即所依赖的伪随机数生成器(PRNG)的强度。
nodejs 实现 github.com/calvinmetca…
const chacha = require('chacha')
const crypto = require('crypto')
let key = crypto.randomBytes(32)
let nonce = crypto.randomBytes(12)
let cipher = chacha.createCipher(key, nonce)
let encryptedData = Buffer.concat([
cipher.update(data, 'utf8'),
cipher.final()
])
let authTag = cipher.getAuthTag()
Buffer.concat([authTag, encryptedData]).toString('base64')
aes-gcm = aes-ctr(计数器模式) + gmac(Galois消息认证码)
aes 是块密码,以块数据为单位进行加密。nodejs 代码示例:
const crypto = require('crypto')
let iv = crypto.randomBytes(12)
let secret = crypto.pbkdf2Sync('hello123', crypto.randomBytes(16), 100, 32, 'sha256')
let cipher = crypto.createCipheriv('aes-256-gcm', secret, iv)
let encryptedData = Buffer.concat([
cipher.update(data, 'utf8'),
cipher.final()
])
let authTag = cipher.getAuthTag()
return Buffer.concat([iv, authTag, encryptedData]).toString('base64')
理论上 chacha20poly1305 优于 aes-gcm,但 aes-gcm 更成熟,使用更广泛。
在支持 aes-gcm 的 cpu 上,aes-gcm 性能优于 chacha20poly1305
ctr vs cbc
aes-cbc 模式
aes 的一种循环模式,前一个分组的密文和当前分组的明文异或操作后再加密,这样来增强破解难度, 因此不能并行计算。
aes-ctr 计数器模式
在计数器模式下,我们不再对密文进行加密,而是对一个逐次累加的计数器进行加密,用加密后的比特序列与明文分组进行 XOR 得到密文,因此可以并行计算。
ctr 模式支持并行运算,性能更好。cbc 模式出现的更早,支持和运用更广泛。
对称签名
目前推荐的算法
sha256
哈希函数需要满足的条件,如果不满足,一般就认为不安全了
- 确定性:哈希函数的算法是确定性算法,算法执行过程不引入任何随机量。这意味着相同消息的哈希结果一定相同。
- 高效性:给定任意一个消息 m,可以快速计算 Hash(m) 。
- 目标抗碰撞性:给定任意一个消息 m0,很难找到另一个消息 m1,使得 hash(m0) = hash(m1).
- 广义抗碰撞性:很难找到两个消息 m0≠m1 ,使得 hash(m0) = hash(m1)。
sha1(160位,理论破解需2^80次运算) -> sha2 (256位,理论破解需2^128次运算)-> sha3
sha1的破解花费了 2^69 次运算,在 google 的云服务器上花费了几天时间,参考链接 www.zhihu.com/question/56…
2010 年密码专家预测的 sha2 系列的安全期是 5-10 年
nodejs sha256 实现
function sha256 (str) {
return crypto.createHash('sha256').update(str).digest('hex')
}
有一个误区是 sha256 数字比 sha1 大很多,性能会慢很多。在安全要求不高的场景,选择sha1.
但实际上两者性能差距不大。sha1 稍快一些。
授权认证
推荐
jsonwebtoken
常见方案:对称加密(sha1, sha256, aes, des3)
const serializer = require('serializer')
function sign (_userId, clientKey, extra) {
let handler = serializer.createSecureSerializer(oauthCryptKey, oauthSignKey)
return handler.stringify([_userId, clientKey, Date.now(), extra])
}
// serializer 中加密流程的对应源码
var CYPHER = 'aes256'
var cypher = crypto.createCipher(CYPHER, encrypt_key + nonce_crypt);
var data = JSON.stringify(obj);
var res = cypher.update(nonce_check, DATA_ENCODING, CODE_ENCODING);
res += cypher.update(data, DATA_ENCODING, CODE_ENCODING);
res += cypher.final(CODE_ENCODING);
var digest = signStr(data, validate_key + nonce_check);
return digest + nonce_crypt + res;
jsonwebtoken 简介
jwt 是一个轻量级的认证规范,常用来在用户和服务器之间传递安全可靠的信息。
jwt 由三部分组成:header、payload、sign:
- header 主要记录了使用的算法等基本信息,默认为 sha256,也支持使用非对称签名算法
- payload 为用户信息和 jwt 定义的标准信息(如过期时间、签授方等),该部分为 -base64 编码,一般因只包含核心信息,避免 token 过长
- signature 主要用于防篡改,也可以传入 algorithm:'none' 表示不签名
nodejs 实现 github.com/auth0/node-…
需要注意的点
1.jwt 的 payload 是 base64 编码的,也就是明文传递的,所以不应该用 jwt 传递敏感信息。
2.jwt 对于同样的输入,默认是秒级唯一的,如果有更高的要求,可以在 payload 中传入 iat
// 源码中iat字段默认为当前时间的秒数,同一秒内输出一致
var timestamp = payload.iat || Math.floor(Date.now() / 1000);
if (!options.noTimestamp) {
payload.iat = timestamp
} else {
delete payload.iat;
}
// 传入iat
jwt.sign({hello: 'world', iat: Date.now()}, 'secret')
欢迎大家关注我们的官方公众号