2012-09-21 200 views
112

我試圖建立兩個函數使用PyCrypto接受兩個參數:消息和密鑰,然後加密/解密消息。加密和解密使用PyCrypto AES 256

我在網上找到了幾個環節,以幫助我,但他們每個人都有缺點:

This one at codekoala使用os.urandom,這是由PyCrypto氣餒。

此外,我給函數的關鍵不能保證具有預期的確切長度。我該怎麼做才能做到這一點?

此外,還有幾種模式,推薦哪一種?我不知道該用什麼:/

最後,到底什麼是IV?我可以提供不同的IV用於加密和解密,還是會以不同的結果返回?

這是我到目前爲止已經完成:

from Crypto import Random 
from Crypto.Cipher import AES 
import base64 

BLOCK_SIZE=32 

def encrypt(message, passphrase): 
    # passphrase MUST be 16, 24 or 32 bytes long, how can I do that ? 
    IV = Random.new().read(BLOCK_SIZE) 
    aes = AES.new(passphrase, AES.MODE_CFB, IV) 
    return base64.b64encode(aes.encrypt(message)) 

def decrypt(encrypted, passphrase): 
    IV = Random.new().read(BLOCK_SIZE) 
    aes = AES.new(passphrase, AES.MODE_CFB, IV) 
    return aes.decrypt(base64.b64decode(encrypted)) 
+10

[os.urandom](http://docs.python.org/3/library/os.html)被_encouraged_上的[PyCrypto](https://www.dlitz.net/software/pycrypto/)網站。它使用Microsoft的[CryptGenRandom](http://msdn.microsoft.com/en-us/library/windows/desktop/aa379942(V = vs.85)的.aspx)函數,它是一個[CSPRNG](HTTP:// en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) –

+4

或'的/ dev/urandom'在Unix –

+1

只是爲了澄清,在該示例** **密碼是其可以是128,192,或256位(16,24的_key_ ,或32個字節) – Mark

回答

84

這裏是我的實現,並與一些修復工作對我來說並增強了與32個字節和IV的密鑰和密碼短語的對齊方式爲16個字節:

import base64 
import hashlib 
from Crypto import Random 
from Crypto.Cipher import AES 

class AESCipher(object): 

    def __init__(self, key): 
     self.bs = 32 
     self.key = hashlib.sha256(key.encode()).digest() 

    def encrypt(self, raw): 
     raw = self._pad(raw) 
     iv = Random.new().read(AES.block_size) 
     cipher = AES.new(self.key, AES.MODE_CBC, iv) 
     return base64.b64encode(iv + cipher.encrypt(raw)) 

    def decrypt(self, enc): 
     enc = base64.b64decode(enc) 
     iv = enc[:AES.block_size] 
     cipher = AES.new(self.key, AES.MODE_CBC, iv) 
     return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8') 

    def _pad(self, s): 
     return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs) 

    @staticmethod 
    def _unpad(s): 
     return s[:-ord(s[len(s)-1:])] 
+8

我知道這已經持續了一段時間,但我認爲這種迴應可能會傳播一些混亂。此函數使用32字節(256字節)的block_size填充輸入數據,但AES使用128位塊大小。在AES256中*鍵*是256位,但不是塊大小。 – Tannin

+4

換句話說,「self.bs」應該被刪除並替換爲「AES.block_size」 – Alexis

+0

你爲什麼要哈希鍵?如果你期待這是一個密碼,那麼你不應該使用SHA256;最好使用密鑰派生函數,比如PyCrypto提供的PBKDF2。 – tweaksp

6

您可以通過使用密碼散列函數( Python的內置hash),如SHA-1或SHA得到一個密碼出任意密碼-256。 Python的包括其標準庫均支持:

import hashlib 

hashlib.sha1("this is my awesome password").digest() # => a 20 byte string 
hashlib.sha256("another awesome password").digest() # => a 32 byte string 

可以截斷的加密哈希值只是通過使用[:16][:24],它會保留其安全性達到您所指定的長度。

+10

你不應該使用SHA-家庭哈希函數從密碼生成密鑰 - 參見[科達硬朗的在話題作文(http://codahale.com/how-to-safely-store-a-password/) 。考慮使用像[scrypt](https://pypi.python.org/pypi/scrypt/)這樣的真正的[鍵派生函數](https://en.wikipedia.org/wiki/Key_derivation_function)。 (Coda Hale的文章是在scrypt出版之前撰寫的。) –

+5

對於未來的讀者,如果您希望從密碼中派生密鑰,請查找PBKDF2。在python中使用相當簡單(https://pypi.python.org/pypi/pbkdf2)。但是,如果你想要散列密碼,bcrypt是一個更好的選擇。 –

130

當輸入的長度不是BLOCK_SIZE的倍數時,您可能需要以下兩個函數來填充(何時進行加密)和unpad(何時進行解密)。

BS = 16 
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[:-ord(s[len(s)-1:])] 

所以你問的關鍵長度?您可以使用密鑰的md5sum而不是直接使用它。

更多的是,根據我使用PyCrypto的一些經驗,當輸入相同時,IV用於混合加密的輸出,所以IV被選爲隨機字符串,並將其用作加密的一部分輸出,然後用它來解密消息。

下面是我的實現,希望這將是對您有用:

import base64 
from Crypto.Cipher import AES 
from Crypto import Random 

class AESCipher: 
    def __init__(self, key): 
     self.key = key 

    def encrypt(self, raw): 
     raw = pad(raw) 
     iv = Random.new().read(AES.block_size) 
     cipher = AES.new(self.key, AES.MODE_CBC, iv) 
     return base64.b64encode(iv + cipher.encrypt(raw)) 

    def decrypt(self, enc): 
     enc = base64.b64decode(enc) 
     iv = enc[:16] 
     cipher = AES.new(self.key, AES.MODE_CBC, iv) 
     return unpad(cipher.decrypt(enc[16:])) 
+1

如果您的輸入完全是BLOCK_SIZE的倍數,會發生什麼情況?我認爲unpad函數會產生一些困惑...... – Kjir

+1

@Kjir,則長度爲BLOCK_SIZE的值序列chr(BS)將被附加到原始數據。 – Marcus

+0

你說得對,現在'pad'和'unpad'函數更清晰了,謝謝! – Kjir

5

爲他人謀取利益,這裏是我的解密實現,我結合@Cyril和@Marcus的答案得。這假定這是通過HTTP Request與加密文本引用和base64編碼進來的。

import base64 
import urllib2 
from Crypto.Cipher import AES 


def decrypt(quotedEncodedEncrypted): 
    key = 'SecretKey' 

    encodedEncrypted = urllib2.unquote(quotedEncodedEncrypted) 

    cipher = AES.new(key) 
    decrypted = cipher.decrypt(base64.b64decode(encodedEncrypted))[:16] 

    for i in range(1, len(base64.b64decode(encodedEncrypted))/16): 
     cipher = AES.new(key, AES.MODE_CBC, base64.b64decode(encodedEncrypted)[(i-1)*16:i*16]) 
     decrypted += cipher.decrypt(base64.b64decode(encodedEncrypted)[i*16:])[:16] 

    return decrypted.strip() 
6

對於有人誰願意使用urlsafe_b64encode和urlsafe_b64decode,這裏的版本that're工作對我來說(花一些時間與Unicode的問題後)

BS = 16 
key = hashlib.md5(settings.SECRET_KEY).hexdigest()[:BS] 
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 
unpad = lambda s : s[:-ord(s[len(s)-1:])] 

class AESCipher: 
    def __init__(self, key): 
     self.key = key 

    def encrypt(self, raw): 
     raw = pad(raw) 
     iv = Random.new().read(AES.block_size) 
     cipher = AES.new(self.key, AES.MODE_CBC, iv) 
     return base64.urlsafe_b64encode(iv + cipher.encrypt(raw)) 

    def decrypt(self, enc): 
     enc = base64.urlsafe_b64decode(enc.encode('utf-8')) 
     iv = enc[:BS] 
     cipher = AES.new(self.key, AES.MODE_CBC, iv) 
     return unpad(cipher.decrypt(enc[BS:])) 
+0

這代碼將打破,如果'BS = 32' –

0
from Crypto import Random 
from Crypto.Cipher import AES 
import base64 

BLOCK_SIZE=16 
def trans(key): 
    return md5.new(key).digest() 

def encrypt(message, passphrase): 
    passphrase = trans(passphrase) 
    IV = Random.new().read(BLOCK_SIZE) 
    aes = AES.new(passphrase, AES.MODE_CFB, IV) 
    return base64.b64encode(IV + aes.encrypt(message)) 

def decrypt(encrypted, passphrase): 
    passphrase = trans(passphrase) 
    encrypted = base64.b64decode(encrypted) 
    IV = encrypted[:BLOCK_SIZE] 
    aes = AES.new(passphrase, AES.MODE_CFB, IV) 
    return aes.decrypt(encrypted[BLOCK_SIZE:]) 
+6

請提供不僅代碼,但也解釋你在做什麼,爲什麼這是更好/什麼是對現有答案的區別。 –

2

這有點晚,但我認爲這會非常有幫助。沒有人提到像PKCS#7填充這樣的使用方案。你可以使用它代替之前的函數來填充(何時進行加密)和unpad(何時解密).i將在下面提供完整的源代碼。

import base64 
import hashlib 
from Crypto import Random 
from Crypto.Cipher import AES 
import pkcs7 
class Encryption: 

    def __init__(self): 
     pass 

    def Encrypt(self, PlainText, SecurePassword): 
     pw_encode = SecurePassword.encode('utf-8') 
     text_encode = PlainText.encode('utf-8') 

     key = hashlib.sha256(pw_encode).digest() 
     iv = Random.new().read(AES.block_size) 

     cipher = AES.new(key, AES.MODE_CBC, iv) 
     pad_text = pkcs7.encode(text_encode) 
     msg = iv + cipher.encrypt(pad_text) 

     EncodeMsg = base64.b64encode(msg) 
     return EncodeMsg 

    def Decrypt(self, Encrypted, SecurePassword): 
     decodbase64 = base64.b64decode(Encrypted.decode("utf-8")) 
     pw_encode = SecurePassword.decode('utf-8') 

     iv = decodbase64[:AES.block_size] 
     key = hashlib.sha256(pw_encode).digest() 

     cipher = AES.new(key, AES.MODE_CBC, iv) 
     msg = cipher.decrypt(decodbase64[AES.block_size:]) 
     pad_text = pkcs7.decode(msg) 

     decryptedString = pad_text.decode('utf-8') 
     return decryptedString 

import StringIO 
import binascii 


def decode(text, k=16): 
    nl = len(text) 
    val = int(binascii.hexlify(text[-1]), 16) 
    if val > k: 
     raise ValueError('Input is not padded or padding is corrupt') 

    l = nl - val 
    return text[:l] 


def encode(text, k=16): 
    l = len(text) 
    output = StringIO.StringIO() 
    val = k - (l % k) 
    for _ in xrange(val): 
     output.write('%02x' % val) 
    return text + binascii.unhexlify(output.getvalue()) 

+0

我不知道誰低估了答案,但我很想知道爲什麼。也許這種方法不安全?解釋會很棒。 –

+0

至少我不是那個倒下來的人:) – xXxpRoGrAmmErxXx

+1

@CyrilN。這個答案表明用SHA-256的單個調用來散列密碼就足夠了。事實並非如此。你真的應該使用PBKDF2或類似的密碼從密碼派生使用大量的迭代計數。 –

2

另取這個(在很大程度上借鑑的解決方案衍生以上),但

  • 用於填充使用空
  • 不使用拉姆達(從不一直是粉絲)

    #!/usr/bin/env python 
    
    import base64, re 
    from Crypto.Cipher import AES 
    from Crypto import Random 
    from django.conf import settings 
    
    class AESCipher: 
        """ 
         Usage: 
         aes = AESCipher(settings.SECRET_KEY[:16], 32) 
         encryp_msg = aes.encrypt('ppppppppppppppppppppppppppppppppppppppppppppppppppppppp') 
         msg = aes.decrypt(encryp_msg) 
         print("'{}'".format(msg)) 
        """ 
        def __init__(self, key, blk_sz): 
         self.key = key 
         self.blk_sz = blk_sz 
    
        def encrypt(self, raw): 
         if raw is None or len(raw) == 0: 
          raise NameError("No value given to encrypt") 
         raw = raw + '\0' * (self.blk_sz - len(raw) % self.blk_sz) 
         iv = Random.new().read(AES.block_size) 
         cipher = AES.new(self.key, AES.MODE_CBC, iv) 
         return base64.b64encode(iv + cipher.encrypt(raw)).decode('utf-8') 
    
        def decrypt(self, enc): 
         if enc is None or len(enc) == 0: 
          raise NameError("No value given to decrypt") 
         enc = base64.b64decode(enc) 
         iv = enc[:16] 
         cipher = AES.new(self.key, AES.MODE_CBC, iv) 
         return re.sub(b'\x00*$', b'', cipher.decrypt(enc[16:])).decode('utf-8') 
    
+0

如果輸入字節[]具有拖尾空值,那麼這將不起作用,因爲在decrypt()函數中,您將填充空白填充任何拖尾的空值。 –

+0

是的,正如我在上面所述,這個邏輯電路填充了空值。如果您想編碼/解碼的項目可能會有空值,那麼最好在這裏使用其他解決方案之一 – MIkee

5

讓我解決你的問題 「的模式。」 AES256是一種分組密碼。它需要輸入一個32字節的密鑰和一個16字節的字符串,稱爲並輸出一個塊。爲了加密,我們在操作模式中使用AES。上面的解決方案建議使用CBC,這是一個例子。另一個稱爲CTR,它使用起來有些簡單:

from Crypto.Cipher import AES 
from Crypto.Util import Counter 
from Crypto import Random 

# AES supports multiple key sizes: 16 (AES128), 24 (AES192), or 32 (AES256). 
key_bytes = 32 

# Takes as input a 32-byte key and an arbitrary-length plaintext and returns a 
# pair (iv, ciphtertext). "iv" stands for initialization vector. 
def encrypt(key, plaintext): 
    assert len(key) == key_bytes 

    # Choose a random, 16-byte IV. 
    iv = Random.new().read(AES.block_size) 

    # Convert the IV to a Python integer. 
    iv_int = int(binascii.hexlify(iv), 16) 

    # Create a new Counter object with IV = iv_int. 
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int) 

    # Create AES-CTR cipher. 
    aes = AES.new(key, AES.MODE_CTR, counter=ctr) 

    # Encrypt and return IV and ciphertext. 
    ciphertext = aes.encrypt(plaintext) 
    return (iv, ciphertext) 

# Takes as input a 32-byte key, a 16-byte IV, and a ciphertext, and outputs the 
# corresponding plaintext. 
def decrypt(key, iv, ciphertext): 
    assert len(key) == key_bytes 

    # Initialize counter for decryption. iv should be the same as the output of 
    # encrypt(). 
    iv_int = int(iv.encode('hex'), 16) 
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int) 

    # Create AES-CTR cipher. 
    aes = AES.new(key, AES.MODE_CTR, counter=ctr) 

    # Decrypt and return the plaintext. 
    plaintext = aes.decrypt(ciphertext) 
    return plaintext 

(iv, ciphertext) = encrypt(key, 'hella') 
print decrypt(key, iv, ciphertext) 

這通常被稱爲AES-CTR。 我建議在使用AES-CBC和PyCrypto時慎用。原因是它要求您指定填充方案,如其他解決方案所示。一般來說,如果你不是細心的填充,還有attacks那個完全破解加密!

現在,重要的是要注意,密鑰必須是隨機的32字節字符串;密碼不需要就夠了。通常情況下,關鍵是像這樣產生的:

# Nominal way to generate a fresh key. This calls the system's random number 
# generator (RNG). 
key1 = Random.new().read(key_bytes) 

可將鑰匙從密碼,也得出:

# It's also possible to derive a key from a password, but it's important that 
# the password have high entropy, meaning difficult to predict. 
password = "This is a rather weak password." 

# For added # security, we add a "salt", which increases the entropy. 
# 
# In this example, we use the same RNG to produce the salt that we used to 
# produce key1. 
salt_bytes = 8 
salt = Random.new().read(salt_bytes) 

# Stands for "Password-based key derivation function 2" 
key2 = PBKDF2(password, salt, key_bytes) 

一些解決方案上面的建議使用SHA256推導的關鍵,但這是一般認爲是bad cryptographic practice。 查看wikipedia瞭解更多關於操作模式的信息。