PyCrypto AES 256を使用した暗号化と復号化


171

PyCryptoを使用して、メッセージとキーの2つのパラメーターを受け入れる2つの関数を作成し、メッセージを暗号化/復号化しようとしています。

私は私を助けるためにウェブ上にいくつかのリンクを見つけましたが、それらのそれぞれに欠陥があります:

codekoalaでのこれはos.urandomを使用していますが、PyCryptoでは推奨されません。

さらに、私が関数に与えるキーは、正確な長さが期待されるとは限りません。それを実現するにはどうすればよいですか?

また、いくつかのモードがありますが、どれがお勧めですか?何を使うかわかりません:/

最後に、IVは正確には何ですか?暗号化と復号化に別のIVを提供できますか、それとも別の結果になりますか?

編集:安全ではなかったため、コード部分を削除しました。


12
os.urandomPyCrypto Webサイトで推奨されてます。これは、CSPRNGであるMicrosoftのCryptGenRandom関数を使用しています
Joel Vroom 2013年

5
または/dev/urandomUnix
Joel Vroom 2013年

2
ただ、この例では、明確にするために、パスフレーズであるキー 128、192、または256ビット(16、24、または32バイト)であることができる
マーク

4
PyCryptoは死んだプロジェクトであることを言及する価値があるかもしれません。最後のコミットは2014年からです。PyCryptodomeは優れたドロップイン置換のように見えます
Overdrivr

1
この質問は古いですが、(2020年の時点で)pycryptoは古くなっており、サポートされなくなっている可能性があることを指摘したいと思います。彼らの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ビットですが、ブロックサイズではありません。
タンニン

13
言い換えると、「self.bs」を削除して「AES.block_size」に置き換える必要があります
Alexis

2
なぜキーをハッシュしているのですか?これがパスワードのようなものであると予想している場合は、SHA256を使用しないでください。PyCryptoが提供するPBKDF2のような鍵導出関数を使用する方が良いでしょう。
tweaksp 2017年

5
@Chris-SHA256は32バイトのハッシュ(AES256に最適なサイズのキー)を提供します。キーの生成/派生はランダム/安全であると想定されており、暗号化/復号化コードの範囲外である必要があります。ハッシュは、選択した暗号でキーが使用できることを保証するだけです。
zwer 2017年

2
_padでself.bsアクセスが必要で、_unpadで不要
mnothic

149

次の2つの機能が必要になる場合がありますpad-パッド(暗号化を行うとき)とunpad- unpadに(復号化を行うとき)入力の長さは、BLOCK_SIZEの倍数でないとき。

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 2013年

2
@Kjir、その後、長さBLOCK_SIZEの値chr(BS)のシーケンスが元のデータに追加されます。
Marcus、

1
@Marcus pad(少なくともPy3では)関数が壊れs[:-ord(s[len(s)-1:])]ています。バージョン間で機能するように置き換えてください。
2014

2
@Torxedパッド機能がpycryptodome(pycryptoフォロー)と)(CryptoUtil.Padding.padに無駄である
Comteの

2
パディング文字として文字定数を持たないのはなぜですか?
Inaimathi 2017

16

「モード」についてのご質問にお答えしましょう。AES256は一種のブロック暗号です。ブロックと呼ばれる32バイトのキーと16バイトの文字列を入力として受け取り、ブロックを出力します。暗号化するために、AES を操作モードで使用します。上記のソリューションは、CBCの使用を提案しています。これは1つの例です。もう1つは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)

キーもパスワードから導出できます:

# 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を使用することを推奨していますが、これは一般的に暗号の不適切な慣行と見なされてます。動作モードの詳細については、ウィキペディアを確認してください。


iv_int = int(binascii.hexlify(iv)、16)は機能しません。iv_int= int(binascii.hexlify(iv)、16)に「インポートbinascii」を追加すると機能します(Python 3.xの場合) )、そうでなければ素晴らしい仕事!
Valmond

AES-GCMとしてAutehnticated Encryptionモードを使用することをお勧めします。GCMは内部的にCTRモードを使用します。
kelalaka

このコードにより、「TypeError:オブジェクトタイプ<クラス 'str'>をCコードに渡すことができません」
Da Woon Jung

7

urlsafe_b64encodeとurlsafe_b64decodeを使用したい人のために、私のために働いているバージョンがあります(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:]))

6

あなたは、暗号ハッシュ関数(使用して、任意のパスワードの外にパスフレーズを取得することができませ Pythonの組み込みhashSHA-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]を使用するだけで暗号化ハッシュ値を切り捨てることができ、指定した長さまでセキュリティが保持されます。


13
パスワードからキーを生成するためにSHAファミリーのハッシュ関数を使用しないでください。このトピックに関するCoda Haleのエッセイを参照してください。代わりにscryptのような実際の鍵導出関数の使用を検討してください。(Coda Haleのエッセイはscryptの公開前に作成されました。)
Benjamin Barenblat '30 / 11/13

7
今後の読者のために、パスフレーズからキーを導出する場合は、PBKDF2を探してください。Pythonでの使用はかなり簡単です(pypi.python.org/pypi/pbkdf2)。ただし、パスワードをハッシュする場合は、bcryptの方が適しています。
Cフェアウェザー2015年

6

刺激を受けたがうまくいかなかった他の答えに感謝します。

それがどのように動作するかを理解しようとしている時間を過ごした後、私は最新で、以下の実装を思い付いたPyCryptodomexのライブラリー(これは私はvirtualenvの...あーで、Windows上で、プロキシの背後にそれを設定するために管理方法をまた別の話である)

に取り組んで実装では、パディング、エンコード、暗号化の手順を書き留めてください(逆も同様です)。順序を考慮して、梱包と開梱を行う必要があります。

base64をインポート
hashlibのインポート
Cryptodome.CipherからインポートAES
Cryptodome.Random import get_random_bytesから

__key__ = hashlib.sha256(b'16文字のキー ').digest()

def encrypt(raw):
    BS = AES.block_size
    パッド=ラムダs:s +(BS-len(s)%BS)* chr(BS-len(s)%BS)

    raw = base64.b64encode(pad(raw).encode( 'utf8'))
    iv = get_random_bytes(AES.block_size)
    cipher = AES.new(key = __key__、mode = AES.MODE_CFB、iv = iv)
    base64.b64encode(iv + cipher.encrypt(raw))を返します

def decode(enc):
    unpad = lambda 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 libsを使用したこの機能例をご利用いただきありがとうございます。とても助かります!
Ygramul

5

他の人のために、@ Cyrilと@Marcusの回答を組み合わせることで私が得た復号化の実装を示します。これは、encryptedTextが引用され、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を使用します
  • ラムダを使用しない(ファンではない)
  • Python 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')

これは、入力byte []に​​後続のヌルがある場合は機能しません。これは、decrypt()関数でパディングヌルと後続のヌルをすべて使用するためです。
バズモシェッティ2017

はい、上で述べたように、このロジックはnullで埋められます。エンコード/デコードするアイテムに後続のnullがある可能性がある場合は、ここで他のソリューションの1つを使用することをお勧めします
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パディングのような使用スキームについては誰も言及していません。前の関数の代わりにそれを使用して、パディング(暗号化を行う場合)およびアンパディング(復号化を行う場合)を行うことができます。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の1回の呼び出しでパスワードをハッシュするだけで十分であることを示唆しています。そうではありません。大きな反復回数を使用してパスワードからキーを導出するには、本当にPBKDF2または同様のものを使用する必要があります。
Artjom B. 2017

詳細@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()に置き換えると、魅力的に動作します!
STEFANI、2018年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.