pycryptodome
PyCryptodome is a self-contained collection of low-level cryptographic primitives for Python. It is a maintained fork of the abandoned PyCrypto, and it keeps the same Crypto package name so old code mostly still imports. Everything is organised by what it does: Crypto.Cipher for AES, ChaCha20 and RSA encryption, Crypto.Hash for SHA-2, SHA-3, BLAKE2 and HMAC, Crypto.Signature for PSS, PKCS#1 v1.5, ECDSA and EdDSA, Crypto.PublicKey for generating and importing RSA, DSA and elliptic curve keys, Crypto.Protocol for KDFs like scrypt, HKDF and PBKDF2 plus Shamir secret sharing, and Crypto.Random for OS-sourced random bytes. Unlike pyca/cryptography it does not wrap OpenSSL: most algorithms are implemented in the project itself, with C extensions only where speed demands it. That makes it easy to install anywhere and means the maintainers own every line of the crypto.
The best-maintained descendant of PyCrypto and the widest algorithm catalogue in Python, but it hands you sharp primitives with no guardrails and no audit. Reach for it when you need an algorithm or a PyCrypto-compatible import path that pyca/cryptography does not give you, and default to cryptography otherwise.
Use it if
- You are maintaining code written against PyCrypto: the Crypto package name and most call signatures carry over, and pycryptodome is the only fork that is still getting fixes
- You need an algorithm pyca/cryptography does not expose: SIV, OCB, CCM, KW and KWP cipher modes, KangarooTwelve, cSHAKE, KMAC, TupleHash, HPKE, or Shamir secret sharing
- You want a dependency that installs anywhere: wheels cover Linux, macOS, Windows including ARM, and PyPy, and there is no OpenSSL version to match at build or run time
- You are implementing a protocol from a specification and need direct access to the primitive, with explicit nonces, tags and padding rather than a high-level envelope
- This is new code and you do not have a specific reason: pyca/cryptography is the usual default because it is OpenSSL-backed, has had external security review, ships high-level recipes like Fernet, and handles X.509
- You need certificates: there is no X.509 parsing, no certificate chain validation, no CSR generation, and no TLS anything, so anything PKI-shaped means a second library
- You want the library to stop you making a mistake: nothing here refuses ECB mode, warns about a reused GCM nonce, or checks that you verified the tag before using plaintext, and each of those is a total break of confidentiality or integrity
- You need a compliance story: there is no FIPS validation and no published third-party audit, which matters if an auditor asks where your crypto comes from
- Release cadence is a concern: 3.23.0 shipped in May 2025 and 3.24.0 is still marked under development well over a year later, with one bugfix listed, from a project that is effectively one maintainer
- You are hashing passwords: scrypt and bcrypt are in here as primitives with parameters you must choose correctly, while argon2-cffi gives you a sensible default and a verify function that handles re-hashing
Setup reality
pip install pycryptodome pulls a prebuilt wheel for mainstream platforms; the 3.23.0 release published 41 files covering CPython from 2.7 upward, macOS, manylinux, musllinux and Windows including ARM. Off that list it compiles C extensions and you need a compiler and Python headers. The real trap is the package name. Modules install under Crypto, the same namespace the dead PyCrypto used, so having both installed at once produces a broken hybrid that fails in confusing ways at import time. If anything in your dependency tree might still pull PyCrypto, install pycryptodomex instead, which is the identical library under the Cryptodome namespace and can coexist. Public key operations are much faster when GMP is present on Unix, and you can force it off with the PYCRYPTODOME_DISABLE_GMP environment variable. Python 3.6 support was dropped in 3.22.0. Nothing about the install tells you which algorithm to pick, and this library will happily let you build AES-ECB.
Patterns
Get cryptographically secure random datagenerate-random-bytes
from Crypto.Random import get_random_bytes
key = get_random_bytes(32) # AES-256 key
salt = get_random_bytes(16)This reads straight from the OS CSPRNG, not a userspace generator, which is one of the changes from PyCrypto. Never use the random module for keys, nonces, or salts: it is a Mersenne Twister and its output is predictable from a few samples.
Encrypt with AES-GCM and get an auth tagaes-gcm-encrypt
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
key = get_random_bytes(32)
cipher = AES.new(key, AES.MODE_GCM)
cipher.update(b"user-id:1234") # authenticated, not encrypted
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
blob = cipher.nonce + tag + ciphertext # 16 + 16 + n bytesLeaving nonce out makes pycryptodome generate a random 16-byte one, which you must store; it is not secret. Reusing a key and nonce pair in GCM leaks the XOR of both plaintexts and lets an attacker forge tags, so generate a fresh cipher object for every message and never rewind.
Decrypt and actually check the tagaes-gcm-decrypt
nonce, tag, ciphertext = blob[:16], blob[16:32], blob[32:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
cipher.update(b"user-id:1234")
try:
plaintext = cipher.decrypt_and_verify(ciphertext, tag)
except ValueError:
raise SecurityError("ciphertext was tampered with")decrypt_and_verify raises ValueError('MAC check failed') and is the only safe way to do this. Calling plain decrypt() skips the check and hands you attacker-controlled bytes that look like plaintext. The associated data passed to update() must match exactly or verification fails.
Use ChaCha20-Poly1305 where AES-NI is absentchacha20-poly1305
from Crypto.Cipher import ChaCha20_Poly1305
cipher = ChaCha20_Poly1305.new(key=key) # 12-byte nonce
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
plain = ChaCha20_Poly1305.new(key=key, nonce=cipher.nonce)\
.decrypt_and_verify(ciphertext, tag)Same authenticated construction as GCM but constant-time in software, which matters on hardware without AES instructions. The default nonce is 12 bytes; pass a 24-byte nonce and you silently get XChaCha20-Poly1305 instead, so be explicit about which one you are storing.
Turn a password into a key with scryptderive-key-from-password
from Crypto.Protocol.KDF import scrypt
from Crypto.Random import get_random_bytes
salt = get_random_bytes(16)
key = scrypt(password, salt, key_len=32, N=2 ** 20, r=8, p=1)Store the salt and every parameter beside the ciphertext or you can never derive the same key again. N must be a power of two and directly sets memory use: 2**20 with r=8 costs about 1 GB, so tune it to the machine that will run it. Pass num_keys=2 to get a list of independent keys from one derivation instead of splitting one.
Split one shared secret into several keyshkdf-expand
from Crypto.Protocol.KDF import HKDF
from Crypto.Hash import SHA256
enc_key, mac_key = HKDF(
master=shared_secret, key_len=32, salt=salt,
hashmod=SHA256, num_keys=2, context=b"v1 session keys",
)HKDF is for high-entropy input such as a Diffie-Hellman output; it is not a password KDF and offers no work factor. The context string is what stops keys derived for one purpose being valid for another, so give each use a distinct label.
Authenticate a message and compare safelyhmac-verify
from Crypto.Hash import HMAC, SHA256
digest = HMAC.new(mac_key, payload, digestmod=SHA256).hexdigest()
try:
HMAC.new(mac_key, payload, digestmod=SHA256).hexverify(received)
except ValueError:
raise SecurityError("bad signature")Use verify() or hexverify() rather than comparing digests with ==. The == operator returns as soon as bytes differ, which leaks the position of the first mismatch and lets an attacker recover a valid tag one byte at a time.
Hash data in chunkshash-a-large-file
from Crypto.Hash import SHA256
h = SHA256.new()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1024 * 1024), b""):
h.update(chunk)
print(h.hexdigest())update() is incremental so memory stays flat on any file size. Crypto.Hash.new('SHA256') builds the same object from a string name when the algorithm comes from configuration. For plain hashing with no exotic algorithm, Python's own hashlib is already there and needs no dependency.
Generate an RSA key and store it encryptedrsa-keypair-and-storage
from Crypto.PublicKey import RSA
key = RSA.generate(3072)
encrypted = key.export_key(
passphrase=passphrase, pkcs=8,
protection="scryptAndAES256-CBC",
)
public = key.publickey().export_key()
loaded = RSA.import_key(encrypted, passphrase=passphrase)The protection string is matched exactly and an unrecognised one raises ValueError('Unknown protection'); scryptAndAES256-CBC and PBKDF2WithHMAC-SHA512AndAES256-GCM are both valid. Without pkcs=8 and protection the private key is written in plaintext PEM.
Encrypt a key with RSA-OAEPrsa-oaep-encrypt
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Hash import SHA256
cipher = PKCS1_OAEP.new(public_key, hashAlgo=SHA256)
wrapped = cipher.encrypt(session_key)
session_key = PKCS1_OAEP.new(private_key, hashAlgo=SHA256).decrypt(wrapped)RSA can only encrypt less than the modulus size, so this is for wrapping a symmetric key, never for the message itself. Set hashAlgo explicitly: the default is SHA-1, which still interoperates but is not what you want in new code. Never use PKCS1_v1_5 for encryption in new designs; it is padding-oracle prone.
Sign with RSA-PSS or ECDSAsign-and-verify
from Crypto.Signature import pss, DSS
from Crypto.Hash import SHA256
from Crypto.PublicKey import ECC
h = SHA256.new(message)
signature = pss.new(rsa_private).sign(h)
pss.new(rsa_public).verify(SHA256.new(message), signature)
ec = ECC.generate(curve="p256")
sig = DSS.new(ec, "fips-186-3").sign(SHA256.new(message))Verification raises ValueError on failure and returns None on success, so it must be wrapped in try/except and never treated as a boolean. A hash object is consumed by sign or verify, so build a fresh one for each call rather than reusing h.
Sign with Ed25519ed25519-sign
from Crypto.PublicKey import ECC
from Crypto.Signature import eddsa
key = ECC.generate(curve="Ed25519")
signature = eddsa.new(key, "rfc8032").sign(message)
eddsa.new(key.public_key(), "rfc8032").verify(message, signature)EdDSA signs the raw message, not a hash object, which is the opposite of the pss and DSS calls above. Ed25519 has no per-signature randomness to get wrong and produces a fixed 64-byte signature, which makes it a safer default than ECDSA when you control both ends.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| cryptography | PyPI | New code that needs mainstream primitives, X.509 certificates, or a high-level recipe like Fernet with the choices already made for you |
| pynacl | PyPI | You want libsodium's opinionated box and secretbox APIs, where the algorithm and nonce handling are decided for you |
| pycryptodomex | PyPI | Something else in your environment still needs the old Crypto namespace and you need the two to coexist |