Web 开发的身份和数据安全_2.密码加密、哈希和加盐

本文主要内容:探讨密码加密和安全性。

静态数据&动态数据

静态数据:不活动的(或静止的)数字数据,存储在服务器中,例如用于存储密码、个人资料、或其他应用所需数据的数据库。

动态数据:传输中的数据,在应用和数据库之间来回发送,或者在网站和 API 或外部数据源之间来回传送。

静态数据

  • 数据库加密是绝对有必要的,尽管 99% 的团体没有这么做。
  • 加密数据库应该使用强加密、受 NIST 认可的算法,如:SHA-256、AES、RSA。
  • 遵循标准做法:1. 分开访问控制(用户登录)和数据加密;2. 定期更新加密数据库的密钥;3. 分开存储加密密钥和数据。
  • 对全球覆盖的应用来说,可以使用数据联合避免恶意访问数据存储器,在需要个人信息的不同区域维护不同的数据库系统。
  • 根据运行应用、网站或服务的需要,应该尽量少存储敏感的用户数据
  • 敏感的财务信息(例如信用卡数据)可以交由他方(支付提供商)处理。

动态数据

动态数据的使用场景:

  • 用户填写的注册信息,用于访问账户和认证身份。
  • 把个人档案信息传输给API服务,或从中获取。
  • 应用或网站收集的其他数据,传给数据库存储起来。

密码攻击媒介

  • 钓鱼;
  • 社会工程;
  • 暴力攻击;(抵抗方式:密钥延伸技术)
  • 字典攻击;(抵抗方式:加盐)
  • 彩虹表;(抵抗方式:加盐)
  • 恶意软件;(抵抗方式:短信验证、n因素认证)
  • 离线破解;

加盐

盐值是一种随机数据,计算密码的哈希值时用于加强数据,抵御多种攻击媒介尤其是字典攻击和彩虹表。这个随机数据(特别长)能确保生成的哈希值是唯一的,即使多个用户使用相同的密码(确实有这种情况),添加唯一的盐值后能确保得到的哈希值仍是唯一的。就因为得到的哈希值是唯一的,我们才得以免受彩虹表和字典攻击的危害。

盐值的特点:盐值需要足够长、不可预测、高度随机、需要使用安全的伪随机函数生成,另外,还要避免使用全局的盐值。

生成随机盐值

使用 Node 原生支持的的 crypto 库生成随机盐值。

const crypto = require('crypto');

// 1.异步方法生成盐值
// crypto.randomBytes() 生成 16 位强加密的伪随机数
crypto.randomBytes(16, (err, buf) => {
  if (err) throw err;
  console.log(`${buf.length} bytes of random data: ${buf.toString('hex')}`);
  console.log(`${buf.length} bytes of random data: ${buf.toString('base64')}`);
});

// 2.同步方法生成盐值
const buf = crypto.randomBytes(256);

重用盐值

用户注册账户或修改密码时应该生成并存储新的盐值和哈希值。

盐值的长度

  • 根据经验,盐值的长度应该与哈希函数的输出长度一致。
  • PBKDF2 标准建议至少应该使用 64 位(8字节)长度的盐值。通常,多数情况下使用的是2的7次方,即 128 位(16字节)

把盐值存储在哪?

盐值可以和哈希值一起以明文形式存储在数据库中。

salt 不能和哈希值存储在一起,不然就和没有 salt 一样,因为攻击者拖库后,构建字典非常简单,因为它能够直接看到 salt,所以一定要分开存储(比如在不同的机器上),这样即使口令密文表被拖库了,而 salt 表没有被拖库,也是相对安全的。

撒胡椒

  • 胡椒是在计算哈希值时随盐值和密码一起传入的值。
  • 使用胡椒的简单公式:hash ( salt + pepper + password ) = password hash
  • 胡椒在代码层计算,而不是使用存储的值。
  • 使用胡椒的原因:利用额外的字符和符号加强密码的强度。

选择正确的密码哈希函数

bcrypt ⭐️⭐️⭐️

GitHub 源码:node.bcrypt.js

特性:专为加密密码设计,底层基于 Blowfish 密码法,有异步方法和同步方法、密码哈希函数、校验密码函数、带 promise 特性...

const bcrypt = require('bcrypt');

// 封装 hash 函数
function bcrypt_encrypt(username, password) {
  // 1. bcrypt 内置了生成盐值的方法:bcrypt.genSalt()
  bcrypt.genSalt(10, (err, salt) => {
    if (err) throw err;
      
    // 2. 生成哈希值:hash()
    bcrypt.hash(password, salt, (err, key) => {
      if (err) throw err;
      
      // 3. 把用户名、密码哈希值和盐值存入数据库
    });
  });
}

// 调用示例
bcrypt_encrypt('zhangsan', '123456');

// 对比哈希值,验证密码:
bcrypt.compare(password, hash, (err, same) => {
  // 返回 true 或 false
});

PBKDF2

  • 1Password、LastPass 等密码管理系统采用的算法;
  • Node.js 中的 crypt 模块原生支持的标准算法;
  • 通过该算法可以实现基于口令的加密(PBE),即基于口令生成密钥

加密示例:

// PBKDF2 算法
function pbkdf2_encrypt(username, password) {
  // 1. crypto.randomBytes()方法生成 32 字节的随机盐值
  crypto.randomBytes(32, (err, salt) => {
    if (err) throw err;

    // 2. 参数列表:(密码,盐值,迭代次数,输出密钥长度,摘要算法)
    crypto.pbkdf2(password, salt, 4096, 512, 'sha256', (err, key) => {
      if (err) throw err;
      
      // 3. 将用户名、密码哈希值和盐值存入数据库
      // Salt 盐值是明文保存的,一般不和最终生成的密钥保存在一起。
      console.log(username, key.toString('hex'), salt.toString('hex'));
    });
  });
}

// 调用示例
pbkdf2_encrypt('zhangSan', '123456');

对比哈希值,验证密码:

const dbsalt = 'USER RECORD SALT FROM YOUR DATABASE';
const dbhash = 'USER RECORD KEY FROM YOUR DATABASE';

crypto.pbkdf2(password, dbsalt, 4096, 512, 'sha256', (err, comparsehash) => {
  if (err) throw err;

  // 比较
  if (dbhash.toString('hex') === comparsehash.toString('hex')) {
    // 密码匹配
  } else {
    // 密码不匹配
  }
});

scrypt

GitHub 源码:node-scrypt

scrypt 的优势和实现如下:

  • 做了特殊设计,是硬件和内存密集型算法,攻击者要实施大型攻击,想要破解需要耗费异常多的硬件和内存。
  • 是加密数字货币莱特币和狗狗币背后采用的算法。
'use strict';

const scrypt = require('scrypt');
const crypto = require('crypto');

function scrypt_encrypt(username, password) {
  // 1. 使用 crypto 的 crypto.randomBytes(...) 方法生成盐值。
  crypto.randomBytes(32, (err, salt) => {
    if (err) throw err;

    // 2. scrypt.hash(...) 生成 64 位的哈希值
    // - N: scrypt 最多使用多长时间(秒数)计算密钥(偶数)。
    // - r:计算密钥时最多使用多少字节 RAM(整数)。默认为0。
    // - p:计算密钥时所用 RAM 占可用值的比例(0-1,换算成百分比)默认为0.5。
    scrypt.hash(password, {"N":16384,"r":8,"p":1}, 64, salt, (err, key) => {
      if (err) throw err;

      // 3. 把用户名、密码哈希值和盐值存入数据库
      console.log(`key is ${key}`);
    });
  });
}

密钥延伸

bcrypt、scrypt 和 PBKDF2 行之有效涉及到的底层概念——密钥延伸(Key Derivation Function,KDF)。
密钥延伸:把弱密码变成特别复杂的长密码,致使暴力攻击等攻击媒介不再可行。

对加密哈希函数来说,密钥延伸体现在不断循环应用哈希函数(哈希函数迭代),直到得到所需长度和复杂度的哈希值为止。

重新计算哈希值

有时可能需要为用户生成新的密码哈希值。比如说:

  • 根据摩尔定律,硬件更新了,要修改加密算法使用的权重/工作因子。
  • 算法变了,或者有更好的算法出现,目前使用的算法不安全。
  • 觉得现有哈希值不再安全。

遇到这些情况,符合常规的做法是为用户生成新哈希值,存储在系统中。用户提供用户名和密码请求登录时,正常比较根据输入值计算的哈希值和存储的哈希值,而不是拒绝登录。随后,为用户生成新的哈希值,替换用户记录中的旧值。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,104评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,816评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,697评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,836评论 1 298
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,851评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,441评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,992评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,899评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,457评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,529评论 3 341
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,664评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,346评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,025评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,511评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,611评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,081评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,675评论 2 359

推荐阅读更多精彩内容