mrkeyoor.com_
Thu 06 Aug 08:49 UTC
PyPISecurityupdated 06 Aug 2026

pycryptodomex

pycryptodomex is PyCryptodome installed under the Cryptodome namespace instead of Crypto. It is the exact same library as the pycryptodome package, built from the same source, with one difference: the import path. That distinction exists because the abandoned PyCrypto library owned the Crypto name, and two packages fighting over the same top-level module in one environment breaks both. If you pick pycryptodomex you write from Cryptodome.Cipher import AES and nothing else in the environment cares. The library itself is a self-contained collection of low-level cryptographic primitives: block and stream ciphers with authenticated modes (GCM, CCM, EAX, SIV, OCB, KW), RSA, DSA, elliptic curves including Ed25519 and X25519, the SHA-2 and SHA-3 families, BLAKE2, HMAC and Poly1305, key derivation functions like scrypt and PBKDF2, and HPKE. It is not a wrapper around OpenSSL: most of it is Python, with C extensions only where speed matters.

Verdict

A complete, well-tested primitives library, and the -x package name is the right pick whenever the Crypto namespace might already be taken. Reach for it when you need an algorithm the cryptography package does not ship, not as your default, because primitives with no guard rails are easy to hold wrong.

API stability5/5The 3.x line has been additive for years, the changelog for each release is mostly new algorithms and fixes rather than breaking changes, and the only removals since 3.19 have been Python 3.5 and 3.6 support; code written against 3.9 still runs on 3.23
Docs4/5pycryptodome.org documents every module with parameter descriptions and runnable examples, and the changelog is specific about which GitHub issue each fix addresses; what is missing is guidance on which primitive to pick, so the docs tell you how to use AES-SIV but not when you should
Maintenance3/5Pushed 2026-07-18 and CVE-2023-52323 was fixed promptly, but 3.23.0 is from 2025-05-17 and 3.24.0 has sat under development since, with 54 open issues out of 87 open issues and PRs; it is one maintainer carrying a security-critical library used by millions of installs a week
Ecosystem4/5Around 17.7M downloads a week for the -x variant alone and a common transitive dependency of blockchain, PDF, and protocol libraries, but the cryptography package is the one most new projects and security reviewers default to, so this is the specialist option

Use it if

  • You need a primitive that the cryptography package does not expose, such as SIV or OCB mode, KangarooTwelve, TupleHash, cSHAKE, Shamir's Secret Sharing, or HPKE from RFC 9180
  • Something else in your environment already imports Crypto and you cannot risk the namespace collision, which is the whole reason this package name exists
  • You are maintaining code written against PyCrypto and want the smallest possible diff: the module layout and most call signatures carry over, with the top-level name changed
  • You want no OpenSSL dependency in your wheel: pycryptodomex ships prebuilt binaries with its own C code, so it does not inherit your system OpenSSL version or its CVE schedule
  • You need to read or write a specific on-the-wire key format, like a password-protected PKCS#8 container or an OpenSSH key, without hand-rolling ASN.1
Skip it if

Setup reality

pip install pycryptodomex gives you 40 prebuilt wheels covering CPython from 2.7 upward across Linux, macOS, Windows, and Windows ARM, so most people never compile anything. When there is no matching wheel, the sdist builds C extensions and you need a compiler plus Python headers, which is the usual cause of a failed install inside a slim Docker image. The import path is the part that actually costs time: everything is Cryptodome, not Crypto, so every StackOverflow answer, every LLM completion, and every PyCrypto-era tutorial you copy will be wrong by one word. Do not install pycryptodome and pycryptodomex together, and never have either alongside the ancient pycrypto package, because they collide in ways that surface as missing attributes rather than clean errors. On Unix, installing GMP separately speeds up RSA and DSA operations noticeably; without it the library falls back to its own bignum code. There are no runtime Python dependencies at all.

Patterns

Authenticated encryption with AES-GCMaes-gcm

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

key = get_random_bytes(32)

cipher = AES.new(key, AES.MODE_GCM)
ciphertext, tag = cipher.encrypt_and_digest(b"attack at dawn")
nonce = cipher.nonce            # 16 bytes, generated for you

decryptor = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = decryptor.decrypt_and_verify(ciphertext, tag)   # ValueError if tampered

Use encrypt_and_digest and decrypt_and_verify, never plain encrypt/decrypt in GCM, or you get ciphertext with no integrity check and silently accept forged data. A cipher object is single use: reusing one to encrypt a second message reuses the nonce and destroys the security of both.

Bind ciphertext to context with associated dataaead-with-associated-data

from Cryptodome.Cipher import ChaCha20_Poly1305
from Cryptodome.Random import get_random_bytes

key = get_random_bytes(32)
header = b'{"user_id":42,"v":1}'

cipher = ChaCha20_Poly1305.new(key=key)
cipher.update(header)                       # authenticated, not encrypted
ct, tag = cipher.encrypt_and_digest(b"session payload")

d = ChaCha20_Poly1305.new(key=key, nonce=cipher.nonce)
d.update(header)
pt = d.decrypt_and_verify(ct, tag)

update() must be called with the identical bytes before decrypting or verification fails, which is the point: it stops an attacker moving a valid ciphertext to a different user or version. ChaCha20-Poly1305 is the sensible default when the CPU has no AES-NI, for example on small ARM boards.

AES-CBC with correct padding, when a protocol forces itaes-cbc-padding

from Cryptodome.Cipher import AES
from Cryptodome.Hash import HMAC, SHA256
from Cryptodome.Util.Padding import pad, unpad
from Cryptodome.Random import get_random_bytes

enc_key, mac_key = get_random_bytes(32), get_random_bytes(32)
cipher = AES.new(enc_key, AES.MODE_CBC)
ct = cipher.encrypt(pad(b"legacy payload", AES.block_size))
mac = HMAC.new(mac_key, cipher.iv + ct, digestmod=SHA256).digest()

HMAC.new(mac_key, cipher.iv + ct, digestmod=SHA256).verify(mac)   # before decrypting
pt = unpad(AES.new(enc_key, AES.MODE_CBC, iv=cipher.iv).decrypt(ct), AES.block_size)

CBC gives you no integrity, so you have to add an HMAC and verify it before decryption, with a separate key. Skipping that ordering is the padding oracle. If nothing external is forcing CBC on you, use MODE_GCM and delete all of this.

Generate, export, and import RSA keysrsa-keys

from Cryptodome.PublicKey import RSA

key = RSA.generate(3072)

private_pem = key.export_key(
    format="PEM", passphrase="correct horse", pkcs=8,
    protection="scryptAndAES128-CBC",
)
public_pem = key.public_key().export_key(format="PEM")

loaded = RSA.import_key(private_pem, passphrase="correct horse")
assert loaded.has_private()

pkcs=8 with an explicit protection string is what you want for a password-protected key; the default PEM encryption is the old, weak DES-EDE3-CBC scheme. RSA.generate(3072) takes a noticeable fraction of a second, so generate once and store, never per request.

Encrypt a symmetric key to an RSA public keyrsa-oaep-encrypt

from Cryptodome.Cipher import PKCS1_OAEP, AES
from Cryptodome.Hash import SHA256
from Cryptodome.PublicKey import RSA
from Cryptodome.Random import get_random_bytes

pub = RSA.import_key(open("pub.pem", "rb").read())
session_key = get_random_bytes(32)

wrapped = PKCS1_OAEP.new(pub, hashAlgo=SHA256).encrypt(session_key)

cipher = AES.new(session_key, AES.MODE_GCM)
body, tag = cipher.encrypt_and_digest(b"...large payload...")

RSA can only encrypt a few dozen bytes, so encrypt a random AES key and use that for the data. Always pass 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 anything new.

Sign and verify with RSA-PSSrsa-pss-sign

from Cryptodome.Hash import SHA256
from Cryptodome.Signature import pss
from Cryptodome.PublicKey import RSA

key = RSA.import_key(open("priv.pem", "rb").read())
h = SHA256.new(b"invoice #4021")
signature = pss.new(key).sign(h)

try:
    pss.new(key.public_key()).verify(SHA256.new(b"invoice #4021"), signature)
except (ValueError, TypeError):
    raise SystemExit("signature rejected")

verify() raises on failure and returns None on success, so a bare call with no try block means every signature passes. You hash first and pass the hash object, not the bytes. Use pss for new work; Cryptodome.Signature.pkcs1_15 exists only for protocols that mandate it.

ECDSA on P-256 and Ed25519ecc-signatures

from Cryptodome.PublicKey import ECC
from Cryptodome.Signature import DSS, eddsa
from Cryptodome.Hash import SHA256

k = ECC.generate(curve="p256")
h = SHA256.new(b"payload")
sig = DSS.new(k, "fips-186-3").sign(h)
DSS.new(k.public_key(), "fips-186-3").verify(h, sig)

ed = ECC.generate(curve="Ed25519")
signer = eddsa.new(ed, "rfc8032")
sig2 = signer.sign(b"payload")
eddsa.new(ed.public_key(), "rfc8032").verify(b"payload", sig2)

DSS mode 'fips-186-3' uses a random nonce; pass 'deterministic-rfc6979' instead when you need reproducible signatures or do not trust the RNG on the device. Ed25519 signs the message directly rather than a hash object, which is the opposite of the ECDSA call and a common copy-paste bug.

Hashes, HMAC, and the SHA-3 familyhash-and-mac

from Cryptodome.Hash import SHA256, SHA3_256, BLAKE2b, HMAC, KMAC128

SHA256.new(b"data").hexdigest()
SHA3_256.new(b"data").hexdigest()
BLAKE2b.new(digest_bits=256, key=b"mac-key").update(b"data").hexdigest()

mac = HMAC.new(b"shared-secret", digestmod=SHA256)
mac.update(b"body")
mac.verify(expected_tag)          # constant time, raises ValueError on mismatch

KMAC128.new(key=b"k" * 16, data=b"data", mac_len=32).hexdigest()

Use mac.verify(expected) rather than comparing hexdigest() strings with ==, because == on the tag leaks timing. HMAC needs digestmod passed explicitly, unlike the stdlib version, and forgetting it raises rather than defaulting to something weak.

Derive keys with scrypt, PBKDF2, and HKDFkey-derivation

from Cryptodome.Protocol.KDF import scrypt, PBKDF2, HKDF
from Cryptodome.Hash import SHA256
from Cryptodome.Random import get_random_bytes

salt = get_random_bytes(16)
key = scrypt(b"user password", salt, key_len=32, N=2**17, r=8, p=1)

legacy = PBKDF2(b"user password", salt, dkLen=32, count=600_000,
                hmac_hash_module=SHA256)

enc_key, mac_key = HKDF(master_secret, 32, salt, SHA256, num_keys=2,
                        context=b"v1 session")

HKDF is for splitting an already-strong secret and is wrong for passwords, which need scrypt or argon2. scrypt with N=2**17 allocates roughly 128 MiB per call, so a login endpoint doing this in-process will fall over under concurrency long before the CPU does.

Hybrid public key encryption (RFC 9180)hpke-encrypt

from Cryptodome.Protocol import HPKE
from Cryptodome.PublicKey import ECC

receiver = ECC.generate(curve="Curve25519")

sender = HPKE.new(receiver_key=receiver.public_key(),
                  aead_id=HPKE.AEAD.AES256_GCM,
                  info=b"telemetry v1")
ct = sender.seal(b"metrics blob", auth_data=b"device-17")
enc = sender.enc                     # send this alongside the ciphertext

recv = HPKE.new(receiver_key=receiver, enc=enc,
                aead_id=HPKE.AEAD.AES256_GCM, info=b"telemetry v1")
pt = recv.unseal(ct, auth_data=b"device-17")

Added in 3.22.0, so pin >=3.22 if you use it. The direction is decided by the key you pass: a public key makes a sealing context, a private key an unsealing one, and the same context can seal many messages in order but cannot do both. info and auth_data must match exactly on both sides.

Split a secret with Shamir's schemesecret-sharing

from Cryptodome.Protocol.SecretSharing import Shamir
from Cryptodome.Random import get_random_bytes

master = get_random_bytes(16)                 # exactly 16 bytes
shares = Shamir.split(3, 5, master)           # any 3 of 5 recover it

for index, share in shares:
    print(index, share.hex())

recovered = Shamir.combine(shares[:3])
assert recovered == master

The secret must be exactly 16 bytes, which in practice means you split an AES-128 key and use that to wrap the real payload. Shares carry no integrity check, so a corrupted or malicious share produces a plausible wrong secret rather than an error; wrap them with a MAC if that matters.

Cryptodome versus Crypto, and how to support bothnamespace-migration

# pycryptodomex  ->  from Cryptodome.Cipher import AES
# pycryptodome   ->  from Crypto.Cipher import AES

try:
    from Cryptodome.Cipher import AES
    from Cryptodome.Random import get_random_bytes
except ImportError:                     # library installed under the legacy name
    from Crypto.Cipher import AES
    from Crypto.Random import get_random_bytes

# check what is actually installed
#   python -c "import Cryptodome; print(Cryptodome.__version__)"
#   pip list | grep -i pycryptodome

Everything below the top-level name is identical, so this shim is the whole migration. What you must not do is depend on both distributions: pip will install them happily, and the failure shows up later as a module that exists but is missing attributes. Pick one per environment and pin it.

Alternatives

PackageRegistryPick it when
cryptographyPyPIYou are writing new code and want the ecosystem default, an OpenSSL-backed backend, and a high-level recipes layer that is harder to misuse
pycryptodomePyPIYou are replacing the dead PyCrypto library in place and want the imports to keep saying Crypto, in an environment you fully control
pynaclPyPIYou want a small opinionated set of modern primitives from libsodium with almost no knobs to get wrong