Symmetric encryption uses one secret key to both lock and unlock data; asymmetric encryption uses a mathematically linked key pair — one public, one private. That distinction sounds academic until you're staring at a code review comment asking why a service encrypts session tokens with RSA-2048 (roughly 1,000-5,000 operations per second on commodity hardware) instead of AES-256-GCM (routinely 1-3 GB/s with AES-NI). Python's cryptography library (currently 43.x as of mid-2026) makes both approaches a few lines of code, but the wrong choice — or a subtly wrong implementation — is how encrypted-at-rest databases and "secure" API tokens end up in breach reports. If you came here looking for concrete examples of asymmetric encryption rather than another definitions page, that's what the RSA-OAEP walkthrough below gives you: real, runnable code. This post walks through working Python examples of AES-256-GCM and RSA-OAEP, explains when to reach for each, and flags the mistakes (ECB mode, static IVs, unauthenticated ciphertext) that static analysis tools flag constantly in real codebases.
What's the actual difference between symmetric and asymmetric encryption?
Symmetric encryption encrypts and decrypts with the identical key, while asymmetric encryption encrypts with a public key and decrypts only with the matching private key. AES (Advanced Encryption Standard) is the dominant symmetric cipher — AES-128, AES-192, and AES-256 refer to key length in bits, with AES-256 the standard for regulated data (FIPS 140-3, PCI DSS 4.0). RSA and elliptic-curve schemes (ECDH, ECDSA) are the common asymmetric algorithms; RSA-2048 is the current minimum for new deployments, with NIST recommending a move to RSA-3072 or ECC (P-256/P-384) beyond 2030 per SP 800-131A. The practical trade-off is speed versus key distribution: symmetric ciphers are orders of magnitude faster but require both parties to already share a secret, while asymmetric ciphers solve the key-distribution problem at a steep computational cost — encrypting 1 MB of data with RSA-2048 directly isn't even supported by the algorithm's math (OAEP padding caps RSA-2048 payloads at 190 bytes), which is why asymmetric encryption is almost never used to encrypt bulk data directly.
How do you implement AES-256-GCM encryption in Python?
You implement AES-256-GCM with Python's cryptography package using AESGCM, a 32-byte key, and a unique 12-byte nonce per message. GCM (Galois/Counter Mode) is an authenticated encryption mode — it produces ciphertext plus a 16-byte authentication tag, so tampering is detected on decryption rather than silently accepted, unlike plain CBC or ECB. Here's a complete, runnable example:
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
def encrypt(plaintext: bytes, key: bytes) -> tuple[bytes, bytes]:
nonce = os.urandom(12) # 96-bit nonce, per NIST SP 800-38D
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data=None)
return nonce, ciphertext
def decrypt(nonce: bytes, ciphertext: bytes, key: bytes) -> bytes:
aesgcm = AESGCM(key)
return aesgcm.decrypt(nonce, ciphertext, associated_data=None)
key = AESGCM.generate_key(bit_length=256)
nonce, ct = encrypt(b"customer_ssn=078-05-1120", key)
plaintext = decrypt(nonce, ct, key)
Two details matter for production code review: the nonce must never repeat under the same key (reuse breaks GCM's confidentiality guarantees within roughly 2^32 messages per key, per the NIST guidance), and the key itself should come from a KMS (AWS KMS, GCP Cloud KMS, HashiCorp Vault) rather than os.environ or a hardcoded constant — the single most common finding in secrets-scanning reports from 2024-2026 vulnerability disclosures.
How do you implement RSA encryption in Python?
RSA is one of the clearest examples of asymmetric encryption algorithms in everyday use, alongside elliptic-curve schemes like ECDH and ECDSA. You implement RSA with cryptography's rsa module, generating a key pair and encrypting with OAEP padding, never with the deprecated PKCS#1 v1.5 padding scheme that enabled the 1998 Bleichenbacher padding-oracle attack (and its 2017 revival, ROBOT, which affected Facebook, PayPal, and F5 load balancers). A minimal 2,048-bit example:
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
message = b"session_key_material"
ciphertext = public_key.encrypt(
message,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
Note the public exponent is hardcoded to 65537 (0x10001) — this is the accepted default since a 1990s-era analysis showed smaller exponents like 3 could be exploited under certain padding conditions. Key generation for RSA-2048 takes roughly 100-300ms on a modern CPU core versus microseconds for an AES key, which is the second reason (after the payload-size limit) that RSA is used to wrap a symmetric key rather than to encrypt application data directly.
When should you use symmetric versus asymmetric encryption in a real system?
You should use symmetric encryption for the actual data and asymmetric encryption only to exchange the symmetric key — this pattern, called hybrid encryption, is exactly what TLS 1.3 does on every HTTPS connection. TLS 1.3's handshake uses ECDHE (elliptic-curve Diffie-Hellman, an asymmetric scheme) to negotiate a shared secret, then switches to AES-128-GCM or ChaCha20-Poly1305 for the actual HTTP traffic, because encrypting a video stream with RSA would be computationally absurd. The same pattern applies to application design: encrypt file contents or database rows with AES-256-GCM, and if you need to share that AES key with a specific recipient (not just decrypt it yourself later), wrap the 32-byte AES key with RSA-OAEP or an ECIES scheme. PGP/GPG email encryption, S/MIME, and most "envelope encryption" features in AWS KMS and Google Cloud KMS all follow this exact structure — a randomly generated Data Encryption Key (DEK) encrypts the payload, and a Key Encryption Key (KEK), often RSA or ECC-based, encrypts the DEK. TLS's ECDHE handshake, PGP's key exchange, and KMS envelope encryption are the asymmetric encryption examples most engineers rely on daily without ever calling rsa.generate_private_key() themselves.
What Python encryption mistakes create real vulnerabilities?
The most common mistake is using AES in ECB mode, which encrypts identical plaintext blocks into identical ciphertext blocks and visibly leaks data patterns — this is the textbook flaw illustrated by the "ECB penguin" image and it still shows up in code review in 2026, usually via Crypto.Cipher.AES.new(key, AES.MODE_ECB) copy-pasted from outdated Stack Overflow answers. A close second is reusing the deprecated, unmaintained pycrypto package (last released in 2013, with CVE-2013-7459 covering a heap-overflow issue and no fix track record since) instead of its maintained fork pycryptodome or the cryptography library. Other recurring findings from real audits: static or predictable IVs/nonces passed into CBC or GCM modes, encryption keys derived directly from passwords without a KDF like PBKDF2, scrypt, or Argon2 (hashlib.md5(password).digest() used as an AES key is still found in production code), and RSA key sizes below 2048 bits generated by copy-pasted tutorials that default to 1024-bit keys — a size NIST deprecated for new use back in 2013 and considered breakable with sufficient compute today. Each of these is mechanically detectable: they show up as specific function calls, specific mode constants, and specific key-size parameters in source code, which is exactly the kind of pattern static analysis and SCA tooling is built to catch before it merges.
How Safeguard Helps
Safeguard's platform is built to catch exactly these patterns before they reach production and to cut through the noise when they do. Griffin AI reviews code changes and dependency updates for insecure cryptographic usage — weak modes, deprecated libraries like pycrypto, undersized RSA keys — and explains the fix in context rather than just flagging a rule match. Reachability analysis then tells you whether a vulnerable crypto call in a transitive dependency (say, a CVE in an outdated cryptography build) is actually invoked by your application code, so teams triage the handful of exploitable paths instead of every CVE in the SBOM. Safeguard generates and ingests SBOMs across your services to track exactly which cryptographic libraries and versions are in use fleet-wide, and where a fix is available, it opens an auto-fix PR that bumps the dependency or swaps the insecure call pattern — turning a crypto audit finding into a mergeable pull request instead of a backlog ticket.