mrkeyoor.com_
Thu 06 Aug 07:41 UTC
PyPISecurityupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5The Crypto.Cipher, Crypto.Hash and Crypto.Signature layouts have been stable across the whole 3.x line, and releases add algorithms rather than change signatures. The breaking changes over the last several years have been dropping end-of-life Python versions, 3.6 in 3.22.0 and 3.5 in 3.21.0.
Docs4/5pycryptodome.org documents every module with API signatures and a runnable example, and there are pages on the PyCrypto differences and the Crypto versus Cryptodome choice. What it does not do is tell you which algorithm to use, so the docs assume you already know GCM beats CBC and why a nonce must never repeat.
Maintenance3/5The repo was pushed July 2026 and has 54 open issues (87 counting PRs), so it is alive, but 3.23.0 is from May 2025 and the unreleased 3.24.0 section lists a single OID fix. Development is overwhelmingly one person, which is a real bus-factor question for a security dependency.
Ecosystem4/5Around 23.9M downloads a week and it appears throughout tooling that inherited PyCrypto-era code. The catch is that new libraries generally depend on pyca/cryptography instead, so pycryptodome tends to arrive as a transitive dependency rather than a deliberate choice.

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
Skip it if

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 bytes

Leaving 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

PackageRegistryPick it when
cryptographyPyPINew code that needs mainstream primitives, X.509 certificates, or a high-level recipe like Fernet with the choices already made for you
pynaclPyPIYou want libsodium's opinionated box and secretbox APIs, where the algorithm and nonce handling are decided for you
pycryptodomexPyPISomething else in your environment still needs the old Crypto namespace and you need the two to coexist