PyCrypto AES 256을 사용한 암호화 및 복호화


171

PyCrypto를 사용하여 메시지와 키라는 두 가지 매개 변수를 허용하고 메시지를 암호화 / 해독하는 두 가지 기능을 작성하려고합니다.

웹에서 도움이되는 몇 가지 링크를 찾았지만 각 링크에 결함이 있습니다.

codekoala에서 이것은 os.urandom을 사용하는데, 이는 PyCrypto에 의해 권장되지 않습니다.

또한 함수에 제공하는 키가 정확한 길이를 보장하지는 않습니다. 그렇게하려면 어떻게해야합니까?

또한 몇 가지 모드가 있는데 어떤 모드가 권장됩니까? 나는 무엇을 사용 해야할지 모르겠다 : /

마지막으로 IV는 정확히 무엇입니까? 암호화 및 암호 해독을 위해 다른 IV를 제공 할 수 있습니까? 그렇지 않으면 다른 결과가 나옵니까?

편집 : 코드 부분이 안전하지 않아 제거되었습니다.


12
os.urandomPyCrypto 웹 사이트 에서 권장 됩니다 . CSPRNG 인 Microsoft의 CryptGenRandom 함수를 사용합니다.
Joel Vroom

5
또는 /dev/urandom유닉스에
Joel Vroom

2
다만이 실시 예에서, 명확하게 암호 는 IS 128, 192, 또는 256 비트 (16, 24, 또는 32 바이트)가 될 수있다
마크

4
PyCrypto는 죽은 프로젝트 라고 언급 할 가치가 있습니다 . 마지막 커밋은 2014 년입니다. PyCryptodome 은 훌륭한 드롭 인 대체품처럼 보입니다
Overdrivr

1
이 질문은 오래되었지만 pycrypto가 오래되어 더 이상 지원되지 않을 가능성이 2020 년 현재입니다. 자신의 GitHub의 페이지 (을 살펴보면 github.com/pycrypto/pycrypto ), 그들의 마지막으로 내가 개발 더 이상 암호화 소프트웨어 사용 의심 될 거라고 2014 년이었다 커밋 표시
irritable_phd_syndrom

답변:


151

다음은 내 구현이며 일부 수정 사항으로 작동하며 키 및 비밀 문구의 정렬을 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 = AES.block_size
        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.encode()))

    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:])]

14
나는 이것이 한동안 일어났다는 것을 알고 있지만이 반응이 혼란을 일으킬 수 있다고 생각합니다. 이 함수는 32 바이트 (256 바이트)의 block_size를 사용하여 입력 데이터를 채우지 만 AES는 128 비트 블록 크기를 사용합니다. AES256에서 는 256 비트이지만 블록 크기는 아닙니다.
Tannin

13
다시 말해서, "self.bs"는 "AES.block_size"로 대체되어야합니다
Alexis

2
왜 키를 해시하고 있습니까? 이것이 암호와 같은 것으로 예상되면 SHA256을 사용하지 않아야합니다. PyCrypto가 제공하는 PBKDF2와 같은 키 파생 함수를 사용하는 것이 좋습니다.
tweaksp

5
@Chris-SHA256은 AES256을위한 완벽한 크기의 키인 32 바이트 해시를 제공합니다. 키의 생성 / 파생은 무작위 / 보안으로 가정되며 암호화 / 암호 해독 코드의 범위를 벗어나야합니다. 해싱은 선택한 암호로 키를 사용할 수 있다는 것을 보증합니다.
zwer

2
_pad에서 self.bs 액세스가 필요하고 _unpad에서 필요하지 않음
mnothic

149

입력 길이가 BLOCK_SIZE의 배수가 아닌 경우- pad암호화 (패딩시) 및 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의 배수 인 입력이 있으면 어떻게됩니까? 언 패드 기능이 약간 혼란 스러울 것이라고 생각합니다.
Kjir

2
@Kjir이면 BLOCK_SIZE 길이의 chr (BS) 값 시퀀스가 ​​원점 데이터에 추가됩니다.
Marcus

1
@Marcus pad함수가 손상되었습니다 (적어도 Py3 s[:-ord(s[len(s)-1:])]에서는).
Torxed

2
@Torxed pad 함수는 pycryptodome (pycrypto followup)과 함께 CryptoUtil.Padding.pad ()에서 사용할 수 있습니다
comte

2
왜 패딩 문자로 문자 상수를 사용하지 않습니까?
Inaimathi

16

"모드"에 대한 질문을하겠습니다. 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이라고합니다. PyCrypto와 함께 AES-CBC를 사용할 때는주의를 기울여야 합니다. 그 이유는 주어진 다른 솔루션으로 예시 된 것처럼 패딩 체계 를 지정해야하기 때문입니다 . 당신이하지 않은 경우 일반적으로, 매우 패딩주의, 거기에 공격 을 완전히 암호화 휴식!

이제 키는 임의의 32 바이트 문자열 이어야 합니다 . 암호로 충분 하지 않습니다 . 일반적으로 키는 다음과 같이 생성됩니다.

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

키 도 password에서 파생 될 수 있습니다 .

# 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을 사용하는 것이 좋지만 일반적으로 잘못된 암호화 방식 으로 간주됩니다 . 작동 모드에 대한 자세한 내용은 Wikipedia 를 확인하십시오 .


iv_int = int (binascii.hexlify (iv), 16)가 작동하지 않으면 iv_int = int (binascii.hexlify (iv), 16)와 'import binascii'를 추가하고 작동합니다 (Python 3.x에서) ), 그렇지 않으면 위대한 작품!
Valmond

Autehnticated Encryption 모드를 AES-GCM으로 사용하는 것이 좋습니다. GCM은 내부적으로 CTR 모드를 사용합니다.
kelalaka

이 코드는 "TypeError : Object type <class 'str'>을 C 코드로 전달할 수 없습니다"
Da Woon Jung

7

urlsafe_b64encode 및 urlsafe_b64decode를 사용하려는 사람은 다음과 같습니다 (유니 코드 문제와 시간을 보낸 후)

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:]))

6

당신은 암호 해시 함수 (사용하여 임의의 암호에서 암호를 얻을 수 없습니다 파이썬의 내장 hashSHA-1, SHA-256 등). 파이썬은 표준 라이브러리에서 두 가지를 모두 지원합니다.

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]그것을 당신이 지정한 길이로 보안을 유지합니다.


13
비밀번호에서 키를 생성하기 위해 SHA 계열 해시 함수를 사용해서는 안됩니다 . 주제에 대한 Coda Hale의 에세이를 참조하십시오 . 대신 scrypt 와 같은 실제 키 파생 함수를 사용하십시오. (코다 헤일의 에세이는 scrypt의 게시하기 전에 작성되었습니다.)
벤자민 Barenblat

7
향후 독자를 위해 암호 문구에서 키를 파생 시키려면 PBKDF2를 찾으십시오. 파이썬 ( pypi.python.org/pypi/pbkdf2 ) 에서 사용하기가 상당히 쉽습니다 . 그러나 비밀번호를 해시하려는 경우 bcrypt가 더 나은 옵션입니다.
C Fairweather

6

영감을 얻었지만 효과가 없었던 다른 답변에 감사드립니다.

어떻게 작동하는지 알아 내려고 시간을 보내고 난 후에, 나는 최신와 아래의 구현 해낸 PyCryptodomex의 라이브러리 (그것은 내가 VIRTUALENV .. 휴에, Windows에서 프록시 뒤에를 설정하는 관리 방법 또 다른 이야기이다)

에 작업이 구현시 패딩, 인코딩, 암호화 단계 (및 그 반대로)를 기록해야합니다. 순서를 염두에두고 포장을 풀고 포장을 풀어야합니다.

수입 base64
수입 해시
Cryptodome.Cipher import AES에서
Cryptodome.Random 가져 오기 get_random_bytes에서

__key__ = hashlib.sha256 (b'16 자 키 ') .digest ()

데프 암호화 (원시) :
    BS = AES.block_size
    패드 = λs : s + (BS-len (s) % BS) * chr (BS-len (s) % BS)

    raw = base64.b64encode (패드 (raw) .encode ( 'utf8'))
    iv = get_random_bytes (AES.block_size)
    암호 = AES.new (키 = __key__, 모드 = AES.MODE_CFB, iv = iv)
    base64.b64encode (iv + cipher.encrypt (raw))를 반환

데프 해독 (enc) :
    unpad = 람다 s : s [:-ord (s [-1 :])]

    enc = base64.b64decode (enc)
    iv = enc [: AES.block_size]
    암호 = AES.new (__ key__, AES.MODE_CFB, iv)
    unpad (base64.b64decode (cipher.decrypt (enc [AES.block_size :])). decode ( 'utf8') 반환

PyCryptodomeX 라이브러리를 사용하여이 기능을 수행 할 수있는 좋은 예에 감사드립니다. 매우 도움이됩니다!
Ygramul

5

다른 사람들을 위해 @Cyril과 @Marcus의 답변을 결합하여 얻은 해독 구현이 있습니다. 이것은 암호화 된 텍스트가 인용되고 base64로 인코딩 된 HTTP 요청을 통해 들어오는 것으로 가정합니다.

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()

5

이것에 대한 또 다른 견해 (위의 솔루션에서 크게 파생 됨)

  • 패딩에 null을 사용
  • 람다를 사용하지 않습니다 (팬이 아님)
  • 파이썬 2.7 및 3.6.5로 테스트

    #!/usr/bin/python2.7
    # you'll have to adjust for your setup, e.g., #!/usr/bin/python3
    
    
    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)
            raw = raw.encode('utf-8')
            iv = Random.new().read( AES.block_size )
            cipher = AES.new( self.key.encode('utf-8'), 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.encode('utf-8'), AES.MODE_CBC, iv )
            return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf-8')

decrypt () 함수에서 후행 널과 패딩 널을 더하기 때문에 입력 바이트 []에 후행 널이 있으면 작동하지 않습니다.
Buzz Moschetti

예, 위에서 언급했듯이이 논리는 null로 채워집니다. 인코딩 / 디코딩하려는 항목에 후행 null이있는 경우 여기에서 다른 솔루션 중 하나를 사용하는 것이 좋습니다.
MIkee

3

나는 라이브러리 CryptoPyCryptodomex라이브러리를 모두 사용했으며 빠르게 타 오르고 있습니다 ...

import base64
import hashlib
from Cryptodome.Cipher import AES as domeAES
from Cryptodome.Random import get_random_bytes
from Crypto import Random
from Crypto.Cipher import AES as cryptoAES

BLOCK_SIZE = AES.block_size

key = "my_secret_key".encode()
__key__ = hashlib.sha256(key).digest()
print(__key__)

def encrypt(raw):
    BS = cryptoAES.block_size
    pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
    raw = base64.b64encode(pad(raw).encode('utf8'))
    iv = get_random_bytes(cryptoAES.block_size)
    cipher = cryptoAES.new(key= __key__, mode= cryptoAES.MODE_CFB,iv= iv)
    a= base64.b64encode(iv + cipher.encrypt(raw))
    IV = Random.new().read(BLOCK_SIZE)
    aes = domeAES.new(__key__, domeAES.MODE_CFB, IV)
    b = base64.b64encode(IV + aes.encrypt(a))
    return b

def decrypt(enc):
    passphrase = __key__
    encrypted = base64.b64decode(enc)
    IV = encrypted[:BLOCK_SIZE]
    aes = domeAES.new(passphrase, domeAES.MODE_CFB, IV)
    enc = aes.decrypt(encrypted[BLOCK_SIZE:])
    unpad = lambda s: s[:-ord(s[-1:])]
    enc = base64.b64decode(enc)
    iv = enc[:cryptoAES.block_size]
    cipher = cryptoAES.new(__key__, cryptoAES.MODE_CFB, iv)
    b=  unpad(base64.b64decode(cipher.decrypt(enc[cryptoAES.block_size:])).decode('utf8'))
    return b

encrypted_data =encrypt("Hi Steven!!!!!")
print(encrypted_data)
print("=======")
decrypted_data = decrypt(encrypted_data)
print(decrypted_data)

2

조금 늦었지만 이것이 매우 도움이 될 것이라고 생각합니다. PKCS # 7 패딩과 같은 사용 체계에 대해서는 아무도 언급하지 않았습니다. 이전 함수 대신 pad (암호화를 할 때)와 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())


누가 답을 내려 놓았는지 모르겠지만 왜 그런지 궁금합니다. 이 방법이 안전하지 않습니까? 설명이 좋을 것입니다.
Cyril N.

1
@CyrilN. 이 답변은 단일 호출 SHA-256으로 비밀번호를 해시하는 것으로 충분하다는 것을 나타냅니다. 그렇지 않습니다. 반복 횟수가 큰 암호에서 키 파생을 위해서는 PBKDF2 또는 이와 유사한 것을 사용해야합니다.
Artjom B.

세부 사항 @ArtjomB.에 감사합니다!
Cyril N.

키와 44 길이의 iv 키가 있습니다. 어떻게 기능을 사용할 수 있습니까?! 내가 찾은 인터넷의 모든 알고리즘은 내 벡터 키의 길이에 문제가있다
mahshid.r


1
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:])

10
코드뿐만 아니라 수행중인 작업과 이것이 더 나은 이유 / 기존 답변과의 차이점을 설명하십시오.
Florian Koch

md5.new (key) .digest ()를 md5 (key) .digest ()로 바꾸면 매력처럼 작동합니다!
스테파니
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.