The first rule of cryptography club is: never invent a cryptography system yourself. The second rule of cryptography club is: never implement a cryptography system yourself: many real-world holes are found in the implementation phase of a cryptosystem as well as in the design.
One useful library for cryptographic primitives in Python is called simply cryptography. It has both "secure" primitives as well as a "hazmat" layer. The "hazmat" layer requires care and knowledge of cryptography and it is easy to implement security holes using it. We will not cover anything in the "hazmat" layer in this introductory article!
The most useful high-level secure primitive in cryptography is the Fernet implementation. Fernet is a standard for encrypting buffers in a way that follows best-practices cryptography. It is not suitable for very big files—anything in the gigabyte range and above—since it requires you to load the whole buffer that you want to encrypt or decrypt into memory at once.
Fernet supports symmetric, or secret key, cryptography: the same key is used for encryption and decryption, and therefore must be kept safe.
Generating a key is easy:
>>> k = fernet.Fernet.generate_key() >>> type(k) <class 'bytes'>
Those bytes can be written to a file with appropriate permissions, ideally on a secure machine.
Once you have key material, encrypting is easy as well:
>>> frn = fernet.Fernet(k) >>> encrypted = frn.encrypt(b"x marks the spot") >>> encrypted[:10] b'gAAAAABb1'
You will get slightly different values if you encrypt on your machine. Not only because (I hope) you generated a different key from me, but because Fernet concatenates the value to be encrypted with some randomly generated buffer. This is one of the "best practices" I alluded to earlier: it will prevent an adversary from being able to tell which encrypted values are identical, which is sometimes an important part of an attack.
Decryption is equally simple:
>>> frn = fernet.Fernet(k) >>> frn.decrypt(encrypted) b'x marks the spot'
Note that this only encrypts and decrypts byte strings. In order to encrypt and decrypt text strings, they will need to be encoded and decoded, usually with UTF-8.
One of the most interesting advances in cryptography in the mid-20th century was public key cryptography. It allows the encryption key to be published while the decryption key is kept secret. It can, for example, be used to store API keys to be used by a server: the server is the only thing with access to the decryption key, but anyone can add to the store by using the public encryption key.
While cryptography does not have any public key cryptographic secure primitives, the PyNaCl library does. PyNaCl wraps and offers some nice ways to use the NaCl encryption system invented by Daniel J. Bernstein.
NaCl always encrypts and signs or decrypts and verifies signatures simultaneously. This is a way to prevent malleability-based attacks, where an adversary modifies the encrypted value.
Encryption is done with a public key, while signing is done with a secret key:
>>> from nacl.public import PrivateKey, PublicKey, Box >>> source = PrivateKey.generate() >>> with open("target.pubkey", "rb") as fpin: ... target_public_key = PublicKey(fpin.read()) >>> enc_box = Box(source, target_public_key) >>> result = enc_box.encrypt(b"x marks the spot") >>> result[:4] b'\xe2\x1c0\xa4'
Decryption reverses the roles: it needs the private key for decryption and the public key to verify the signature:
>>> from nacl.public import PrivateKey, PublicKey, Box >>> with open("source.pubkey", "rb") as fpin: ... source_public_key = PublicKey(fpin.read()) >>> with open("target.private_key", "rb") as fpin: ... target = PrivateKey(fpin.read()) >>> dec_box = Box(target, source_public_key) >>> dec_box.decrypt(result) b'x marks the spot'
The PocketProtector library builds on top of PyNaCl and contains a complete secrets management solution.