Python has no built-in encryption schemes, no. You also should take encrypted data storage serious; trivial encryption schemes that one developer understands to be insecure and a toy scheme may well be mistaken for a secure scheme by a less experienced developer. If you encrypt, encrypt properly.

You don’t need to do much work to implement a proper encryption scheme however. First of all, don’t re-invent the cryptography wheel, use a trusted cryptography library to handle this for you. For Python 3, that trusted library is cryptography.

I also recommend that encryption and decryption applies to bytes; encode text messages to bytes first; stringvalue.encode() encodes to UTF8, easily reverted again using bytesvalue.decode().

Last but not least, when encrypting and decrypting, we talk about keys, not passwords. A key should not be human memorable, it is something you store in a secret location but machine readable, whereas a password often can be human-readable and memorised. You can derive a key from a password, with a little care.

But for a web application or process running in a cluster without human attention to keep running it, you want to use a key. Passwords are for when only an end-user needs access to the specific information. Even then, you usually secure the application with a password, then exchange encrypted information using a key, perhaps one attached to the user account.

Symmetric key encryption

Fernet – AES CBC + HMAC, strongly recommended

The cryptography library includes the Fernet recipe, a best-practices recipe for using cryptography. Fernet is an open standard, with ready implementations in a wide range of programming languages and it packages AES CBC encryption for you with version information, a timestamp and an HMAC signature to prevent message tampering.

Fernet makes it very easy to encrypt and decrypt messages and keep you secure. It is the ideal method for encrypting data with a secret.

I recommend you use Fernet.generate_key() to generate a secure key. You can use a password too (next section), but a full 32-byte secret key (16 bytes to encrypt with, plus another 16 for the signature) is going to be more secure than most passwords you could think of.

The key that Fernet generates is a bytes object with URL- and file-safe base64 characters, so printable:

from cryptography.fernet import Fernet

key = Fernet.generate_key()  # store in a secure location
# PRINTING FOR DEMO PURPOSES ONLY, don't do this in production code
print("Key:", key.decode())

To encrypt or decrypt messages, create a Fernet() instance with the given key, and call the Fernet.encrypt() or Fernet.decrypt(), both the plaintext message to encrypt and the encrypted token are bytes objects.

encrypt() and decrypt() functions would look like:

from cryptography.fernet import Fernet

def encrypt(message: bytes, key: bytes) -> bytes:
    return Fernet(key).encrypt(message)

def decrypt(token: bytes, key: bytes) -> bytes:
    return Fernet(key).decrypt(token)

Demo:

>>> key = Fernet.generate_key()
>>> print(key.decode())
GZWKEhHGNopxRdOHS4H4IyKhLQ8lwnyU7vRLrM3sebY=
>>> message = 'John Doe'
>>> token = encrypt(message.encode(), key)
>>> print(token)
'gAAAAABciT3pFbbSihD_HZBZ8kqfAj94UhknamBuirZWKivWOukgKQ03qE2mcuvpuwCSuZ-X_Xkud0uWQLZ5e-aOwLC0Ccnepg=='
>>> decrypt(token, key).decode()
'John Doe'

Fernet with password – key derived from password, weakens the security somewhat

You can use a password instead of a secret key, provided you use a strong key derivation method. You do then have to include the salt and the HMAC iteration count in the message, so the encrypted value is not Fernet-compatible anymore without first separating salt, count and Fernet token:

import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

backend = default_backend()
iterations = 100_000

def _derive_key(password: bytes, salt: bytes, iterations: int = iterations) -> bytes:
    """Derive a secret key from a given password and salt"""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(), length=32, salt=salt,
        iterations=iterations, backend=backend)
    return b64e(kdf.derive(password))

def password_encrypt(message: bytes, password: str, iterations: int = iterations) -> bytes:
    salt = secrets.token_bytes(16)
    key = _derive_key(password.encode(), salt, iterations)
    return b64e(
        b'%b%b%b' % (
            salt,
            iterations.to_bytes(4, 'big'),
            b64d(Fernet(key).encrypt(message)),
        )
    )

def password_decrypt(token: bytes, password: str) -> bytes:
    decoded = b64d(token)
    salt, iter, token = decoded[:16], decoded[16:20], b64e(decoded[20:])
    iterations = int.from_bytes(iter, 'big')
    key = _derive_key(password.encode(), salt, iterations)
    return Fernet(key).decrypt(token)

Demo:

>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'

Including the salt in the output makes it possible to use a random salt value, which in turn ensures the encrypted output is guaranteed to be fully random regardless of password reuse or message repetition. Including the iteration count ensures that you can adjust for CPU performance increases over time without losing the ability to decrypt older messages.

A password alone can be as safe as a Fernet 32-byte random key, provided you generate a properly random password from a similar size pool. 32 bytes gives you 256 ^ 32 number of keys, so if you use an alphabet of 74 characters (26 upper, 26 lower, 10 digits and 12 possible symbols), then your password should be at least math.ceil(math.log(256 ** 32, 74)) == 42 characters long. However, a well-selected larger number of HMAC iterations can mitigate the lack of entropy somewhat as this makes it much more expensive for an attacker to brute force their way in.

Just know that choosing a shorter but still reasonably secure password won’t cripple this scheme, it just reduces the number of possible values a brute-force attacker would have to search through; make sure to pick a strong enough password for your security requirements.

Alternatives

Obscuring

An alternative is not to encrypt. Don't be tempted to just use a low-security cipher, or a home-spun implementation of, say Vignere. There is no security in these approaches, but may give an inexperienced developer that is given the task to maintain your code in future the illusion of security, which is worse than no security at all.

If all you need is obscurity, just base64 the data; for URL-safe requirements, the base64.urlsafe_b64encode() function is fine. Don't use a password here, just encode and you are done. At most, add some compression (like zlib):

import zlib
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

def obscure(data: bytes) -> bytes:
    return b64e(zlib.compress(data, 9))

def unobscure(obscured: bytes) -> bytes:
    return zlib.decompress(b64d(obscured))

This turns b'Hello world!' into b'eNrzSM3JyVcozy_KSVEEAB0JBF4='.

Integrity only

If all you need is a way to make sure that the data can be trusted to be unaltered after having been sent to an untrusted client and received back, then you want to sign the data, you can use the hmac library for this with SHA1 (still considered secure for HMAC signing) or better:

import hmac
import hashlib

def sign(data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
    assert len(key) >= algorithm().digest_size, (
        "Key must be at least as long as the digest size of the "
        "hashing algorithm"
    )
    return hmac.new(key, data, algorithm).digest()

def verify(signature: bytes, data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
    expected = sign(data, key, algorithm)
    return hmac.compare_digest(expected, signature)

Use this to sign data, then attach the signature with the data and send that to the client. When you receive the data back, split data and signature and verify. I've set the default algorithm to SHA256, so you'll need a 32-byte key:

key = secrets.token_bytes(32)

You may want to look at the itsdangerous library, which packages this all up with serialisation and de-serialisation in various formats.

Using AES-GCM encryption to provide encryption and integrity

Fernet builds on AEC-CBC with a HMAC signature to ensure integrity of the encrypted data; a malicious attacker can't feed your system nonsense data to keep your service busy running in circles with bad input, because the ciphertext is signed.

The Galois / Counter mode block cipher produces ciphertext and a tag to serve the same purpose, so can be used to serve the same purposes. The downside is that unlike Fernet there is no easy-to-use one-size-fits-all recipe to reuse on other platforms. AES-GCM also doesn't use padding, so this encryption ciphertext matches the length of the input message (whereas Fernet / AES-CBC encrypts messages to blocks of fixed length, obscuring the message length somewhat).

AES256-GCM takes the usual 32 byte secret as a key:

key = secrets.token_bytes(32)

then use

import binascii, time
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidTag

backend = default_backend()

def aes_gcm_encrypt(message: bytes, key: bytes) -> bytes:
    current_time = int(time.time()).to_bytes(8, 'big')
    algorithm = algorithms.AES(key)
    iv = secrets.token_bytes(algorithm.block_size // 8)
    cipher = Cipher(algorithm, modes.GCM(iv), backend=backend)
    encryptor = cipher.encryptor()
    encryptor.authenticate_additional_data(current_time)
    ciphertext = encryptor.update(message) + encryptor.finalize()        
    return b64e(current_time + iv + ciphertext + encryptor.tag)

def aes_gcm_decrypt(token: bytes, key: bytes, ttl=None) -> bytes:
    algorithm = algorithms.AES(key)
    try:
        data = b64d(token)
    except (TypeError, binascii.Error):
        raise InvalidToken
    timestamp, iv, tag = data[:8], data[8:algorithm.block_size // 8 + 8], data[-16:]
    if ttl is not None:
        current_time = int(time.time())
        time_encrypted, = int.from_bytes(data[:8], 'big')
        if time_encrypted + ttl < current_time or current_time + 60 < time_encrypted:
            # too old or created well before our current time + 1 h to account for clock skew
            raise InvalidToken
    cipher = Cipher(algorithm, modes.GCM(iv, tag), backend=backend)
    decryptor = cipher.decryptor()
    decryptor.authenticate_additional_data(timestamp)
    ciphertext = data[8 + len(iv):-16]
    return decryptor.update(ciphertext) + decryptor.finalize()

I've included a timestamp to support the same time-to-live use-cases that Fernet supports.

Other approaches on this page, in Python 3

AES CFB - like CBC but without the need to pad

This is the approach that All Іѕ Vаиітy follows, albeit incorrectly. This is the cryptography version, but note that I include the IV in the ciphertext, it should not be stored as a global (reusing an IV weakens the security of the key, and storing it as a module global means it'll be re-generated the next Python invocation, rendering all ciphertext undecryptable):

import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

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

backend = default_backend()

def aes_cfb_encrypt(message, key):
    algorithm = algorithms.AES(key)
    iv = secrets.token_bytes(algorithm.block_size // 8)
    cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(message) + encryptor.finalize()
    return b64e(iv + ciphertext)

def aes_cfb_decrypt(ciphertext, key):
    iv_ciphertext = b64d(ciphertext)
    algorithm = algorithms.AES(key)
    size = algorithm.block_size // 8
    iv, encrypted = iv_ciphertext[:size], iv_ciphertext[size:]
    cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
    decryptor = cipher.decryptor()
    return decryptor.update(encrypted) + decryptor.finalize()

This lacks the added armoring of an HMAC signature and there is no timestamp; you’d have to add those yourself.

The above also illustrates how easy it is to combine basic cryptography building blocks incorrectly; All Іѕ Vаиітy‘s incorrect handling of the IV value can lead to a data breach or all encrypted messages being unreadable because the IV is lost. Using Fernet instead protects you from such mistakes.

AES ECB – not secure

If you previously implemented AES ECB encryption and need to still support this in Python 3, you can do so still with cryptography too. The same caveats apply, ECB is not secure enough for real-life applications. Re-implementing that answer for Python 3, adding automatic handling of padding:

from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

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

backend = default_backend()

def aes_ecb_encrypt(message, key):
    cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
    encryptor = cipher.encryptor()
    padder = padding.PKCS7(cipher.algorithm.block_size).padder()
    padded = padder.update(msg_text.encode()) + padder.finalize()
    return b64e(encryptor.update(padded) + encryptor.finalize())

def aes_ecb_decrypt(ciphertext, key):
    cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
    decryptor = cipher.decryptor()
    unpadder = padding.PKCS7(cipher.algorithm.block_size).unpadder()
    padded = decryptor.update(b64d(ciphertext)) + decryptor.finalize()
    return unpadder.update(padded) + unpadder.finalize()

Again, this lacks the HMAC signature, and you shouldn’t use ECB anyway. The above is there merely to illustrate that cryptography can handle the common cryptographic building blocks, even the ones you shouldn’t actually use.

Answer from Martijn Pieters on Stack Overflow
Top answer
1 of 13
242

Python has no built-in encryption schemes, no. You also should take encrypted data storage serious; trivial encryption schemes that one developer understands to be insecure and a toy scheme may well be mistaken for a secure scheme by a less experienced developer. If you encrypt, encrypt properly.

You don’t need to do much work to implement a proper encryption scheme however. First of all, don’t re-invent the cryptography wheel, use a trusted cryptography library to handle this for you. For Python 3, that trusted library is cryptography.

I also recommend that encryption and decryption applies to bytes; encode text messages to bytes first; stringvalue.encode() encodes to UTF8, easily reverted again using bytesvalue.decode().

Last but not least, when encrypting and decrypting, we talk about keys, not passwords. A key should not be human memorable, it is something you store in a secret location but machine readable, whereas a password often can be human-readable and memorised. You can derive a key from a password, with a little care.

But for a web application or process running in a cluster without human attention to keep running it, you want to use a key. Passwords are for when only an end-user needs access to the specific information. Even then, you usually secure the application with a password, then exchange encrypted information using a key, perhaps one attached to the user account.

Symmetric key encryption

Fernet – AES CBC + HMAC, strongly recommended

The cryptography library includes the Fernet recipe, a best-practices recipe for using cryptography. Fernet is an open standard, with ready implementations in a wide range of programming languages and it packages AES CBC encryption for you with version information, a timestamp and an HMAC signature to prevent message tampering.

Fernet makes it very easy to encrypt and decrypt messages and keep you secure. It is the ideal method for encrypting data with a secret.

I recommend you use Fernet.generate_key() to generate a secure key. You can use a password too (next section), but a full 32-byte secret key (16 bytes to encrypt with, plus another 16 for the signature) is going to be more secure than most passwords you could think of.

The key that Fernet generates is a bytes object with URL- and file-safe base64 characters, so printable:

from cryptography.fernet import Fernet

key = Fernet.generate_key()  # store in a secure location
# PRINTING FOR DEMO PURPOSES ONLY, don't do this in production code
print("Key:", key.decode())

To encrypt or decrypt messages, create a Fernet() instance with the given key, and call the Fernet.encrypt() or Fernet.decrypt(), both the plaintext message to encrypt and the encrypted token are bytes objects.

encrypt() and decrypt() functions would look like:

from cryptography.fernet import Fernet

def encrypt(message: bytes, key: bytes) -> bytes:
    return Fernet(key).encrypt(message)

def decrypt(token: bytes, key: bytes) -> bytes:
    return Fernet(key).decrypt(token)

Demo:

>>> key = Fernet.generate_key()
>>> print(key.decode())
GZWKEhHGNopxRdOHS4H4IyKhLQ8lwnyU7vRLrM3sebY=
>>> message = 'John Doe'
>>> token = encrypt(message.encode(), key)
>>> print(token)
'gAAAAABciT3pFbbSihD_HZBZ8kqfAj94UhknamBuirZWKivWOukgKQ03qE2mcuvpuwCSuZ-X_Xkud0uWQLZ5e-aOwLC0Ccnepg=='
>>> decrypt(token, key).decode()
'John Doe'

Fernet with password – key derived from password, weakens the security somewhat

You can use a password instead of a secret key, provided you use a strong key derivation method. You do then have to include the salt and the HMAC iteration count in the message, so the encrypted value is not Fernet-compatible anymore without first separating salt, count and Fernet token:

import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

backend = default_backend()
iterations = 100_000

def _derive_key(password: bytes, salt: bytes, iterations: int = iterations) -> bytes:
    """Derive a secret key from a given password and salt"""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(), length=32, salt=salt,
        iterations=iterations, backend=backend)
    return b64e(kdf.derive(password))

def password_encrypt(message: bytes, password: str, iterations: int = iterations) -> bytes:
    salt = secrets.token_bytes(16)
    key = _derive_key(password.encode(), salt, iterations)
    return b64e(
        b'%b%b%b' % (
            salt,
            iterations.to_bytes(4, 'big'),
            b64d(Fernet(key).encrypt(message)),
        )
    )

def password_decrypt(token: bytes, password: str) -> bytes:
    decoded = b64d(token)
    salt, iter, token = decoded[:16], decoded[16:20], b64e(decoded[20:])
    iterations = int.from_bytes(iter, 'big')
    key = _derive_key(password.encode(), salt, iterations)
    return Fernet(key).decrypt(token)

Demo:

>>> message = 'John Doe'
>>> password = 'mypass'
>>> password_encrypt(message.encode(), password)
b'9Ljs-w8IRM3XT1NDBbSBuQABhqCAAAAAAFyJdhiCPXms2vQHO7o81xZJn5r8_PAtro8Qpw48kdKrq4vt-551BCUbcErb_GyYRz8SVsu8hxTXvvKOn9QdewRGDfwx'
>>> token = _
>>> password_decrypt(token, password).decode()
'John Doe'

Including the salt in the output makes it possible to use a random salt value, which in turn ensures the encrypted output is guaranteed to be fully random regardless of password reuse or message repetition. Including the iteration count ensures that you can adjust for CPU performance increases over time without losing the ability to decrypt older messages.

A password alone can be as safe as a Fernet 32-byte random key, provided you generate a properly random password from a similar size pool. 32 bytes gives you 256 ^ 32 number of keys, so if you use an alphabet of 74 characters (26 upper, 26 lower, 10 digits and 12 possible symbols), then your password should be at least math.ceil(math.log(256 ** 32, 74)) == 42 characters long. However, a well-selected larger number of HMAC iterations can mitigate the lack of entropy somewhat as this makes it much more expensive for an attacker to brute force their way in.

Just know that choosing a shorter but still reasonably secure password won’t cripple this scheme, it just reduces the number of possible values a brute-force attacker would have to search through; make sure to pick a strong enough password for your security requirements.

Alternatives

Obscuring

An alternative is not to encrypt. Don't be tempted to just use a low-security cipher, or a home-spun implementation of, say Vignere. There is no security in these approaches, but may give an inexperienced developer that is given the task to maintain your code in future the illusion of security, which is worse than no security at all.

If all you need is obscurity, just base64 the data; for URL-safe requirements, the base64.urlsafe_b64encode() function is fine. Don't use a password here, just encode and you are done. At most, add some compression (like zlib):

import zlib
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

def obscure(data: bytes) -> bytes:
    return b64e(zlib.compress(data, 9))

def unobscure(obscured: bytes) -> bytes:
    return zlib.decompress(b64d(obscured))

This turns b'Hello world!' into b'eNrzSM3JyVcozy_KSVEEAB0JBF4='.

Integrity only

If all you need is a way to make sure that the data can be trusted to be unaltered after having been sent to an untrusted client and received back, then you want to sign the data, you can use the hmac library for this with SHA1 (still considered secure for HMAC signing) or better:

import hmac
import hashlib

def sign(data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
    assert len(key) >= algorithm().digest_size, (
        "Key must be at least as long as the digest size of the "
        "hashing algorithm"
    )
    return hmac.new(key, data, algorithm).digest()

def verify(signature: bytes, data: bytes, key: bytes, algorithm=hashlib.sha256) -> bytes:
    expected = sign(data, key, algorithm)
    return hmac.compare_digest(expected, signature)

Use this to sign data, then attach the signature with the data and send that to the client. When you receive the data back, split data and signature and verify. I've set the default algorithm to SHA256, so you'll need a 32-byte key:

key = secrets.token_bytes(32)

You may want to look at the itsdangerous library, which packages this all up with serialisation and de-serialisation in various formats.

Using AES-GCM encryption to provide encryption and integrity

Fernet builds on AEC-CBC with a HMAC signature to ensure integrity of the encrypted data; a malicious attacker can't feed your system nonsense data to keep your service busy running in circles with bad input, because the ciphertext is signed.

The Galois / Counter mode block cipher produces ciphertext and a tag to serve the same purpose, so can be used to serve the same purposes. The downside is that unlike Fernet there is no easy-to-use one-size-fits-all recipe to reuse on other platforms. AES-GCM also doesn't use padding, so this encryption ciphertext matches the length of the input message (whereas Fernet / AES-CBC encrypts messages to blocks of fixed length, obscuring the message length somewhat).

AES256-GCM takes the usual 32 byte secret as a key:

key = secrets.token_bytes(32)

then use

import binascii, time
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.exceptions import InvalidTag

backend = default_backend()

def aes_gcm_encrypt(message: bytes, key: bytes) -> bytes:
    current_time = int(time.time()).to_bytes(8, 'big')
    algorithm = algorithms.AES(key)
    iv = secrets.token_bytes(algorithm.block_size // 8)
    cipher = Cipher(algorithm, modes.GCM(iv), backend=backend)
    encryptor = cipher.encryptor()
    encryptor.authenticate_additional_data(current_time)
    ciphertext = encryptor.update(message) + encryptor.finalize()        
    return b64e(current_time + iv + ciphertext + encryptor.tag)

def aes_gcm_decrypt(token: bytes, key: bytes, ttl=None) -> bytes:
    algorithm = algorithms.AES(key)
    try:
        data = b64d(token)
    except (TypeError, binascii.Error):
        raise InvalidToken
    timestamp, iv, tag = data[:8], data[8:algorithm.block_size // 8 + 8], data[-16:]
    if ttl is not None:
        current_time = int(time.time())
        time_encrypted, = int.from_bytes(data[:8], 'big')
        if time_encrypted + ttl < current_time or current_time + 60 < time_encrypted:
            # too old or created well before our current time + 1 h to account for clock skew
            raise InvalidToken
    cipher = Cipher(algorithm, modes.GCM(iv, tag), backend=backend)
    decryptor = cipher.decryptor()
    decryptor.authenticate_additional_data(timestamp)
    ciphertext = data[8 + len(iv):-16]
    return decryptor.update(ciphertext) + decryptor.finalize()

I've included a timestamp to support the same time-to-live use-cases that Fernet supports.

Other approaches on this page, in Python 3

AES CFB - like CBC but without the need to pad

This is the approach that All Іѕ Vаиітy follows, albeit incorrectly. This is the cryptography version, but note that I include the IV in the ciphertext, it should not be stored as a global (reusing an IV weakens the security of the key, and storing it as a module global means it'll be re-generated the next Python invocation, rendering all ciphertext undecryptable):

import secrets
from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

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

backend = default_backend()

def aes_cfb_encrypt(message, key):
    algorithm = algorithms.AES(key)
    iv = secrets.token_bytes(algorithm.block_size // 8)
    cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
    encryptor = cipher.encryptor()
    ciphertext = encryptor.update(message) + encryptor.finalize()
    return b64e(iv + ciphertext)

def aes_cfb_decrypt(ciphertext, key):
    iv_ciphertext = b64d(ciphertext)
    algorithm = algorithms.AES(key)
    size = algorithm.block_size // 8
    iv, encrypted = iv_ciphertext[:size], iv_ciphertext[size:]
    cipher = Cipher(algorithm, modes.CFB(iv), backend=backend)
    decryptor = cipher.decryptor()
    return decryptor.update(encrypted) + decryptor.finalize()

This lacks the added armoring of an HMAC signature and there is no timestamp; you’d have to add those yourself.

The above also illustrates how easy it is to combine basic cryptography building blocks incorrectly; All Іѕ Vаиітy‘s incorrect handling of the IV value can lead to a data breach or all encrypted messages being unreadable because the IV is lost. Using Fernet instead protects you from such mistakes.

AES ECB – not secure

If you previously implemented AES ECB encryption and need to still support this in Python 3, you can do so still with cryptography too. The same caveats apply, ECB is not secure enough for real-life applications. Re-implementing that answer for Python 3, adding automatic handling of padding:

from base64 import urlsafe_b64encode as b64e, urlsafe_b64decode as b64d

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

backend = default_backend()

def aes_ecb_encrypt(message, key):
    cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
    encryptor = cipher.encryptor()
    padder = padding.PKCS7(cipher.algorithm.block_size).padder()
    padded = padder.update(msg_text.encode()) + padder.finalize()
    return b64e(encryptor.update(padded) + encryptor.finalize())

def aes_ecb_decrypt(ciphertext, key):
    cipher = Cipher(algorithms.AES(key), modes.ECB(), backend=backend)
    decryptor = cipher.decryptor()
    unpadder = padding.PKCS7(cipher.algorithm.block_size).unpadder()
    padded = decryptor.update(b64d(ciphertext)) + decryptor.finalize()
    return unpadder.update(padded) + unpadder.finalize()

Again, this lacks the HMAC signature, and you shouldn’t use ECB anyway. The above is there merely to illustrate that cryptography can handle the common cryptographic building blocks, even the ones you shouldn’t actually use.

2 of 13
86

Assuming you are only looking for simple obfuscation that will obscure things from the very casual observer, and you aren't looking to use third party libraries. I'd recommend something like the Vigenere cipher. It is one of the strongest of the simple ancient ciphers.

Vigenère cipher

It's quick and easy to implement. Something like:

import base64

def encode(key, string):
    encoded_chars = []
    for i in xrange(len(string)):
        key_c = key[i % len(key)]
        encoded_c = chr(ord(string[i]) + ord(key_c) % 256)
        encoded_chars.append(encoded_c)
    encoded_string = "".join(encoded_chars)
    return base64.urlsafe_b64encode(encoded_string)

Decode is pretty much the same, except you subtract the key.

It is much harder to break if the strings you are encoding are short, and/or if it is hard to guess the length of the passphrase used.

If you are looking for something cryptographic, PyCrypto is probably your best bet, though previous answers overlook some details: ECB mode in PyCrypto requires your message to be a multiple of 16 characters in length. So, you must pad. Also, if you want to use them as URL parameters, use base64.urlsafe_b64_encode(), rather than the standard one. This replaces a few of the characters in the base64 alphabet with URL-safe characters (as it's name suggests).

However, you should be ABSOLUTELY certain that this very thin layer of obfuscation suffices for your needs before using this. The Wikipedia article I linked to provides detailed instructions for breaking the cipher, so anyone with a moderate amount of determination could easily break it.

🌐
Javascan
javascan.com › 1352 › python-encode-and-decode-string-with-key
Python Encode And Decode String With Key - JavaScan.com
September 24, 2023 - Python Encode And Decode String With Key, Encoding and decoding a string with a key in Python can be done using various encryption techniques, such as Caesar Cipher, Vigenère Cipher, or more advanced methods like AES encryption.
Discussions

How do I encrypt and decrypt a string in python? - Stack Overflow
In case u need to encrypt a message ... is a key123', AES.MODE_CFB, 'This is an IV456'). Refer the docs at pythonhosted.org/pycrypto 2015-12-15T23:39:11.42Z+00:00 ... I'm getting this error while encoding 'utf-8' codec can't decode byte 0xa6 in position 7: invalid start byte 2016-11-17T10:16:45.683Z+00:00 ... Error: Input strings must be a ... More on stackoverflow.com
🌐 stackoverflow.com
encode and decode a string in python - Stack Overflow
I would like to encode and decode a string with a key for encoding and decoding. Vigenere would do it, but vigenere cannot handle the "-" character and also numbers. Is there a good solution for t... More on stackoverflow.com
🌐 stackoverflow.com
October 29, 2018
python - how to encode/decode a simple string - Stack Overflow
This character is # used to ensure ... string: %s' % decoded ... That's not what a hash means. ... Security is hard. What scenario are you trying to defend? ... I need to encode an account id into something longer and than decode it back when I need it. ... What attackers are you trying to defend against? Do you want to have a secret key... More on stackoverflow.com
🌐 stackoverflow.com
Learn String Manipulation by Building a Cipher - Step 85
Do not change vigenere in the return statement. Also remove the -1 in decryption arguments: · It is great that you solved the challenge, but instead of posting your full working solution, it is best to stay focused on answering the original poster’s question(s) and help guide them with hints ... More on forum.freecodecamp.org
🌐 forum.freecodecamp.org
0
0
January 30, 2024
🌐
Javatpoint
javatpoint.org › 1352 › python-encode-and-decode-string-with-key
Python Encode And Decode String With Key - Javatpoint
September 24, 2023 - Python Encode And Decode String With Key, Encoding and decoding a string with a key in Python can be done using various encryption techniques, such as Caesar Cipher, Vigenère Cipher, or more advanced methods like AES encryption.
🌐
GeeksforGeeks
geeksforgeeks.org › how-to-encrypt-and-decrypt-strings-in-python
How to Encrypt and Decrypt Strings in Python? - GeeksforGeeks
August 14, 2024 - Install the python rsa library with the following command. ... Generate public and private keys with rsa.newkeys() method. Encode the string to byte string.
🌐
Stack Overflow
stackoverflow.com › questions › 53035124 › encode-and-decode-a-string-in-python
encode and decode a string in python - Stack Overflow
October 29, 2018 - from cryptography.fernet import Fernet # Creates a random 44 character key in bytes format k = Fernet.generate_key() # Class specific to your key f = Fernet(k) mybyte = b'Hello, World!' enc = f.encrypt(mybyte) dec = f.decrypt(enc) dec==mybyte # True
🌐
GitHub
gist.github.com › VJPranay › b4b3ae945d4a0c58d76f0cb6a0ff55f8
String encryption and decryption using Python · GitHub
String encryption and decryption using Python . GitHub Gist: instantly share code, notes, and snippets.
🌐
Delft Stack
delftstack.com › home › howto › python › python encrypt string
How to Encrypt a Python String | Delft Stack
February 2, 2024 - The program below uses the cryptocode library to encrypt a string in Python: import cryptocode str_encoded = cryptocode.encrypt("I am okay", "wow") # And then to decode it: str_decoded = cryptocode.decrypt(str_encoded, "wow") print(str_decoded) ... The first parameter in the function would be the string that needs to be encrypted. The second parameter needs to be the key, which will be used for the decryption purpose.
Find elsewhere
🌐
Programiz
programiz.org › 1352 › python-encode-and-decode-string-with-key
Redirecting...
Category Search · programiz.org · Business and Finance · Education · Television · Technology & Computing · Travel · Shopping · Video Gaming · Movies
🌐
DigitalOcean
digitalocean.com › community › tutorials › python-string-encode-decode
Python String Encode and Decode: Complete Guide | DigitalOcean
August 3, 2022 - Master Python string encode() and decode() methods. Learn UTF-8, ASCII, Unicode handling, error handling modes, and practical encoding/decoding examples.
Top answer
1 of 1
4

You probably need to elaborate on how you are going to use it and why, as you have just opened Pandora's box :)

An encoding is reversible and should only be used to make data fit into something else (like base 64 binary data when you can only use text), a hash (like sha224) is not supposed to be reversible.

If you want to verify a user entering a password, you hash it (with like sha224) and store the hash, then when the user enters password again, you hash their entry and compare. This is the simplified version, you also need to add "salt" to avoid a simple "dictionary attack". I won't elaborate as that wasn't the question you asked.

To quickly answer your question you want an encryption library, like the cipher AES-128, which has a secret key and with the key you can recover the original data. There will be some details in the library on how to create the key (it has to be a specific length and will be manipulated to make it that length). If your key is based on simple passwords, go look at PBKDF2, which makes a strong encryption key from a weak password.

Don't confuse hmac as encryption (hmac uses another function, like the hashing function sha224), if the receiver of a messages shares a hmac key with the sender, they can "authenticate" that the message can from the sender, and it came without alteration.

Good luck!

P.S. here is a good book if you really want to start digging in: Cryptography Engineering: Design Principles and Practical Applications

A popular related answer: https://stackoverflow.com/a/4948393/1322463

Wikipedia has good articles too.

🌐
Medium
medium.com › @vivekmcm1 › understanding-pythons-encode-and-decode-with-real-world-examples-5d2080d66f01
Understanding Python’s encode() and decode() with Real-World Examples | by Vivek | Medium
August 30, 2025 - errors tells Python what to do if it encounters a character that can’t be encoded (strict, ignore, replace). ... text = "Hello World" byte_text = text.encode() # Default UTF-8 encoding print(byte_text) # b'Hello World' Notice the b prefix? That indicates a bytes object. ... Here, the é character is ignored because ASCII cannot represent it. ... Decoding is the reverse process: converting bytes back into a string.
🌐
DEV Community
dev.to › guardianangel › how-to-encrypt-a-text-using-python-key-and-text-and-decrypt-that-cipher-in-javascript-using-the-same-key-5dbm
How to encrypt a text using Python (key and text) and decrypt that cipher in JavaScript using the same key. - DEV Community
April 11, 2023 - Here's an example using AES in CBC mode with PKCS7 padding in Python and Node.js: ... import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad def encrypt(key, text): iv = b'\x00' * 16 # initialization vector, must be random for real-world use cipher = AES.new(key.encode(), AES.MODE_CBC, iv) text = text.encode() padded_text = pad(text, AES.block_size, style='pkcs7') encrypted_text = cipher.encrypt(padded_text) return base64.b64encode(iv + encrypted_text).decode()
🌐
Finxter
blog.finxter.com › home › learn python blog › two easy ways to encrypt and decrypt python strings
Two Easy Ways to Encrypt and Decrypt Python Strings - Be on the Right Side of Change
February 1, 2023 - To encrypt and decrypt a Python string, install and import the cryptography library, generate a Fernet key, and create a Fernet object with it.
🌐
freeCodeCamp
forum.freecodecamp.org › python
Learn String Manipulation by Building a Cipher - Step 85 - Python - The freeCodeCamp Forum
January 30, 2024 - ###I have done as asked to replace vigenere with encrypt and decrypt respectively and can’t find the mistake. f encrypt(message, key): return encrypt(message, key) def decrypt(message, key): return decrypt(message, key, -1) encryption = encrypt(text, custom_key) print(encryption) decryption = decrypt(encryption, custom_key, -1) print(decryption) text = 'Hello Zaira!' custom_key = 'python' def vigenere(message, key, direction=1): key_index = 0 alphabet = 'abcdefghijklmnopqrstuvwx...
🌐
LinkedIn
linkedin.com › pulse › create-your-own-custom-encryption-python-dennis-chow-mba
Create Your Own Custom Encryption in Python
January 5, 2021 - I’ve made this for the sake of ease of use and simple portability if you were to export these methods to another python program. def chowdecrypt(ciphertext, key): #data dictionary of common text and CLI chars encodeddict = { 'a' : 1, 'b' : 2, 'c' : 3, 'd' : 4, 'e' : 5, 'f' : 6, 'g' : 7, 'h' : 8, 'i' : 9, 'j' : 10, 'k' : 11, 'l' : 12, 'm' : 13, 'n' : 14, 'o' : 15, 'p' : 16, 'q' : 17, 'r' : 18, 's' : 19, 't' : 20, 'u' : 21, 'v' : 22, 'w' : 23, 'x' : 24, 'y' : 25, 'z' :26, ' ' : 100, 'A' : 101, 'B' : 102, 'C' : 103, 'D' : 103, 'E' : 104, 'F' : 105, 'G' : 106, 'H' : 107, 'I' : 108, 'J' : 109, 'K
🌐
Python Forum
python-forum.io › thread-36432.html
Encrypt and decrypt in python using own fixed key
Hi I want to encrypt an d decrypt. But I want to use fixed defined key for example: key = 'Abcd123'. Can some one help how to do it. Thanks in advance. I use below code. from cryptography.fernet import Fernet message = 'I am python' key = Fernet.gen...
🌐
Medium
medium.com › @projectsexplained › simple-text-encryption-and-decryption-in-python-a4787f9a7aae
Simple Text Encryption and Decryption in Python | by Projects Explained | Medium
August 21, 2023 - This project is a great way to have fun while learning and practicing Python. ... First Things First, lets dive into our Code project output to get an idea of what we are going to do. ... Let’s unravel this encryption and decryption code together. Piece by piece, you’ll see how simple and cool this is. ... We’re starting with ‘encrypt(message, key)’. It takes a ‘message’ (the text you want to encrypt) and a ‘key’ (a secret number that guides the encryption).
🌐
Medium
medium.com › @info_82002 › a-beginners-guide-to-encryption-and-decryption-in-python-12d81f6a9eac
A Beginner’s Guide to Encryption and Decryption in Python
August 6, 2024 - print(f"Decrypted: {plain_text.decode()}"): Decodes the decrypted byte string plain_text into a human-readable string and prints it. Symmetric Encryption: Fernet uses symmetric encryption, meaning the same key is used for both encryption and decryption. Key Security: The generated key is crucial for security. It should be kept secret and never shared. Byte Strings: Cryptographic operations in Python typically use byte strings.