Hash 函数及其重要性

2023-11-12

不时会爆出网站的服务器和数据库被盗取,考虑到这点,就要确保用户一些敏感数据(例如密码)的安全性。今天,我们要学的是 hash 背后的基础知识,以及如何用它来保护你的 web 应用的密码。

申明

密码学是非常复杂的一门学科,我不是这方面的专家,在很多大学和安全机构,在这个领域都有长期的研究。

本文我试图使事情简单化,呈现给大家的是一个 web 应用中安全存储密码的合理方法。

“Hashing” 做的是什么?

Hashing 将一段数据(无论长还是短)转成相对较短的一段数据,例如一个字符串或者一个整数。

这是通过使用单向哈希函数来完成的。“单向” 意味着逆转它是困难的,或者实际上是不可能的。

加密可以保证信息的安全性,避免被拦截到被破解。Python 的加密支持包括使用 hashlib 的标准算法(例如 MD5 和 SHA),根据信息的内容生成签名,HMAC 用来验证信息在传送过程中没有被篡改。

一个通常使用的 hash 函数的例子是 md5(),这也是当前在很多不同语言和系统中比较流行的:

import hashlib

data = "Hello World"

h = hashlib.md5()
h.update(data)
print(h.hexdigest())
# b10a8db164e0754105b7a99be72e3fe5

为了计算一个数据块(这儿是 ASCII 字符串)的 MD5 哈希值或摘要,首先需要创建一个 hash 对象,然后添加数据,再调用 digest() 或者 hexdigest() 函数。本例使用的是 hexdigest() 方法来代替 digest() ,是因为为了更清晰地输出而对结果进行了格式化。如果你能接受输出二进制的摘要值,那么就用 digest()

使用 Hash 函数来存储密码

用户注册的过程通常是这样的:

  • 用户填写注册表单,包括密码这一项
  • web 脚本将所有的信息存储在数据库中
  • 然而,密码在存储之前需要通过 hash 函数进行转化
  • 最原始版本的密码并没有保存在任何地方,因此从技术上讲它消失了

用户登录的过程:

  • 用户输入用户名和密码
  • 脚本用同样的 hash 函数来转化密码
  • 脚本找到记录在数据库中的用户信息,读取保存 hash 之后的密码
  • 比较两者的值,如果匹配了就完成了登录

注意原始密码不会存储在任何地方!那么如果数据库被盗,那么用户的登录信息不会被盗,是吗?答案是“根据情况来定”。让我们看看一些潜在的问题:

问题1:Hash 冲突

当两个不同的输入数据产生相同的 Hash 结果时,这就发生了 Hash 冲突。发生的概率依赖于你所使用的函数。

如果利用呢?

作为例子,我使用一些老的脚本,它们使用 crc32() 来 Hash 密码。这个函数会产生 32 位整数的结果,这意味着仅仅有2^32 (i.e. 4,294,967,296) 种结果。

让我们来 hash 一个密码:

import binascii
result = binascii.crc32('supersecretpassword')
print(result) #323322056

现在,我们假设有人盗取了数据库,有了 hash 值。我们也许并不能将 323322056 转成 supersecretpassword,然而我们能用一个简单的脚本,来找到另一个密码可以转化为相同的 hash 值:

import binascii,base64

i = 0
while True:
    if binascii.crc32(base64.encodestring(bytes(i,))) == 323322056:
        print(base64.encodestring(i))
        i += 1

这可能需要运行好一会,但最终会返回一个字符串。我们可以用返回的字符串来代替 supersecretpassword,也同样能登录进入那个用户的账户。

举例来说,在我电脑运行那个脚本一会之后,得到字符串 MTIxMjY5MTAwNg==,让我们测试一下:

import binascii

print(binascii.crc32("supersecretpassword"))
#323322056

print(binascii.crc32("MTIxMjY5MTAwNg=="))
#323322056

如何避免呢?

现在,一个强大的家庭 PC 机就可以用来每秒钟运行一个哈希函数十亿次之多,那么我们需要一个产生非常大范围数的哈希函数。

举例来说,md5() 可能就比较适合,因为它产生 128 位哈希值,也就是 340,282,366,920,938,463,463,374,607,431,768,211,456 可能的结果。通过遍历找到冲突不可能的,然而有些人仍然这样做(参考这里)。

Sha1

Sha1() 是一个更好的替代方案,它会产生甚至长达 160 位的 hash 值。

问题2:彩虹表

甚至我们解决了冲突的问题,我们还不能确保安全。

彩虹表(rainbow table)是通过计算一些常用的单词和它们的组合的 hash 值而创建的。这些表有多达上百万或上亿项。

举例来说,你可以遍历一个字典,为每个单词产生一个 hash 值。你也可以将它们进行组合,也为组合的单词产生 hash 值。这还没完,你甚至可以以数字插入单词的开始、结尾、中间,将它们也存入表中。

考虑到现在存储系统非常廉价,可以产生和使用上 G 量级的彩虹表。

如何利用呢?

让我们想象一下,一个大的数据库被盗,里面有一千万的密码哈希值。在彩虹表中搜索与数据库中密码哈希值的匹配是件相当简单的事,不是所有密码都能找到,但也不是都找不到!它们中的一些肯定可以找到!

如何避免呢?

我们尝试添加 “salt” 来解决,下面是个例子:

import hashlib

password = "EasyPassword"

print(hashlib.sha1(password).hexdigest())
# ff166c2477f864d609ca8111680bfa387eb4e509

salt = "f#@V)Hu^%Hgfds"

print(hashlib.sha1(salt + password).hexdigest())
# 3e7edaceb96becaf69ae7e73073812ea136188e2

我们做的很简单,在 hash 密码之前将“盐化”字符串与用户密码连接,这样很显然 hash 的结果和之前建立的彩虹表没有一个匹配。但是,我们还不够安全!

问题3:彩虹表问题(续)

记住,在数据库被盗之后,还可以重新开始构建彩虹表。

如何利用呢?

即使使用了盐化技术,仍然有可能随着数据库被盗而破解。他们所要做的是重新产生彩虹表,但这次他们会连接盐化字符串到每个密码上。

举例来说,通常彩虹表中 easypassword 可能存在,但在新的彩虹表中,也存在 f#@V)Hu^%Hgfdseasypassword 这样的密码。当他们将上千万条盗来的经过 salted 的哈希值与这张新彩虹表比较时,他们也会能找到一些相同的匹配。

如何避免呢?

我们使用唯一的盐化字符串替代,每个用户都不一样。

一种备选盐化字符串是从数据库中取得用户的 ID:

hashlib.sha1(userid + password).hexdigest()

基于的假设是用户的 ID 号永远不会改变,一般这都是成立的。

我们也可以为每个用户产生一个随机字符串,把它作为这个唯一的 salt。但是我们需要保证要将这个唯一的盐化字符串保存在用户记录的某个位置。

import hashlib, os

def unique_salt():
    return hashlib.sha1(os.urandom(10)).hexdigest()[:22]

salt = unique_salt()
password = "" # str or int
hash = hashlib.sha1(salt + str(password)).hexdigest()
print(hash)
# 37dec03d2761122819f8708e6d5c8392ee02b40d

这种方法有效预防了使用彩虹表破解,因为现在每一个密码都经过不同的值盐化过,攻击者需要生成一千万个独立的彩虹表,这实际上是不现实的。

问题4:Hash 速度

大多 Hash 函数在设计时都注重速度,因为它们常用于计算大数据集和文件的 checksum 值,来检查数据的完整性。

如何利用呢?

就像我之前提到的,一个现代 PC 机带有强大的 GPU(或者显卡)可以完成每秒钟上千次的 hash 运算。这种方法,他们可以使用暴力攻击法,尝试每个可能的密码。

你可能认为需要不少于 8 位长度的密码可能避免暴力破解,让我们看下面的分析来决定是否真的能避免:

  • 如果密码包含小写字母、大写字母以及数字,也就是有 62 (26+26+10) 种可能的字符。
  • 一个 8 位长的字符串就有 62^8 可能的组合,比 218 万亿略小一点。
  • 以每秒十亿的 hash 速率,大约在 60 个小时内就可以破解。

如果是 6 位长的密码,这也相当普遍,只需要在 1 分钟之内就可以破解。

如果要求 9 位或 10 位长度的密码,这样就会让你的用户体验非常不好。

如何避免呢?

使用一个低速的 hash 函数。

假设你是用一个 hash 函数,在同样的硬件下,每秒钟只能进行 100 万次 hash 运算,而不是 10 亿次,暴力破解将会花费比以前多出 1000 倍的时间。那么 60 个小时将会变成将近 7 年!

一种你可以实现的方法是:

import hashlib

def my_hash(password, salt):
    hash = hashlib.sha1(salt + password).hexdigest()

    for i in range(1000):
        hash = hashlib.sha1(hash).hexdigest()
    return hash

print(my_hash("12345", "f#@V)Hu^%Hgfds"))

或者,你可以使用支持 “开销参数”(例如 BLOWFISH)的算法。在 Python 中,可以利用 py-crypt 库。

import bcrypt

def my_hash(password):
    return bcrypt.hashpw(password, bcrypt.gensalt(10))

print(my_hash("atdk"))
#$2a$10$WNhGOdVhoZrrKgwxGa2VIuzfAvm9oFWZF9PIVtLIoU5LQOVGLuLrq

注意输出:

  1. 第一个值是 $2a,表明我们使用的是 BLOWFISH 算法。
  2. 这种情形下第二个值是 $10,是“开销参数”。是执行迭代的次数以 2 为对数的结果,它将会迭代(10 => 2^10 = 1024) 次。这个数值可以在 4 到 31 范围内变化。

让我们运行例子:

import bcrypt, os, hashlib

def my_hash(password, unique_salt):
    return bcrypt.hashpw(password, bcrypt.gensalt(10) + unique_salt)

def unique_salt():
    return hashlib.sha1(os.urandom(10)).hexdigest()[:22]

password = "verysecret"

print(my_hash(password, unique_salt()))
# $2a$10$aHx0q.FE/tGvGWzlm6yePemYx9SAsBP2iSiy/uFx7pyjpy980Hita

结果包括算法($2a),开销参数($10),使用的 22 位 salt,剩下的是计算的 hash 值。让我们测试一下:

import bcrypt, os, hashlib

# assume this was pulled from the database
hash = "$2a$10$6XDaX/3kNby0jI9Ih/Re7.478DOMZK9OnA2mTxKUP0My.39N.jdky"

# assume this is the password the user entered to log back in
password = "verysecret"

def check_password(hash, password):
    salt = hash[:29]
    new_hash = bcrypt.hashpw(password, salt)
    return hash == new_hash

if check_password(hash, password):
    print("Access Granted")
else:
    print("Access Denied")

当我们运行时,我们看到输出 "Access Granted!"。

整合所有的问题

如果考虑到上面的所有问题,根据我们目前所学的,写一个实用类:

import bcrypt, os, hashlib

class PassHash():
    def unique_salt(self):
        return hashlib.sha1(os.urandom(10)).hexdigest()[:22]

    def hash(self, password):
        return bcrypt.hashpw(password, bcrypt.gensalt(10) + self.unique_salt())

    def check_password(self, hash, password):
        full_salt = hash[:29]
        new_hash = bcrypt.hashpw(password, full_salt)
        return hash == new_hash

obj = PassHash()

a = obj.hash("12345")
print(a) # $2a$10$gBSbmXKanQJOTSabtX4wfOE2RT2mKDFbCY6r7cqCJSk2YPGjIDrou

b = obj.check_password(a, "12345")
print(b) # True

现在,我们可以在我们的表单中使用该类来 hash 我们密码,确保安全性。

结论

这种 hash 密码的方法对大多 web 应用已经足够了。别忘记你还可以要求你的用户使用更强的密码,通过强制最小密码长度,组合字符、数字和特殊字符等方法。


编译自:http://pypix.com/python/hash-functions/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Hash 函数及其重要性 的相关文章

随机推荐

  • 【优化求解】基于粒子群算法集合生物地理算法CPSOBBO求解MLP问题matlab代码

    1 简介 Biogeography Based Optimizer BBO is employed as a trainer for Multi Layer Perceptron MLP The current source codes a
  • 卷积网络训练太慢?Yann LeCun:已解决CIFAR-10,目标 ImageNet

    摘要 CIFAR 10竞赛之后 卷积网络之父Yann LeCun接受相关采访 他认为 卷积网络需要大数据和高性能计算机的支持 深层卷积网络的训练时间不是问题 运行时间才是关键 Yann LeCun还分享了他正在做的一些最新研究 Kaggle
  • Centos7.6 源码编译部署percona mysql 5.7.39-42

    Centos7 6 源码编译部署percona mysql 5 7 39 42 参考链接 mysql5 7 35源码编译安装部署 CentOS7 编译安装 Percona Server 5 7 percona Server for MySQ
  • 年薪100万和10万程序员的差距

    点击蓝字关注 回复 职场进阶 获取职场进阶精品资料一份 我们看武侠大片 经常有那种本来可以练就绝世武功的大虾 阴差阳错练的走火入魔 一开始还可以硬撑 还能打败一些虾兵蟹将 遇见真正的高手 这些大虾们立马就败下阵来 其实程序员的职业生涯 如同
  • jquery获取上级、同级和下级元素

    1 JQuery parent expr 找父亲节点 可以传入expr进行过滤 比如 parent parent 或者 parent parent child 2 JQuery parents expr 查找所有祖先元素 不限于父元素 3
  • SQL Server安装教程(2022年更新)

    很多朋友在安装SQL Server的过程中会碰到一些小状况 今天就以Microsoft SQL Server2019为例来聊聊SQL Server安装的相关问题 提示 若之前安装过SQL Server 务必保证在重装前将其卸载干净 目录 1
  • ODrive踩坑(一)windows下使用环境的搭建,odrivetool及USB驱动的安装

    最近有空玩玩无刷电机 早就听说ODrive的控制效果不凡 淘宝400买来玩玩 电机使用我以前囤的几个拆机DJI 3512 别看拆机 但悟的电机是针不戳 编码器使用TLE5012B E1000磁编码器 干回老本行画了张PCB 一方面连接编码器
  • JS异常: Uncaught RangeError: Maximum call stack size exceeded

    今天被一个bug弄得头大 找了无数资料 网上说是递归函数的原因 https blog csdn net qq 30100043 article details 72642205 还是未能解决问题 继续找 最后在 https blog csd
  • 【故障集合】综合架构rsync服务与nfs服务错误集合(持续补充中)

    一 rsync服务 1 1 not a regular file 不是普通文件 scp跟cp类型 默认只能复制普通文件 复制目录 加上 r参数即可 root backup scp etc 172 16 1 31 tmp root 172 1
  • 软件设计七大原则

    在软件开发中 为了提高软件系统的可维护性和可复用性 增加软件的可扩展性和灵活性 程序员要尽量根据 7 条原则来开发程序 从而提高软件开发效率 节约软件开发成本和维护成本 我来依次来总结这 7 条原则 这 7 种设计原则是软件设计模式必须尽量
  • uni-app 框架超详细新手入门

    什么是uni app 介绍 uni app 是一个使用 Vue js 开发跨平台应用的前端框架 开发者通过编写 Vue js 代码 uni app 将其编译到iOS Android 微信小程序等多个平台 保证其正确运行并达到优秀体验 uni
  • 公网远程访问宝塔面板和网页【内网穿透】

    1 ngrok 限制带宽 不限制流量 进入ngrok官网 Sunny Ngrok内网转发内网穿透 国内内网映射服务器 1 注册 2 开通隧道 3 穿透网页 1 配置ngrok 输入网页本地地址和端口号 2 运行ngrok sunny cli
  • IntelliJ IDEA下载安装教程(超详细图解)

    1 IDEA的下载 官网链接 https www jetbrains com idea l 1 点击上面链接进入官网 点击Developer Tools 再点击 Intellij IDEA 2 点击Download 进入IDEA下载界面 3
  • Vue:数据双向绑定和v-系列指令

    Vue js是当下最火的一款前端框架了 学习的时候要多动手实践以帮助理解 我是通过例子来学习的 这样记的快一些 目录 Vue js介绍 如何引入Vue 何为声明式渲染 如何实现 文本插值 message v html v bind 绑定元素
  • 数组的添加和删除过滤方法总结es6 filter()

    es6 filter 数组过滤方法总结 Array every x gt x 是每一个都要满足 Array some x gt x 是有一个满足 Array find findIndex 返回符合条件的第一个值 Array filter 过
  • C语言,文件定位问题详解

    最近在完成上机题碰到文件操作 要求能够修改某一行的数据 关于这个问题 我们自然就会想到用fseek来定位然后修改这一行的内容 可是问题就来了 具体啥问题请往下看 比如说text txt的内容为 这时我想把第二内容改为24 很明显用fseek
  • union关键字,理解-应用场景-优点

    C语言共用体 C语言union用法 详解 biancheng net 理解为 需求场景 某变量可具有多重身份 然而在使用某变量时 它只能确定为其中一种身份 union关键字 这种好处是 既可以满足需求 又可以不浪费内存 union关键字创建
  • Spring MVC 起步

    一 MVC 首先http的请求到达前端控制器 前端知道具体的请求 将代理给到控制器 控制器了解具体的业务细节 因此调用业务逻辑 生成业务数据 并将业务数据返回给前端控制器 然后前端控制器将数据分发给我们的业务视图 由业务视图来呈现业务页面返
  • JS对于字符串的切割截取

    1 slice start end 方法 start 起始索引 开始位置 end 终止索引 结束位置 如果某个参数为负 则从字符串的结尾开始计数 如果省略第二个参数 则该方法将裁剪字符串的剩余部分 var str Apple Banana
  • Hash 函数及其重要性

    不时会爆出网站的服务器和数据库被盗取 考虑到这点 就要确保用户一些敏感数据 例如密码 的安全性 今天 我们要学的是 hash 背后的基础知识 以及如何用它来保护你的 web 应用的密码 申明 密码学是非常复杂的一门学科 我不是这方面的专家