Python 3.2 - CBC模式下的SymétricAES加密 - 请求的建议

时间:2013-12-10 12:36:27

标签: python python-3.x cryptography aes

我的目标是使用python 3.x设置简单加密,所以我在本周末搜索了网页以获取有关RSA / AES等的信息......实际上,可能看起来像是加密文本数据的可能性以合理的安全方式传播..也没有偏执狂,我不是专家只是想确保没有钥匙的东西很难读!

老实说,我对密码学知之甚少。经过几个小时的搜索和收集信息和源代码后,由于Python 2.7中提供的示例导致无效长度问题或其他转换错误,我的尝试失败了。我在python 3中发现了很少的例子,并且使用的加密方法似乎并不合适或严重。

我终于能够运行以下接受ISO 8859-1编码字符的代码。我实际上用UTF-8编码封装了所有内容以避免语言问题。我希望如此......

我想知道我是否采用正确的设计方式,特别是如果数据安全性可以接受,我再也不是在寻找伟大的安全解决方案,只是想保护自己的个人数据而不是保护一个军事防御秘密哈哈!

随时转发您的意见或建议,特别是我可能错过的内容!

非常感谢每次前进。

Emmanuel(法国)

注意:下一步我将尝试将RSA加密的AES密码与文本流一起发送给收件人。由于每个消息的AES密码不同,客户端需要自动将其转换为能够解码密码消息。 AES密码将使用最强密钥以RSA非对称加密方式传输,而不会出现性能故障。目的是在合理的时间范围内传输简单的消息(没有base64编码)或大量数据。

@ +见。

要执行下面的代码,您应该安装PyCrypto(python 3.2)

import os, base64, hashlib
from Crypto.Cipher import AES

class Aes(object):

# Crypte / décrypte un texte donné en AES mode CBC. Accepte l'encodage base64.
# Encrypts input text string & decrypts bytes encoded string with or without base64 encoding
# Author: emmanuel.brunet@live.fr - 12/2013


SALT_LENGTH = 64
DERIVATION_ROUNDS=10000
BLOCK_SIZE = 16
KEY_SIZE = 256
MODE = AES.MODE_CBC

def encrypt(self, source, aes_key, outfile=None, base64_encode=False):
    '''
    Crypte l'entrée source en AES mode CBC avec sortie encodage base64 / fichier facultative

    @param str source: text to encode or text file path
    @param bytes aes_key: password
    @parm str outfile: disk file to write encoded text to. defaults to None
    @param bool base64_encode: returns base64 encoded string if True (for emails) or bytes if False

    @return bytes ciphertext: the bytes encoded string.
    '''


    '''
    ----------------------------
    Inputs management
    ----------------------------
    '''
    if os.path.exists(source):

        fp = open(source, 'rb')
        input_text = fp.read()
        fp.close()

    else:

        input_text = bytes(source, 'UTF-8')

    if input_text == b'':
        print('No data to encrypt')
        return

    padding_len = 16 - (len(input_text) % 16)         
    padded_text = str(input_text, 'UTF-8') + chr(padding_len) * padding_len

    '''
    ---------------------------------------------------------
    Computes the derived key (derived_key). 
    ---------------------------------------------------------
    Elle permet d'utiliser la clé initiale (aes_key) plusieurs 
    fois, une pour chaque bloc à encrypter.
    ---------------------------------------------------------
    '''

    salt = os.urandom(self.SALT_LENGTH)

    derived_key = bytes(aes_key, 'UTF-8')     

    for unused in range(0,self.DERIVATION_ROUNDS):

        derived_key = hashlib.sha256(derived_key + salt).digest()

    derived_key = derived_key[:self.KEY_SIZE]

    '''
    ----------------
    Encrypt
    ----------------
    '''      
    # The initialization vector should be random
    iv = os.urandom(self.BLOCK_SIZE)

    cipherSpec = AES.new(derived_key, self.MODE, iv)
    cipher_text = cipherSpec.encrypt(padded_text)
    cipher_text = cipher_text + iv + salt

    '''
    -------------------------
    Output management
    -------------------------
    '''
    if outfile is None:
        '''
        Returns cipher in base64 encoding. Useful for email management for instance
        '''
        if base64_encode:
            return(base64.b64encode(cipher_text))
        else:
            return(cipher_text)

    else:
        '''
        Writes result to disk
        '''

        fp = open(outfile, 'w')

        if base64_encode:
            fp.write(base64.b64encode(cipher_text))
        else:
            fp.write(cipher_text)

        fp.close()

        print('Cipher text saved in', outfile)


def decrypt(self, source, aes_key, outfile=None, base64_encode=False):
    '''
    Decrypts encoded string or data file

    @param bytes or str source: encrypted bytes string to decode or file path        
    @param bytes aes_key: password
    @parm str outfile: disk file to write encoded text to. defaults to None        
    @param bool base64_encode: cipher text is given base64 encoded (for mails content for examples)

    @returns str secret_text: the decoding text string or None if invalid key given
    '''

    '''
    ---------------------------
    Input management
    ---------------------------
    '''

    if type(source) == str and os.path.exists(source):

        fp = open(source, 'rb')
        ciphertext = fp.read()
        fp.close()

    elif type(source) == bytes:
        ciphertext = source

    else:
        print('Invalid data source')
        return

    if base64_encode:
        encoded_text = base64.b64decode(ciphertext)
    else:
        # decodedCiphertext = ciphertext.decode("hex")
        encoded_text = ciphertext

    '''
    -------------------------
    Computes derived key
    -------------------------
    '''

    iv_start = len(encoded_text) - self.BLOCK_SIZE - self.SALT_LENGTH
    salt_start = len(encoded_text) - self.SALT_LENGTH
    data, iv, salt = encoded_text[:iv_start], encoded_text[iv_start:salt_start], encoded_text[salt_start:]

    derived_key = bytes(aes_key, 'utf-8')

    for unused in range(0, self.DERIVATION_ROUNDS):
        derived_key = hashlib.sha256(derived_key + salt).digest()

    derived_key = derived_key[:self.KEY_SIZE]


    '''
    -------------------------
    Decrypt
    -------------------------
    '''
    Cipher = AES.new(derived_key, self.MODE, iv)
    padded_text = Cipher.decrypt(data)

    padding_length = padded_text[-1]
    secret_text = padded_text[:-padding_length]

    '''
    Si le flux n'est pas décodé (mot de passe invalide),  la conversion UTF-8 plante ou au mieux on obtient un texte illisible
    '''
    try:
        secret_text = str(secret_text, 'utf-8')
    except:
        return

    if outfile is None:

        return(secret_text)

    else:
        '''
        Writes result to disk
        '''
        fp = open(outfile, 'w')
        fp.write(secret_text)
        fp.close()

最终内容

我做了以下更改:

  • 使用PBKDF2作为KDF和HMAC-sha512
  • 修正了常数问题
  • 强制性套餐现在是:PyCypto& pbkdf2-1.3

我已经尝试了很多时间来插入新的代码块...但它不起作用。文本编辑器的行为非常奇怪。

3 个答案:

答案 0 :(得分:1)

你的表现比我预期的要好:P。 只需要几点建议来改进您的代码:

  • 如果你使用像PBKDF2这样漂亮,着名且强大的密钥派生函数和HMAC-sha256会更好。您的KDF看起来很强大,但在谈论加密时,最好依靠广泛审查的算法。
  • 您可以考虑使用os.random而不是os.urandom(或者至少可以简单地从一个切换到另一个)来获得更多的熵。
  • 您可以在加密输出中添加一些“标题”,这样您就可以在不知道密钥大小和其他可变内容之前解密它,现在这些内容是硬编码的。
  • 让用户更轻松地更改现在硬编码的设置。

另外,为了您的下一步,我建议您查看DH key exchange。这将为您提供完美的前瞻性保密。

答案 1 :(得分:1)

浪漫已经发表了一些有趣的评论,但我还有很多其他的评论。正如浮士德已经说过的那样,你似乎正朝着正确的方向前进。

  1. 使用PBKDF2进行密钥派生,而不是使用专有的KDF;
  2. 在密文的末尾添加HMAC,哈希盐以及有关算法的任何信息,在信任明文和填充之前检查HMAC;
  3. 请注意,对于127或以上的字符,UTF-8 编码 不兼容与ISO 8859-1编码,毫无疑问,它可以编码所有为 ISO 8859-1 定义的字符;
  4. 应该将盐放在密文之前,否则你无法有效解密(在开始解密之前你需要所有的密文);
  5. 可以将IV设置为全零(并且可能不发送)如果每次都生成一个随机盐(只有当密钥重新启用时才需要随机IV) - 二手);
  6. 在解密过程中,不要盲目依赖padding_length = padded_text[-1](参见有关HMAC的部分);
  7. 提供时使用常量,例如AES.block_size代替16;
  8. 使用库提供的随机数生成器,您可以接受基类BaseRNG中的任何内容,但默认情况下使用OSRNG
  9. 请注意,我发现Python加密的RNG类非常难以理解,如果找不到利用库中的方法的好方法,请保留os.urandom

答案 2 :(得分:0)

最后一个源版本0.3。希望它会帮助别人。

# -*- coding: utf-8 -*-
from Crypto.Cipher import AES
from Crypto.Hash import HMAC, SHA512
from pbkdf2 import PBKDF2
import os, base64, bz2, binascii

class Aes(object):
'''
Crypte / décrypte un texte donné en AES mode CBC. Accepte l'encodage base64.
Encrypts input text string & decrypts bytes encoded string with or without base64 encoding

PyCrypto and pbkdf2-1.3 packages are mandatory

Author: emmanuel.brunet@live.fr - 12/2013
''' 

SALT_LENGTH = 32 # 32 bytes = 256 bits salt
DERIVATION_ROUNDS=7000
KEY_SIZE = 32 # 256 bits key
MODE = AES.MODE_CBC

def encrypt(self, source, aes_key, outfile=None, base64_encode=False):
    '''
    Crypte l'entrée source en AES mode CBC avec sortie encodage base64 / fichier facultative

    @param str source: text to encode or text file path        
    @param bytes aes_key: password in byte
    @parm str outfile: disk file to write encoded text to. defaults to None
    @param bool base64_encode: returns base64 encoded string if True (for emails) or bytes if False

    @return bytes ciphertext: the bytes encoded string.
    '''


    '''
    ----------------------------
    Inputs management
    ----------------------------
    '''
    if os.path.exists(source):

        fp = open(source, 'rb')
        input_text = fp.read()
        fp.close()

    else:

        input_text = bytes(source, 'UTF-8')

    if input_text == b'':
        print('No data to encrypt')
        return

    '''
    # padding_len = AES.block_size - (len(input_text) % AES.block_size) 
    # padded_text = str(input_text, 'UTF-8') + chr(padding_len) * padding_len 
    '''

    '''
    -------------------
    Compress
    ------------------
    '''
    cmp_text = bz2.compress(input_text)
    b64_bin = base64.b64encode(cmp_text)
    b64_str = str(b64_bin, 'UTF-8')

    padding_len = AES.block_size - (len(b64_str) % AES.block_size) 
    padded_text = b64_str + chr(padding_len) * padding_len

    '''
    ---------------------------------------------------------
    Derived key computing PBKDF2 / specs RSA PKCS#5 V2.0 
    ---------------------------------------------------------
    '''

    salt = os.urandom(self.SALT_LENGTH)

    derived_key = PBKDF2(bytes(aes_key, 'UTF-8'), salt, iterations=self.DERIVATION_ROUNDS, digestmodule=SHA512, macmodule=HMAC).read(self.KEY_SIZE)

    '''
    ----------------
    Encrypt
    ----------------
    '''      
    # le vecteur d'initialisation doit être aléatoire
    iv = os.urandom(AES.block_size)

    Cipher = AES.new(derived_key, self.MODE, iv)
    cipher_text = Cipher.encrypt(padded_text)

    cipher_text = cipher_text + iv + salt
    # cipher_text = salt + cipher_text

    '''
    -------------------------
    Output management
    -------------------------
    '''
    if outfile is None:
        '''
        Returns cipher in base64 encoding. Useful for email management for instance
        '''
        if base64_encode:
            return(base64.b64encode(cipher_text))
        else:
            return(cipher_text)

    else:
        '''
        Writes result to disk
        '''

        fp = open(outfile, 'w')

        if base64_encode:
            fp.write(base64.b64encode(cipher_text))
        else:
            fp.write(cipher_text)

        fp.close()

        print('Cipher text saved in', outfile)


def decrypt(self, source, aes_key, outfile=None, base64_encode=False):
    '''
    @param bytes or str source: encrypted bytes string to decode or file path        
    @param bytes aes_key: password
    @parm str outfile: disk file to write encoded text to. defaults to None        
    @param bool base64_encode: cipher text is given base64 encoded (for mails content for examples)

    @returns str secret_text: the decoding text string or None if invalid key given
    '''

    '''
    ---------------------------
    Input management
    ---------------------------
    '''

    if type(source) == str and os.path.exists(source):

        fp = open(source, 'rb')
        ciphertext = fp.read()
        fp.close()

    elif type(source) == bytes:

        ciphertext = source

    else:
        print('Invalid data source')
        return

    if base64_encode:

        encoded_text = base64.b64decode(ciphertext)

    else:

        encoded_text = ciphertext

    salt_start = len(encoded_text) - self.SALT_LENGTH
    iv_start = len(encoded_text) - AES.block_size - self.SALT_LENGTH        
    data, iv, salt = encoded_text[:iv_start], encoded_text[iv_start:salt_start], encoded_text[salt_start:]

    '''
    -------------------------
    Derived key computing
    -------------------------
    '''

    # derived_key = PBKDF2(bytes(aes_key, 'UTF-8'), salt).read(self.KEY_SIZE)
    derived_key = PBKDF2(bytes(aes_key, 'UTF-8'), salt, iterations=self.DERIVATION_ROUNDS, digestmodule=SHA512, macmodule=HMAC).read(self.KEY_SIZE)   

    '''
    -------------------------
    Decrypt
    -------------------------
    '''
    Cipher = AES.new(derived_key, self.MODE, iv)
    padded_text = Cipher.decrypt(data)

    padding_length = padded_text[-1]
    secret_text = padded_text[:-padding_length]

    '''
    --------------------------
    Decompress
    --------------------------
    '''

    cmp_text = base64.b64decode(secret_text)
    secret_text = bz2.decompress(cmp_text)

    '''
    Si le flux n'est pas décodé (mot de passe invalide),  la conversion UTF-8 plante ou au mieux on obtient un texte illisible
    '''
    try:
        secret_text = str(secret_text, 'utf-8')
    except:
        return

    if outfile is None:

        return(secret_text)

    else:
        '''
        Writes result to disk
        '''
        fp = open(outfile, 'w')
        fp.write(secret_text)
        fp.close()