使用 Python Hashlib 进行安全哈希

本篇教程将引导你学习如何利用 Python 的 hashlib 模块内置功能来生成安全的哈希值。

了解哈希的重要性以及如何通过编程计算安全的哈希值可能对你有所帮助——即使你并非专注于应用程序安全领域。 但为何如此呢?

在处理 Python 项目时,你可能会遇到需要存储密码和其他敏感信息的情况,比如在数据库或源代码文件中。此时,对这些敏感信息执行哈希运算,并存储哈希值而非原始信息,将会更加安全可靠。

本指南中,我们将首先介绍什么是哈希,以及它与加密的不同之处。 接着,我们会深入探讨安全哈希函数的特性。随后,我们会使用常用的哈希算法,在 Python 中计算明文的哈希值。过程中,我们将借助内置的 hashlib 模块。

那么,让我们开始这趟探索之旅吧!

何为哈希?

哈希过程接受一段消息字符串,并输出一个固定长度的值,称为哈希值。这意味着,对于特定的哈希算法,其输出的哈希值长度是固定的,与输入消息的长度无关。那么,哈希与加密有何差异?

在加密中,消息或明文会通过加密算法转换为密文。然后,我们可以使用解密算法将密文还原为原始消息。

但哈希的工作方式截然不同。我们已经知道,加密过程是可逆的,即可以从密文还原出明文,反之亦然。

与加密不同,哈希是一个单向过程,这意味着我们无法从哈希值还原出原始的消息。

哈希函数的特性

以下是哈希函数应满足的一些关键特性:

  • 确定性:哈希函数是确定性的。对于相同的消息 m,其哈希值始终相同。
  • 单向性(Preimage Resistant):这一点我们在前面已经提到,即哈希是不可逆的。从输出的哈希值反推出原始消息 m 在计算上是不可行的。
  • 抗碰撞性:找到两个不同的消息字符串 m1m2,使得 m1 的哈希值等于 m2 的哈希值,在计算上应该是极其困难的。这个特性被称为抗碰撞性。
  • 第二原像抗性(Second Preimage Resistant): 对于给定的消息 m1 和其对应的哈希值,找到另一条消息 m2 使得 hash(m1) = hash(m2),在计算上是不可行的。

Python 的 hashlib 模块

Python 内置的 hashlib 模块提供了多种哈希算法和消息摘要算法的实现,包括 SHA 和 MD5 算法等。

要使用 Python 的 hashlib 模块中的构造函数和内置函数,你可以将其导入到你的工作环境中,如下所示:

import hashlib

hashlib 模块提供了 algorithms_availablealgorithms_guaranteed 常量,它们分别表示在当前平台上可用和保证可用的算法集合。

因此,algorithms_guaranteedalgorithms_available 的子集。

启动 Python 的 REPL 环境,导入 hashlib 并访问 algorithms_availablealgorithms_guaranteed 常量:

>>> hashlib.algorithms_available
# 输出
{'md5', 'md5-sha1', 'sha3_256', 'shake_128', 'sha384', 'sha512_256', 'sha512', 'md4', 
'shake_256', 'whirlpool', 'sha1', 'sha3_512', 'sha3_384', 'sha256', 'ripemd160', 'mdc2', 
'sha512_224', 'blake2s', 'blake2b', 'sha3_224', 'sm3', 'sha224'}
>>> hashlib.algorithms_guaranteed
# 输出
{'md5', 'shake_256', 'sha3_256', 'shake_128', 'blake2b', 'sha3_224', 'sha3_384', 
'sha384', 'sha256', 'sha1', 'sha3_512', 'sha512', 'blake2s', 'sha224'}

正如你所见,algorithms_guaranteed 确实是 algorithms_available 的一个子集。

如何在 Python 中创建哈希对象

接下来,我们来学习如何在 Python 中创建哈希对象。我们将使用以下方法来计算消息字符串的 SHA256 哈希值:

  • 通用的 new() 构造函数
  • 特定于算法的构造函数

使用 new() 构造函数

首先,初始化我们的消息字符串:

>>> message = "techblik.com is awesome!"

要实例化一个哈希对象,我们可以使用 new() 构造函数,并传入算法名称,如下所示:

>>> sha256_hash = hashlib.new("SHA256")

现在,我们可以使用消息字符串作为参数,在哈希对象上调用 update() 方法:

>>> sha256_hash.update(message)

如果你直接这样操作,你会遇到错误,因为哈希算法只能处理字节字符串。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Unicode-objects must be encoded before hashing

要获取编码后的字符串,可以在消息字符串上调用 encode() 方法,然后在 update() 方法调用中使用它。完成编码后,你可以调用 hexdigest() 方法,获取消息字符串对应的 SHA256 哈希值。

sha256_hash.update(message.encode())
sha256_hash.hexdigest()
# 输出:'b360c77de704ad8f02af963d7da9b3bb4e0da6b81fceb4c1b36723e9d6d9de3d'

除了使用 encode() 方法对消息字符串进行编码外,你还可以在字符串前面加上 b 前缀,将其直接定义为字节字符串,如下所示:

message = b"techblik.com is awesome!"
sha256_hash.update(message)
sha256_hash.hexdigest()
# 输出: 'b360c77de704ad8f02af963d7da9b3bb4e0da6b81fceb4c1b36723e9d6d9de3d'

得到的哈希值与之前的相同,这验证了哈希函数的确定性。

此外,消息字符串中哪怕是很小的变化,都会导致哈希值发生巨大改变(这种现象也称为“雪崩效应”)。

为了验证这一点,让我们将 “awesome” 中的 “a” 改为 “A”,并重新计算哈希值:

message = "techblik.com is Awesome!"
h1 = hashlib.new("SHA256")
h1.update(message.encode())
h1.hexdigest()
# 输出: '3c67f334cc598912dc66464f77acb71d88cfd6c8cba8e64a7b749d093c1a53ab'

可见,哈希值已经完全改变了。

使用特定于算法的构造函数

在之前的示例中,我们使用了通用的 new() 构造函数,并传入 “SHA256” 作为算法名称来创建哈希对象。

除了这种方法,我们还可以直接使用 sha256() 构造函数,如下所示:

sha256_hash = hashlib.sha256()
message= "techblik.com is awesome!"
sha256_hash.update(message.encode())
sha256_hash.hexdigest()
# 输出: 'b360c77de704ad8f02af963d7da9b3bb4e0da6b81fceb4c1b36723e9d6d9de3d'

得到的输出哈希值与之前针对消息字符串 “techblik.com is awesome!” 计算出的哈希值相同。

探索哈希对象的属性

哈希对象还提供一些有用的属性:

  • digest_size 属性表示摘要的大小(以字节为单位)。 例如,SHA256 算法会返回一个 256 位的哈希值,相当于 32 个字节。
  • block_size 属性指的是哈希算法中使用的块大小。
  • name 属性是我们可以在 new() 构造函数中使用的算法名称。当哈希对象没有描述性名称时,查询此属性会很有帮助。

现在,让我们来检查一下之前创建的 sha256_hash 对象的这些属性:

>>> sha256_hash.digest_size
32
>>> sha256_hash.block_size
64
>>> sha256_hash.name
'sha256'

接下来,我们将探讨使用 Python 的 hashlib 模块进行哈希运算的一些有趣的应用。

哈希的实际应用

验证软件和文件的完整性

作为开发者,我们经常需要下载和安装各种软件包,无论是在 Linux 发行版上,还是在 Windows 或 Mac 系统中。

然而,某些软件包镜像可能不可信。你经常可以在下载链接旁边找到哈希值(或校验和)。你可以通过计算已下载软件的哈希值,并将其与官方提供的哈希值进行对比,从而验证下载的软件是否完整、未被篡改。

这种方法同样适用于你机器上的文件。即使文件内容发生极小的改动,也会导致哈希值发生显著变化。因此,通过验证哈希值,你可以检查文件是否被修改过。

这是一个简单的例子。在你的工作目录中创建一个名为 “my_file.txt” 的文本文件,并在其中添加一些内容。

$ cat my_file.txt
This is a sample text file.
We are  going to compute the SHA256 hash of this text file and also
check if the file has been modified by
recomputing the hash.

然后,你可以以读取二进制模式 ('rb') 打开该文件,读取文件内容,并计算其 SHA256 哈希值,如下所示:

>>> import hashlib
>>> with open("my_file.txt","rb") as file:
...     file_contents = file.read()
...     sha256_hash = hashlib.sha256()
...     sha256_hash.update(file_contents)
...     original_hash = sha256_hash.hexdigest()

这里的变量 original_hash 就是 “my_file.txt” 在当前状态下的哈希值。

>>> original_hash
# 输出: '53bfd0551dc06c4515069d1f0dc715d002d451c8799add29f3e5b7328fda9f8f'

现在,我们来修改 “my_file.txt” 文件。比如,你可以删除 “going” 这个词前面的多余空格。 🙂

再次计算文件的哈希值,并将其存储在 computed_hash 变量中。

>>> import hashlib
>>> with open("my_file.txt","rb") as file:
...     file_contents = file.read()
...     sha256_hash = hashlib.sha256()
...     sha256_hash.update(file_contents)
...     computed_hash = sha256_hash.hexdigest()

然后,你可以添加一个简单的断言语句,来验证 computed_hash 是否等于 original_hash

>>> assert computed_hash == original_hash

如果文件被修改过(在本例中为真),你会得到一个 AssertionError

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

你还可以在存储密码等敏感信息时使用哈希技术。例如,可以将用户密码的哈希值存储在数据库中。在用户登录时,对用户输入的密码进行哈希处理,并将其哈希值与数据库中存储的哈希值进行对比,以验证密码的正确性。

总结

希望本教程能帮助你理解如何使用 Python 生成安全的哈希值。以下是本教程的要点:

  • Python 的 hashlib 模块提供了多种哈希算法的现成实现。你可以使用 hashlib.algorithms_guaranteed 来获取在你平台上保证可用的算法列表。
  • 要创建一个哈希对象,你可以使用通用的 new() 构造函数,其语法为:hashlib.new("algo-name")。或者,你也可以使用与特定哈希算法对应的构造函数,例如:hashlib.sha256() 用于 SHA256 哈希运算。
  • 在初始化消息字符串和哈希对象后,你可以调用哈希对象的 update() 方法,然后再调用 hexdigest() 方法来获取哈希值。
  • 哈希技术在检查软件工件和文件的完整性,以及在数据库中存储敏感信息等方面都非常有用。

接下来,你可以学习如何使用 Python 编写一个随机密码生成器。