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


171

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

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

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

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

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

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

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


12
os.urandom 은 PyCrypto 웹 사이트 에서 권장 됩니다 . 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

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

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.