Encrypt and decrypt using PyCrypto AES-256
Asked Answered
H

16

216

I'm trying to build two functions using PyCrypto that accept two parameters: the message and the key, and then encrypt/decrypt the message.

I found several links on the web to help me out, but each one of them has flaws:

This one at codekoala uses os.urandom, which is discouraged by PyCrypto.

Moreover, the key I give to the function is not guaranteed to have the exact length expected. What can I do to make that happen?

Also, there are several modes, which one is recommended? I don't know what to use :/

Finally, what exactly is the IV? Can I provide a different IV for encrypting and decrypting, or will this return in a different result?

Hubble answered 21/9, 2012 at 5:54 Comment(10)
os.urandom is encouraged on the PyCrypto website. It uses Microsoft's CryptGenRandom function which is a CSPRNGLanguage
or /dev/urandom on UnixLanguage
Just to clarify, in this example passphrase is the key which can be 128, 192, or 256 bits (16, 24, or 32 bytes)Shoveler
@Totem the manpage has since been updated. See bugzilla.kernel.org/show_bug.cgi?id=71211 for background, and random(7) as well as random(4). You should be using /dev/urandom.Galenism
It might be worth to mention that PyCrypto is a dead project. Last commit is from 2014. PyCryptodome looks like a good drop-in replacementExasperate
Pycrypto has not been updated in many years. use pycryptodome or cryptography insteadBenito
Unfortunately this pycrypto solution is not working together with python 3.8 as time.clock() has been removed.Shaving
pycrypto should not be used, is is unsupported and has a remote code execution vulnerability. cvedetails.com/vulnerability-list/vendor_id-11993/…Flounce
I've updated the question by removing the code part.Hubble
This question is old, but I'd like to point out (as of 2020) that pycrypto is likely outdated and no longer supported. Looking at their github page (github.com/pycrypto/pycrypto), it appears their last commit was in 2014. I'd be leery of using cryptographic software that is no longer under developmentNert
M
218

Here is my implementation, and it works for me with some fixes. It enhances the alignment of the key and secret phrase with 32 bytes and IV to 16 bytes:

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 AESCipher._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:])]
Mothy answered 21/2, 2014 at 8:10 Comment(18)
Why are you hashing the key? If you're expecting that this is something like a password, then you shouldn't be using SHA256; better to use a key derivation function, like PBKDF2, which PyCrypto provides.Leblanc
@Chris - SHA256 gives out a 32-byte hash - a perfect-sized key for AES256. Generation/derivation of a key is assumed to be random/secure and should be out of the encryption/decryption code's scope - hashing is just a guarantee that the key is usable with the selected cipher.Stormie
@Stormie - I see what you're getting at, but I think this is misleading. The key for encryption should be a uniform random, 32-byte string. Providing an interface for this function that accepts a string of any length might lead one to believe that it is safe to us a string of any length.Leblanc
@mnothic What is the reason for _pad being a normal class method and _unpad being a staticmethod? What's the difference?Arium
in _pad self.bs access is needed and in _unpad doesn't needMothy
@mnothic - silly question: why s[:-ord(s[len(s)-1:])] instead of just s[:-ord(s[-1])]?Inapprehensible
@Inapprehensible I just take it from the pad unpad functions just explained downside so ask to himMothy
It works great, thanks. Do you know how to make it to support unicode?Benito
@ryugie, it's a little late, but I was wondering the same thing myself. Say x = b"123". ord takes a string, but x[-1] is an int. To get a string you need to do x[-1:] :)Anaphrodisiac
/!\ importantly, the order of operations in pad is (bs - (len(msg) % bs)) (16 when len(msg) % bs == 0), NOT '((bs - len(msg)) % bs)' (0 when len(msg) % bs == 0). eg the mod-before-subtract order of operations will ensure the last byte in the encrypted message is the padding length, and there will always be paddingDioxide
Your _pad() and _unpad() was exactly what I needed. This could work also with DES with few modifications. Many thanks!!Covell
Please add an example to show how to use this class.Refit
Could this maybe be a module? Instead of having to copy & paste code, just have it uploaded to pypi? Then, it can be used like this: import socrypto as sc # The "so" is short for StackOverflow raw = "foo"`` enc = sc.encrypt(raw, key="cipher-key")` print("Gibberish:") print(enc) dec = sc.decrypt(enc, key="cipher-key") print("Decrypted") print(dec)Airship
I had to modify it to be able to encrypt strings containing non ASCII characters such as áàâãéèêíïóôõöúçñÁÀÂÃÉÈÍÏÓÔÕÖÚÇÑ. To do so I had to replace len(s) in _pad() for len(s.encode()) in order to get the correct padding. Otherwise I would get the following error: "data must be padded to 16 byte boundary in cbc mode".Wine
Thanks, it works! ``` CIPHER_KEY = 'Ay1sg0$32lsghR#8' cipher = AESCipher(CIPHER_KEY) cyrillic_lowercase = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя' def gen(size, chars=string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation + cyrillic_lowercase): return ''.join(random.choice(chars) for x in range(size)) def test_cipher(): for i in range(0, random.randint(1, 300)): word = gen(random.randint(1, 300)) enc = cipher.encrypt(word) assert word == cipher.decrypt(enc) ```Deva
just a heads up, i had to use the pycryptodome package to get this working because the pycrypto pacakge doesnt support newer python versionsLiripipe
Hey, I am new to this field. Can you tell me what do I pass in "key"? –Rivalry
If you are having trouble encrypting text containing special characters like "áàâãéèêíïóôõöúçñÁÀÂÃÉÈÍÏÓÔÕÖÚÇÑ" or if your text's length is a multiple of the AES blocksize (and therefore has problems with the unpadding), see my answer belowOrganotherapy
A
158

You may need the following two functions: pad- to pad (when doing encryption) and unpad- to unpad (when doing decryption) when the length of input is not a multiple of 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:])]

So you're asking the length of key? You can use the MD5 hash of the key rather than use it directly.

More, according to my little experience of using PyCrypto, the IV is used to mix up the output of a encryption when input is same, so the IV is chosen as a random string, and use it as part of the encryption output, and then use it to decrypt the message.

And here's my implementation:

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:] ))
Ause answered 21/9, 2012 at 6:12 Comment(15)
What happens if you have an input that is exactly a multiple of BLOCK_SIZE? I think that the unpad function would get a little confused...Ure
@Kjir, then a sequence of value chr(BS) in length BLOCK_SIZE will be appended to the origin data.Ause
you are right, the pad and unpad functions are now clearer for me, thank you!Ure
@Ause the pad function is broken (at least in Py3), replace with s[:-ord(s[len(s)-1:])] for it to work across versions.Analgesic
is the raw a string?Holdup
I tried using this to test it msg=129093409304930940950390539405 key = os.urandom(16) C = AESCipher(key).encrypt(msg) print(C) but I got a typerror "TypeError: object of type 'long' has no len()"Holdup
Just make your msg str.Rockies
@Analgesic pad function is avail in CryptoUtil.Padding.pad() with pycryptodome (pycrypto followup)Disadvantaged
Why not just have a character constant as the padding char?Annamariaannamarie
I had to change cipher.encrypt( raw ) to cipher.encrypt( str.encode(raw) ) to get this workingWelsh
@rob, where is your raw come from?Ause
@marcus to test it I was just using a literal stringWelsh
Excellent answer!!. Very much authentic solution for encryption and decryption.Fossilize
What happens if there is no pad? What is pad? I have no idea of any of these, so is it ok to just omit pad, is going to be too risky?Heraclitean
Hey, I am new to this field. Can you tell me what do I pass in "key"? –Rivalry
L
22

Let me address your question about "modes." AES-256 is a kind of block cipher. It takes as input a 32-byte key and a 16-byte string, called the block and outputs a block. We use AES in a mode of operation in order to encrypt. The solutions above suggest using CBC, which is one example. Another is called CTR, and it's somewhat easier to use:

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)

This is often referred to as AES-CTR. I would advise caution in using AES-CBC with PyCrypto. The reason is that it requires you to specify the padding scheme, as exemplified by the other solutions given. In general, if you're not very careful about the padding, there are attacks that completely break encryption!

Now, it's important to note that the key must be a random, 32-byte string; a password does not suffice. Normally, the key is generated like so:

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

A key may be derived from a password, too:

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

Some solutions above suggest using SHA-256 for deriving the key, but this is generally considered bad cryptographic practice. Check out Wikipedia for more on modes of operation.

Leblanc answered 20/6, 2017 at 20:17 Comment(5)
iv_int = int(binascii.hexlify(iv), 16) doesn't work, replace it with iv_int = int(binascii.hexlify(iv), 16) plus the 'import binascii' and it should work (on Python 3.x), otherwise great work!Demy
Note that it is better to use Autehnticated Encryption modes as AES-GCM. GCM internally uses CTR mode.Standard
This code cause "TypeError: Object type <class 'str'> cannot be passed to C code"Josephus
@DaWoonJung I observed the same with Python 3.9.7. ciphertext = aes.encrypt(plaintext.encode('utf-8')) and the ciphertext would need the decode('utf-8') to complete the str, bytes roundtrip.Confidence
i got error when i tried to decrypt. the error message : 'bytes' object has no attribute 'encode'. but when i check type iv it's said <class 'bytes'>. how to fix that?Hast
R
10

I am grateful for the other answers which inspired me, but it didn't work for me.

After spending hours trying to figure out how it works, I came up with the implementation below with the newest PyCryptodomex library (it is another story how I managed to set it up behind proxy, on Windows, in a virtualenv... phew)

It is working on your implementation. Remember to write down padding, encoding, and encrypting steps (and vice versa). You have to pack and unpack, keeping in mind the order.

import base64
import hashlib
from Cryptodome.Cipher import AES
from Cryptodome.Random import get_random_bytes

__key__ = hashlib.sha256(b'16-character key').digest()

def encrypt(raw):
    BS = AES.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(AES.block_size)
    cipher = AES.new(key= __key__, mode= AES.MODE_CFB,iv= iv)
    return base64.b64encode(iv + cipher.encrypt(raw))

def decrypt(enc):
    unpad = lambda s: s[:-ord(s[-1:])]

    enc = base64.b64decode(enc)
    iv = enc[:AES.block_size]
    cipher = AES.new(__key__, AES.MODE_CFB, iv)
    return unpad(base64.b64decode(cipher.decrypt(enc[AES.block_size:])).decode('utf8'))
Reconstructive answered 6/2, 2019 at 14:50 Comment(1)
Thank you kindly for a functioning example of this with the PyCryptodomeX libs. That's quite helpful!Wallah
C
8

For someone who would like to use urlsafe_b64encode and urlsafe_b64decode, here are the version that're working for me (after spending some time with the unicode issue)

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:]))
Calderon answered 31/8, 2015 at 12:18 Comment(0)
S
6

You can get a passphrase out of an arbitrary password by using a cryptographic hash function (NOT Python's builtin hash) like SHA-1 or SHA-256. Python includes support for both in its standard library:

import hashlib

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

You can truncate a cryptographic hash value just by using [:16] or [:24] and it will retain its security up to the length you specify.

Statfarad answered 21/9, 2012 at 6:8 Comment(2)
You should not use a SHA-family hash function for generating a key from a password – see Coda Hale’s essay on the topic. Consider using a real key derivation function like scrypt instead. (Coda Hale’s essay was written before scrypt’s publication.)Puddle
For future readers, if you're looking to derive a key from a passphrase, look for PBKDF2. It's fairly easy to use in python (pypi.python.org/pypi/pbkdf2). If you're looking to hash passwords, however, bcrypt is a better option.Oratorio
W
6

Another take on this (heavily derived from solutions above) but

  • uses null for padding
  • does not use lambda (never been a fan)
  • tested with python 2.7 and 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')
    
Wallack answered 6/5, 2017 at 23:22 Comment(3)
This will not work if the input byte[] has trailing nulls because in the decrypt() function you will eat your padding nulls PLUS any trailing nulls.Bite
Yes, as I state above, this logic pads with nulls. If the items you want encode/decode might have trailing nulls, better use one of the other solutions hereWallack
** warning ** this function does not work with latin and special characters.Irreproachable
Q
5

For the benefit of others, here is my decryption implementation which I got to by combining the answers of @Cyril and @Marcus. This assumes that this coming in via HTTP Request with the encryptedText quoted and base64 encoded.

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()
Quinonoid answered 16/5, 2013 at 18:18 Comment(1)
Hey, I am new to this field. Can you tell me what do I pass in "key"? –Rivalry
P
5

I have used both Crypto and PyCryptodomex library and it is blazing fast...

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)
Prolific answered 25/7, 2019 at 7:26 Comment(1)
TypeError: 'iv' is an invalid keyword argument for this function. getting errorGrappa
S
3

You can use a scheme like PKCS#7 padding. You can use it instead the previous functions to pad (when doing encryption) and unpad (when doing decryption). I will provide the full source code below.

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())
Squab answered 19/11, 2016 at 0:11 Comment(3)
I don't know who downvoted the answer but I'd be curious to know why. Maybe this method is not secure? An explanation would be great.Hubble
@CyrilN. This answer suggests that hashing the password with a single invocation of SHA-256 is enough. It isn't. You really should use PBKDF2 or similar for key derivation from a password using a large iteration count.Grouch
I have a key and also iv key with 44 length. How can i use your functions ?! all of algorithms in the internet that i found, has problem with length of my vector keyCaplin
A
2

See mnothic's answer.

Compatible UTF-8 encoding:

def _pad(self, s):
    s = s.encode()
    res = s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs).encode()
    return res
Angkor answered 17/7, 2019 at 2:22 Comment(2)
What is this? A correction to that answer? Something else? Please elaborate.Hermaphroditism
OK, the OP has left the building: "Last seen more than 1 year ago"Hermaphroditism
I
1

Encryption and decryption of Latin and special characters (Chinese) using AES-256 with utf8mb4:

For those who need to encrypt and decrypt Latin and special values, such as Chinese, here is a modification of the @MIkee code to do this task.

Remembering that UTF-8 alone does not handle this type of encoding.

import base64, re
from Crypto.Cipher import AES
from Crypto import Random
from django.conf import settings

import codecs

# Make utf8mb4 recognizable.
codecs.register(lambda name: codecs.lookup('utf8') if name == 'utf8mb4' else None)


class AESCipher:

    def __init__(self, key, blk_sz):
        self.key = key
        self.blk_sz = blk_sz

    def encrypt( self, raw ):
        # raw is the main value
        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('utf8mb4')
        # Initialization vector to avoid same encrypt for same strings.
        iv = Random.new().read( AES.block_size )
        cipher = AES.new( self.key.encode('utf8mb4'), AES.MODE_CFB, iv )
        return base64.b64encode( iv + cipher.encrypt( raw ) ).decode('utf8mb4')

    def decrypt( self, enc ):
        # enc is the encrypted value
        if enc is None or len(enc) == 0:
            raise NameError("No value given to decrypt")
        enc = base64.b64decode(enc)
        iv = enc[:16]
        # AES.MODE_CFB that allows bigger length or Latin values
        cipher = AES.new(self.key.encode('utf8mb4'), AES.MODE_CFB, iv )
        return re.sub(b'\x00*$', b'', cipher.decrypt( enc[16:])).decode('utf8mb4')

Usage:

>>> from django.conf import settings
>>> from aesencryption import AESCipher
>>>
>>> aes = AESCipher(settings.SECRET_KEY[:16], 32)
>>>
>>> value = aes.encrypt('漢字')
>>>
>>> value
'hnuRwBjwAHDp5X0DmMF3lWzbjR0r81WlW9MRrWukgQwTL0ZI88oQaWvMfBM+W87w9JtSTw=='
>>> dec_value = aes.decrypt(value)
>>> dec_value
'漢字'
>>>

The same for Latin letters, such as ã, á, à, â, ã, ç, etc.

Attention point

Bear in mind that if you will store Latin values to your database, you need to set it to allow such type of data. Therefore, if your database is set as utf-8 it will not accept such type of data. You will need to change there as well.

Irreproachable answered 18/10, 2021 at 15:28 Comment(0)
M
1

PyCrypto is old and busted.

The cryptography has better support these days.

Here's another implementation. Note that this returns bytes, you'd need to use base64 to convert them to a string for transmission.

import os
import hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

_BLOCK_SIZE = 16

class AesStringCipher:
    def __init__(self, key):
        self._key = hashlib.sha256(key.encode()).digest()

    def encrypt_str(self, raw:str) -> bytes:
        iv = os.urandom(_BLOCK_SIZE)
        cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv), default_backend())
        encryptor = cipher.encryptor()
        raw = _pad(raw)
        return iv + encryptor.update(raw.encode('utf-8')) + encryptor.finalize()

    def decrypt_str(self, enc:bytes) -> str:
        iv = enc[:_BLOCK_SIZE]
        enc = enc[_BLOCK_SIZE:]
        cipher = Cipher(algorithms.AES(self._key), modes.CBC(iv), default_backend())
        decryptor = cipher.decryptor()
        raw = decryptor.update(enc) + decryptor.finalize()
        raw = raw.decode('utf-8')
        return _unpad(raw)

def _pad(s:str) -> str:
    padding = (_BLOCK_SIZE - (len(s) % _BLOCK_SIZE))
    return s + padding * chr(padding)

def _unpad(s:str) -> str:
    return s[:-ord(s[len(s)-1:])]


if __name__ == '__main__':
    cipher = AesStringCipher('my secret password')

    secret_msg = 'this is a super secret msg ...'
    enc_msg = cipher.encrypt_str(secret_msg)
    dec_msg = cipher.decrypt_str(enc_msg)

    assert secret_msg == dec_msg
Motherless answered 5/4, 2022 at 5:14 Comment(1)
Re "PyCrypto is old and busted.": Yes, "This software is no longer maintained. PyCrypto 2.x is unmaintained, obsolete, and contains security vulnerabilities."Hermaphroditism
B
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:])
Burleigh answered 11/8, 2016 at 13:24 Comment(3)
Please provide not only code but also explain what you are doing and why this is better / what is the difference to existing answers.Punkie
Replace the md5.new(key).digest() by md5(key).digest(), and it work like a charm !Spavin
it appears to be this library: pycryptodome.orgDegradation
D
0

You can use the new django-mirage-field package.

Deva answered 5/11, 2021 at 0:9 Comment(2)
It does some crypto things, but can you elaborate?Hermaphroditism
Yes, it ecrypts fields "out-of-the-box".Deva
O
0

Following @mnothic implementation, I made a few changes to fix a couple of issues with it. First I made _unpad into an instance method, because in decrypt it is called with a self. prefix instead of an AESCipher prefix. The second thing I did, was I changed the padding operation order, to account for text using characters that when encoded, result in more than just one byte, thereby shifting the padding. So all I did, was put the padding after the encoding. Finally, I changed the padding and unpadding logic to be able to handle bytes instead of strings, and also to account for extreme cases in which the previous logic would have failed, like when the original text requires no padding.

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):
        iv = Random.new().read(AES.block_size)
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return base64.b64encode(iv + cipher.encrypt(self._pad(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) * bytes(0x01)

    def _unpad(self, s):
        for i in range(1, len(s) - 1):
            if s[-i] != 0:
                break
        i -= 1
        return s[:-i] if i > 0 else s
Organotherapy answered 27/6, 2023 at 20:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.