用 PyCrypto 处理密码——哈希函数,对称加密算法与公开密钥算法

翻译自 http://www.laurentluce.com/posts/python-and-cryptography-with-pycrypto/
原标题 Python and cryptography with pycrypto

这不是一篇完全翻译的文章,有对内容的适当删改。

目录:

  • 哈希函数
  • 对称加密算法
  • 公开密钥算法(非对称加密算法)

安装

此节由译者增加并编撰。

PyCrypto 是一个用于信息安全的 Python 库,提供了多种对数据处理的方法。

在 Windows 下安装 PyCrypto 非常不便,但是你可以访问 http://www.voidspace.org.uk/python/modules.shtml#pycrypto 下载二进制安装包。

PyCrypto 的官网是 https://www.dlitz.net/software/pycrypto/,在 CentOS 下安装 PyCrypto 需要 python-devel 库。

yum install python-devel
pip install pycrypto

哈希函数

哈希函数接受一个长度不定的字符串,然后输出一个固定长度的字符串。输出的字符串被称作哈希值,理想的哈希函数应该遵循以下几点:

  • 难以根据输入的字符串猜测输出的哈希值
  • 难以找到相同的两个输出的哈希值
  • 难以修改输入的字符串而不修改输出的哈希值

根据这些特性,哈希值可以被认为是原文的“签名”。

哈希函数可以被用于计算和校验数据,它可以被用在数字签名和认证上,让我们来看一个哈希函数应用的例子:SHA-256

SHA-256

用哈希函数 SHA-256 应该这样做:

>>> from Crypto.Hash import SHA256
>>> SHA256.new('abc').hexdigest()
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'

如 MD5 这样的哈希函数可能会受到碰撞攻击,碰撞攻击指的是两个不同的输入产生出相同的哈希值输。2004 年和 2008 年发现的 Preimage attack(直译“原像攻击”),可以让你找到经过哈希函数前的值。

应用场景

哈希函数可以用在用户的密码管理中,网站通常存储的是密码的哈希值而不是密码本身,这样只有用户自己会知道自己的密码。当用户登录时,将用户的密码的哈希值与存储在数据库中的哈希值进行比对。如果匹配,授予用户访问权限。

from Crypto.Hash import SHA256
def check_password(clear_password, password_hash):
    return SHA256.new(clear_password).hexdigest() == password_hash

像例子这样存储密码并不完全安全,建议使用 py-bcrypt 这样的模块来对密码进行哈希。

另外一种应用场景是文件完整性检查,很多提供下载的文件都附带着 MD5 校验,下面是计算一个文件的 MD5 并验证的代码。它一块一块的计算,以避免内存超限。

import os
from Crypto.Hash import MD5
def get_file_checksum(filename):
    h = MD5.new()
    chunk_size = 8192 
    with open(filename, 'rb') as f:
        while True:
            chunk = f.read(chunk_size)
            if len(chunk) == 0:
                break
            h.update(chunk)
    return h.hexdigest()
    

哈希函数的比较

哈希函数 哈希输出结果(bits) 安全
MD2 128
MD4 128
MD5 128
SHA-1 160
SHA-256 256

加密算法

加密算法需要一些文字作为输入,并且使用一个可以变动的密钥,输出最终加密好的密文。加密算法有两种工作方式:分组密码和流加密。分组密码将明文分成多个等长的块(block),每组长度为 8 或 16 字节,然后对块进行加密,而流密码逐字节加密。知道密钥,你就可以解密密文。

分组密码

我们先来看看分组密码:DES。使用 8 字节的密码且数据块工作在 8 字节长度。最简单的加密模式是电子密码本(ECB)模式,每个块被独立地加密,以形成加密后的文本。

用 PyCrypto 很容易实现 DES ECB 加密。下面的例子作为密钥的“01234567”长度是八位,而文本“abcdefgh”的长度也是八位。

>>> from Crypto.Cipher import DES
>>> des = DES.new('01234567', DES.MODE_ECB)
>>> text = 'abcdefgh'
>>> cipher_text = des.encrypt(text)
>>> cipher_text
'\xec\xc2\x9e\xd9] a\xd0'
>>> des.decrypt(cipher_text)
'abcdefgh'

一个更强的模式是 CFB(Cipher feedback),CFB 能够将块密文(Block Cipher)转换为流密文(Stream Cipher)。每一个密文块都依赖于前面的密文块与明文块,同时为了保证每条消息的唯一性,需要在第一个块中使用初始化向量(initialization vector)。

下面演示如何使用 DES 的 CFB 模式,原文有 16 个字节(8 的倍数)。

>>> from Crypto.Cipher import DES
>>> from Crypto import Random
>>> iv = Random.get_random_bytes(8)
>>> des1 = DES.new('01234567', DES.MODE_CFB, iv)
>>> des2 = DES.new('01234567', DES.MODE_CFB, iv)
>>> text = 'abcdefghijklmnop'
>>> cipher_text = des1.encrypt(text)
>>> cipher_text
"?\\\x8e\x86\xeb\xab\x8b\x97'\xa1W\xde\x89!\xc3d"
>>> des2.decrypt(cipher_text)
'abcdefghijklmnop'

流密码

这些算法基于每个字节工作,单个块大小始终一个字节。PyCrypto 支持两种流密码算法:ARC4 与 XOR,并且只支持 ECB 模式。

让我们来看看 ARC4 使用密码“01234567”的代码:

>>> from Crypto.Cipher import ARC4
>>> obj1 = ARC4.new('01234567')
>>> obj2 = ARC4.new('01234567')
>>> text = 'abcdefghijklmnop'
>>> cipher_text = obj1.encrypt(text)
>>> cipher_text
'\xf0\xb7\x90{#ABXY9\xd06\x9f\xc0\x8c '
>>> obj2.decrypt(cipher_text)
'abcdefghijklmnop'

应用

很容易使用 PyCrypto 来加密解密一个文件,让我们用 DES3(三重 DES)来做这个任务。当文件很大时,在加密和解密时使用数据块来避免使用过多的内存。在数据块小于 16 字节时,我们在加密前给它补位。

import os
from Crypto.Cipher import DES3

def encrypt_file(in_filename, out_filename, chunk_size, key, iv):
    des3 = DES3.new(key, DES3.MODE_CFB, iv)

    with open(in_filename, 'r') as in_file:
        with open(out_filename, 'w') as out_file:
            while True:
                chunk = in_file.read(chunk_size)
                if len(chunk) == 0:
                    break
                elif len(chunk) % 16 != 0:
                    chunk += ' ' * (16 - len(chunk) % 16)
                out_file.write(des3.encrypt(chunk))

def decrypt_file(in_filename, out_filename, chunk_size, key, iv):
    des3 = DES3.new(key, DES3.MODE_CFB, iv)

    with open(in_filename, 'r') as in_file:
        with open(out_filename, 'w') as out_file:
            while True:
                chunk = in_file.read(chunk_size)
                if len(chunk) == 0:
                    break
                out_file.write(des3.decrypt(chunk))

下面是上面定义的两个函数的使用。

from Crypto import Random
iv = Random.get_random_bytes(8)
with open('to_enc.txt', 'r') as f:
    print 'to_enc.txt: %s' % f.read()
encrypt_file('to_enc.txt', 'to_enc.enc', 8192, key, iv)
with open('to_enc.enc', 'r') as f:
    print 'to_enc.enc: %s' % f.read()
decrypt_file('to_enc.enc', 'to_enc.dec', 8192, key, iv)
with open('to_enc.dec', 'r') as f:
    print 'to_enc.dec: %s' % f.read()

公开密钥算法

上面看到的加密算法的缺点是,双方都需要知道密钥。公共密钥算法中,有两个不同的密钥,一个用于加密一个用于解密,知道私钥就可以生成数个公钥。你只需要共享你的密钥并且只有你的私钥能解密发送的消息。

生成公钥与私钥

用 pycrypto 生成公钥与私钥,需要指定密钥大小:这里选择 1024 位长度,越长的密钥越安全。我们还需要指定一个随机数发生器,这里为 PyCrypto 指定为 Random 模块。

>>> from Crypto.PublicKey import RSA
>>> from Crypto import Random
>>> random_generator = Random.new().read
>>> key = RSA.generate(1024, random_generator)
>>> key
<_RSAobj @0x7f60cf1b57e8 n(1024),e,d,p,q,u,private>

让我们来看看这个对象支持的方法。函数 can_encrypt() 检查该算法加密数据的能力。函数 can_sign() 检查签名消息的能力。函数 has_private() 在私钥存在的情况下返回 True。

>>> key.can_encrypt()
True
>>> key.can_sign()
True
>>> key.has_private()
True

加密

现在我们有密钥对,可以加密一些数据。首先,我们提取密钥对的公共密钥并用它来加密一些数据。参数 32 是一个用于 RSA 加密的随机数。这一步生成我们公开的加密密钥,用它把别人发给我们的数据加密。

>>> public_key = key.publickey()
>>> enc_data = public_key.encrypt('abcdefgh', 32)
>>> enc_data
('\x11\x86\x8b\xfa\x82\xdf\xe3sN ~@\xdbP\x85
\x93\xe6\xb9\xe9\x95I\xa7\xadQ\x08\xe5\xc8$9\x81K\xa0\xb5\xee\x1e\xb5r
\x9bH)\xd8\xeb\x03\xf3\x86\xb5\x03\xfd\x97\xe6%\x9e\xf7\x11=\xa1Y<\xdc
\x94\xf0\x7f7@\x9c\x02suc\xcc\xc2j\x0c\xce\x92\x8d\xdc\x00uL\xd6.
\x84~/\xed\xd7\xc5\xbe\xd2\x98\xec\xe4\xda\xd1L\rM`\x88\x13V\xe1M\n X
\xce\x13 \xaf\x10|\x80\x0e\x14\xbc\x14\x1ec\xf6Rs\xbb\x93\x06\xbe',)

解密

我们用私钥来解密数据。

>>> key.decrypt(enc_data)
'abcdefgh'

签名

对消息进行签名可以帮助验证消息的作者,并且确保我们能相信数据的来源。接下来是对一个消息进行签名的示例,计算这条消息的哈希值,然后传递到 sign() 方法。你也可以使用其他算法,比如 DSA 或 ELGamal。

>>> from Crypto.Hash import SHA256
>>> from Crypto.PublicKey import RSA
>>> from Crypto import Random
>>> key = RSA.generate(1024, random_generator)
>>> text = 'abcdefgh'
>>> hash = SHA256.new(text).digest()
>>> hash
'\x9cV\xccQ\xb3t\xc3\xba\x18\x92\x10\xd5\xb6\xd4\xbfWy\r5\x1c\x96\xc4|\x02\x19\x0e\xcf\x1eC\x065\xab'
>>> signature = key.sign(hash, '')

验证

明文随着消息一起发送的情况下,知道公钥的情况下可以轻松验证消息。接收方计算哈希值,然后使用公共密钥与 verify() 方法验证来源。

>>> text = 'abcdefgh'
>>> hash = SHA256.new(text).digest()
>>> public_key.verify(hash, signature)
True