Decrypting encrypted data means recovering the plaintext from the ciphertext, using the same key (or the corresponding key) it was encrypted with. It sounds like the mirror image of encryption, but in practice you need three pieces of correct information together: the algorithm, the key, and the mode's parameters, such as the IV, nonce, or authentication tag. Miss even one of these and decryption fails, or in some cases produces corrupted output without even flagging an error.

Requests for "how do I decrypt" a file or a message almost always come from one of these scenarios: an encrypted backup whose password you've lost but not the derived key, forensic analysis on a device under warrant, or a CTF challenge. In the two previous articles we covered the basic concepts of cryptography and how symmetric, asymmetric, and hashing work. Here we look at the flip side: how decryption actually works in practice, and where legitimate decryption ends and cryptanalysis begins.

Decryption or cryptanalysis: where the line is

The line between the two comes down to the key. If you have it and use it to reverse the encryption, you're decrypting. If you don't have it and try to reconstruct it or bypass it starting from the ciphertext, that's cryptanalysis: brute force against weak keys, known-plaintext attacks, chosen-ciphertext attacks, or side-channel attacks like a padding oracle against CBC.

The distinction matters because it completely changes the technical approach. Decryption is a deterministic operation: correct key, correct output, in milliseconds. Cryptanalysis is probabilistic and often computationally expensive, and it only makes sense in authorized contexts such as pentesting, forensics under warrant, or CTF competitions.

Decrypting AES-CBC in Python

CBC (Cipher Block Chaining) requires the key and the IV (initialization vector) used at encryption time, plus removal of the PKCS7 padding applied before encrypting.

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
import os

key = os.urandom(32)  # AES-256
iv = os.urandom(16)

# encryption
padder = padding.PKCS7(128).padder()
padded_data = padder.update(b"secret message") + padder.finalize()

encryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()

# decryption: you need both the key and the IV, the key alone isn't enough
decryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()

unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()

print(plaintext)  # b'secret message'

A common error during CBC decryption is an unpadding exception: it means the padding bytes expected at the end of the text don't add up, almost always because the key or IV used to decrypt aren't the right ones, or because the ciphertext was altered in transit. This same behavior, if an application exposes it as an error message distinguishable from the outside, is the basis of the padding oracle attack: an attacker can use the difference between "valid padding" and "invalid padding" to decrypt a message without ever knowing the key.

Decrypting AES-GCM: the role of the authentication tag

GCM (Galois/Counter Mode) behaves differently from CBC on one decisive point: if the key, nonce, or ciphertext don't match exactly, decryption doesn't produce corrupted output, it raises an explicit exception.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
import os

key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12)

ciphertext = aesgcm.encrypt(nonce, b"secret message", None)

try:
    plaintext = aesgcm.decrypt(nonce, ciphertext, None)
except InvalidTag:
    print("Key, nonce or ciphertext don't match: decryption impossible")

This property, called authentication, is why GCM has replaced CBC as the default mode: it silently prevents ciphertext tampering, and also makes debugging much easier when decryption fails.

Decrypting with RSA: the private key in action

RSA only decrypts with the private key corresponding to the public key used for encryption. In practice RSA doesn't encrypt bulk data, as explained in the article on asymmetric cryptography: it's used to exchange a symmetric session key, not the actual content.

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()

oaep = padding.OAEP(
    mgf=padding.MGF1(algorithm=hashes.SHA256()),
    algorithm=hashes.SHA256(),
    label=None
)

ciphertext = public_key.encrypt(b"AES session key", oaep)
plaintext = private_key.decrypt(ciphertext, oaep)

print(plaintext)  # b'AES session key'

The RSA keys used here are conceptually the same keys you generate for SSH authentication: a public/private pair where only whoever holds the private key can decrypt (or sign).

When decryption becomes an attack

If the key isn't available, attempting to decrypt anyway requires a different strategy depending on how the key was generated:

  • key derived from a weak password: a dictionary or brute-force attack against the derivation function (PBKDF2, Argon2) can reconstruct the key if the password is common or short, using tools like hashcat.
  • insufficient key length: 64 or 128-bit AES keys generated with weak pseudo-random generators are attackable in reasonable time with dedicated hardware; with AES-256 and a cryptographically secure generator, direct brute force against the key remains computationally impractical.
  • IV or nonce reuse: in CBC, a reused IV exposes patterns in the encrypted blocks; in GCM, reusing the nonce with the same key is more serious, because it allows recovering the authentication tag and forging messages.
  • padding oracle: if an application distinguishes between a padding error and other errors, an attacker can decrypt a CBC message byte by byte without ever having the key, by repeatedly querying the oracle.

These are pentest or CTF scenarios, not routine operations: they require explicit authorization when applied to systems you don't own.

Which scenario actually applies to you

Scenario Do you have the key? Approach Typical tools
Recovering a personal encrypted backup Yes, but the password is lost Re-derive the key with the same KDF (PBKDF2/Argon2) used at encryption time Python scripts, OpenSSL
Forensic analysis under warrant Often no Extracting the key from the device's memory or keystore Volatility, Autopsy
CTF or research in a controlled environment No, that's the point of the challenge Cryptanalysis: brute force, padding oracle, known-plaintext hashcat, John the Ripper, CyberChef
Debugging an application that encrypts its own data Yes Direct decryption as in the examples above a dedicated script in the same language as the app

Resources