Python 作为计算机和网络安全领域中最受欢迎的语言之一,在安全性和密码学方面具有巨大潜力。本主题涉及 Python 中的密码学功能和实现,从其在计算机和网络安全中的应用到哈希和加密/解密算法。

Section 197.1: 安全的密码哈希

hashlib 模块提供的 PBKDF2 算法可用于执行安全的密码哈希。虽然该算法无法防止暴力攻击以从存储的哈希中恢复原始密码,但它使此类攻击变得非常昂贵。

import hashlib
import os

salt = os.urandom(16)
hash = hashlib.pbkdf2_hmac('sha256', b'password', salt, 100000)

PBKDF2 可以与任何摘要算法配合使用,上述示例使用了通常推荐的 SHA256。随机盐应该与哈希密码一起存储,您需要再次使用它来比较输入的密码与存储的哈希。每个密码都必须使用不同的盐进行哈希,这是非常重要的。至于迭代次数,建议将其设置为应用程序能够承受的最高值。

如果您希望结果为十六进制格式,可以使用 binascii 模块:

import binascii
hexhash = binascii.hexlify(hash)

注意:虽然 PBKDF2 并不差,但 bcrypt 和 scrypt 被认为在抵御暴力攻击方面更为强大。目前,它们都不是 Python 标准库的一部分。

Section 197.2: 计算消息摘要

hashlib 模块允许通过 new 方法创建消息摘要生成器。这些生成器可以将任意字符串转换为固定长度的摘要:

import hashlib

h = hashlib.new('sha256')
h.update(b'Nobody expects the Spanish Inquisition.')
h.digest()
# ==> b'.\xdf\xda\xdaVR[\x12\x90\xff\x16\xfb\x17D\xcf\xb4\x82\xdd)\x14\xff\xbc\xb6Iy\x0c\x0eX\x9eF-='

请注意,在调用 digest 之前,您可以任意多次调用 update,这对于分块哈希大文件非常有用。您还可以使用 hexdigest 获取十六进制格式的摘要:

h.hexdigest()
# ==> '2edfdada56525b1290ff16fb1744cfb482dd2914ffbcb649790c0e589e462d3d'

Section 197.3: 可用的哈希算法

调用 hashlib.new 时需要指定一个算法名称以生成生成器。要了解当前 Python 解释器支持哪些算法,可以使用 hashlib.algorithms_available:

import hashlib
hashlib.algorithms_available
# ==> {'sha256', 'DSA-SHA', 'SHA512', 'SHA224', 'dsaWithSHA', 'SHA', 'RIPEMD160', 'ecdsa-with-SHA1', 'sha1', 'SHA384', 'md5', 'SHA1', 'MD5', 'MD4', 'SHA256', 'sha384', 'md4', 'ripemd160', 'sha224', 'sha512', 'DSA', 'dsaEncryption', 'sha', 'whirlpool'}

返回的列表会根据平台和解释器而有所不同;请确保检查您的算法是否可用。

还有一些算法在所有平台和解释器上都有保证可用,这些算法可以通过 hashlib.algorithms_guaranteed 获取:

hashlib.algorithms_guaranteed
# ==> {'sha256', 'sha384', 'sha1', 'sha224', 'md5', 'sha512'}

Section 197.4: 文件哈希

哈希是一种将可变长度的字节序列转换为固定长度序列的函数。对文件进行哈希有许多好处。哈希可用于检查两个文件是否相同,或验证文件内容是否未被损坏或更改。

您可以使用 hashlib 为文件生成哈希:

import hashlib

hasher = hashlib.new('sha256')
with open('myfile', 'r') as f:
  contents = f.read()
  hasher.update(contents)

print hasher.hexdigest()

对于较大的文件,可以使用固定长度的缓冲区:

import hashlib
SIZE = 65536
hasher = hashlib.new('sha256')
with open('myfile', 'r') as f:
  buffer = f.read(SIZE)
  while len(buffer) > 0:
    hasher.update(buffer)
    buffer = f.read(SIZE)
print(hasher.hexdigest())

Section 197.5: 使用 pycrypto 生成 RSA 签名

RSA 可用于创建消息签名。只有拥有私有 RSA 密钥的访问权限,才能生成有效的签名,而验证则只需要相应的公钥即可。因此,只要对方知道您的公钥,他们就可以验证消息是由您签名且未被篡改的——例如电子邮件中就使用了这种方法。目前,需要使用第三方模块(如 pycrypto)来实现此功能。

import errno

from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5

message = b'This message is from me, I promise.'

try:
  with open('privkey.pem', 'r') as f:
    key = RSA.importKey(f.read())
except IOError as e:
  if e.errno != errno.ENOENT:
    raise
  # No private key, generate a new one. This can take a few seconds.
  key = RSA.generate(4096)
  with open('privkey.pem', 'wb') as f:
    f.write(key.exportKey('PEM'))
  with open('pubkey.pem', 'wb') as f:
    f.write(key.publickey().exportKey('PEM'))

hasher = SHA256.new(message)
signer = PKCS1_v1_5.new(key)
signature = signer.sign(hasher)

验证签名的过程类似,但使用公钥而不是私钥:

with open('pubkey.pem', 'rb') as f:
  key = RSA.importKey(f.read())
hasher = SHA256.new(message)
verifier = PKCS1_v1_5.new(key)
if verifier.verify(hasher, signature):
  print('Nice, the signature is valid!')
else:
  print('No, the message was signed with the wrong private key or modified')

注意:上述示例使用了非常常见的 PKCS#1 v1.5 签名算法。pycrypto 还实现了更新的 PKCS#1 PSS 算法,如果想使用该算法,只需在示例中将 PKCS1_v1_5 替换为 PKCS1_PSS 即可。然而,目前似乎没有理由使用它。

Section 197.6: 使用 pycrypto 进行非对称 RSA 加密

非对称加密的优势在于,可以在不与消息接收者交换密钥的情况下加密消息。发送者仅需知道接收者的公钥,就可以以一种只有指定接收者(拥有相应私钥)才能解密的方式加密消息。目前,需要使用第三方模块(如 pycrypto)来实现此功能。

from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey import RSA

message = b'This is a very secret message.'

with open('pubkey.pem', 'rb') as f:
  key = RSA.importKey(f.read())
cipher = PKCS1_OAEP.new(key)
encrypted = cipher.encrypt(message)

如果接收者拥有正确的私钥,他们就可以解密消息:

with open('privkey.pem', 'rb') as f:
  key = RSA.importKey(f.read())

cipher = PKCS1_OAEP.new(key)
decrypted = cipher.decrypt(encrypted)

注意:上述示例使用了 PKCS#1 OAEP 加密方案。pycrypto 还实现了 PKCS#1 v1.5 加密方案,但由于已知的缺陷,不建议在新协议中使用该方案。

Section 197.7: 使用 pycrypto 进行对称加密

Python 的内置密码学功能目前仅限于哈希。加密需要第三方模块,如 pycrypto。例如,它提供了 AES 算法,该算法被认为是当前对称加密的最新技术。

以下代码将使用密码对给定消息进行加密:

import hashlib
import math
import os

from Crypto.Cipher import AES

IV_SIZE = 16 # 128 bit, fixed for the AES algorithm
KEY_SIZE = 32 # 256 bit meaning AES-256, can also be 128 or 192 bits
SALT_SIZE = 16 # This size is arbitrary

cleartext = b'Lorem ipsum'
password = b'highly secure encryption password'
salt = os.urandom(SALT_SIZE)
derived = hashlib.pbkdf2_hmac('sha256', password, salt, 100000,
dklen=IV_SIZE + KEY_SIZE)

iv = derived[0:IV_SIZE]
key = derived[IV_SIZE:]

encrypted = salt + AES.new(key, AES.MODE_CFB, iv).encrypt(cleartext)

AES 算法需要三个参数:加密密钥、初始化向量(IV)和要加密的实际消息。如果您有一个随机生成的 AES 密钥,那么您可以直接使用它,并仅生成一个随机初始化向量。然而,密码的大小不合适,而且直接使用它也是不可取的,因为它不是真正的随机的,因此其熵相对较低。相反,我们使用内置的 PBKDF2 算法从密码中生成一个 128 位的初始化向量和 256 位的加密密钥。

注意:随机盐非常重要,它确保每个加密的消息都有不同的初始化向量和密钥。这特别确保了两个相同的消息不会产生相同的加密文本,它还防止攻击者将猜测一个密码的工作量用于另一个密码加密的消息。必须将盐与加密的消息一起存储,以便为解密派生相同的初始化向量和密钥。

以下代码将再次解密我们的消息:

salt = encrypted[0:SALT_SIZE]
derived = hashlib.pbkdf2_hmac('sha256', password, salt, 100000,dklen=IV_SIZE + KEY_SIZE)

iv = derived[0:IV_SIZE]
key = derived[IV_SIZE:]
cleartext = AES.new(key, AES.MODE_CFB, iv).decrypt(encrypted[SALT_SIZE:])