All topics
Learning hub

Encryption notes for developers

Master Encryption with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallTest yourself — Encryption quizMore notes
Encryption

Encryption Essentials

Encryption Essentials Encryption transforms readable data (plaintext) into an unreadable form (ciphertext) using an algorithm and a key, so that only someone ho

Encryption Essentials

Encryption transforms readable data (plaintext) into an unreadable form (ciphertext) using an algorithm and a key, so that only someone holding the right key can reverse the process. It protects confidentiality of data at rest (files, databases) and in transit (network traffic). Everything below is the mental model you need before reaching for a crypto library — get the primitives right and the libraries do the hard math for you.

Symmetric vs Asymmetric

Symmetric encryption uses the same key to encrypt and decrypt. It's fast and used for bulk data — AES is the modern standard. The hard problem it doesn't solve is key distribution: both parties need the same secret key before they can talk, and if that key leaks, every message it ever protected is compromised.

Asymmetric (public-key) encryption uses a mathematically linked key pair: a public key anyone can have, and a private key only the owner holds. Data encrypted with the public key can only be decrypted with the private key. This solves key distribution — you can publish your public key openly — but it's orders of magnitude slower than symmetric encryption and not designed for large payloads. RSA and elliptic-curve cryptography (ECC) are the common algorithms.

In practice, real systems combine both: TLS, PGP, and most "encrypted file" formats use asymmetric encryption only to exchange a random symmetric key, then encrypt the actual data with that symmetric key. This is called hybrid encryption — you get the key-distribution benefits of asymmetric crypto and the speed of symmetric crypto.

AES in Practice

AES (Advanced Encryption Standard) is a block cipher — it encrypts data in fixed-size 128-bit blocks. A block cipher needs a mode of operation to handle data longer than one block, and the mode you choose matters as much as the key size. AES-256-GCM is the current default recommendation: GCM (Galois/Counter Mode) is an authenticated encryption mode, meaning it detects tampering as well as providing confidentiality, and it doesn't require padding.

const crypto = require('crypto')

function encrypt(plaintext, key) {
  // key must be 32 bytes for AES-256; never reuse an IV with the same key
  const iv = crypto.randomBytes(12) // 96-bit IV is recommended for GCM
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)

  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()])
  const authTag = cipher.getAuthTag() // detects any tampering with ciphertext

  return {
    ciphertext: encrypted.toString('base64'),
    iv: iv.toString('base64'),
    authTag: authTag.toString('base64'),
  }
}

function decrypt({ciphertext, iv, authTag}, key) {
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'))
  decipher.setAuthTag(Buffer.from(authTag, 'base64'))

  // throws if the auth tag doesn't match — ciphertext was tampered with
  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(ciphertext, 'base64')),
    decipher.final(),
  ])
  return decrypted.toString('utf8')
}

// Deriving a key from a password — never use a raw password as an AES key
function deriveKey(password, salt) {
  // scrypt is memory-hard, resists GPU/ASIC brute-forcing better than PBKDF2
  return crypto.scryptSync(password, salt, 32)
}

Note what makes this correct: a fresh random IV (initialization vector) per encryption operation, an authenticated mode (GCM) instead of a bare mode, and a key-derivation function (scrypt/Argon2/PBKDF2) rather than hashing a password once and using it directly as a key.

RSA and Key Exchange

RSA security relies on the difficulty of factoring the product of two large primes. Modern usage requires 2048-bit keys minimum (3072/4096 for longer-term security); anything below 2048 bits is considered breakable with enough compute. RSA is almost never used to encrypt bulk data directly — it's used to encrypt a symmetric key, or for digital signatures.

# Generate a 4096-bit RSA private key
openssl genrsa -out private.pem 4096

# Derive the public key from it
openssl rsa -in private.pem -pubout -out public.pem

# Encrypt a small payload (e.g. a symmetric key) with the public key
openssl pkeyutl -encrypt -pubin -inkey public.pem \
  -in symmetric_key.bin -out encrypted_key.bin \
  -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256

# Decrypt with the private key
openssl pkeyutl -decrypt -inkey private.pem \
  -in encrypted_key.bin -out symmetric_key.bin \
  -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256

# Modern TLS prefers elliptic curve (ECDHE) over RSA key exchange —
# smaller keys, comparable security, and forward secrecy by default
openssl ecparam -genkey -name prime256v1 -out ec_private.pem

The OAEP padding scheme above matters: textbook RSA without proper padding is deterministic and vulnerable to several attacks. Always use a library's high-level API (or OAEP padding explicitly) rather than raw RSA math.

Hashing, MACs, and Digital Signatures

Hashing is not encryption — it's a one-way function that maps input to a fixed-size digest, and there's no key involved and no way to reverse it. It's used for integrity checks, password storage (with a slow, salted algorithm like bcrypt/Argon2, never a raw fast hash like SHA-256), and as a building block for other primitives.

A MAC (Message Authentication Code, e.g. HMAC-SHA256) combines a hash function with a shared secret key to prove both integrity and authenticity — that the message wasn't altered and that it came from someone who holds the key. GCM mode bakes this in automatically via its auth tag, which is why AES-GCM is preferred over AES-CBC-then-HMAC-manually.

A digital signature is the asymmetric equivalent: the sender hashes the message and encrypts the hash with their private key. Anyone with the sender's public key can verify the signature matches the message, proving authenticity (only the private key holder could have produced it) and integrity, without needing a shared secret. This is how code signing, TLS certificates, and "verified commits" work.

const crypto = require('crypto')

// Sign
const {privateKey, publicKey} = crypto.generateKeyPairSync('ed25519')
const message = Buffer.from('transfer $500 to account #4471')
const signature = crypto.sign(null, message, privateKey)

// Verify — anyone with publicKey can do this, no secret required
const isValid = crypto.verify(null, message, publicKey, signature)
console.log(isValid) // true, unless message or signature was altered

Key Management

The algorithm is rarely where encryption implementations fail — key management is. A leaked or mishandled key defeats even perfect math. Production systems typically use a KMS (Key Management Service — AWS KMS, GCP Cloud KMS, HashiCorp Vault) to generate, store, rotate, and audit access to keys, rather than storing raw keys in application code, config files, or environment variables checked into version control.

Envelope encryption is the standard pattern for encrypting large amounts of application data: generate a random data encryption key (DEK) per record or file, encrypt the data with the DEK, then encrypt the DEK itself with a master key stored in the KMS. This means the KMS never touches the bulk data (fast, scalable) and rotating the master key only requires re-wrapping DEKs, not re-encrypting all data.

Common Pitfalls

  • ECB mode — encrypts identical plaintext blocks to identical ciphertext blocks, leaking structural patterns (the classic example: an ECB-encrypted image where you can still see the outline of the original picture). Never use ECB; use GCM or another authenticated mode.

  • Reusing an IV/nonce with the same key in GCM — this is catastrophic, it can leak the authentication key and allow forgery. Always generate a fresh random IV per encryption operation.

  • Rolling your own crypto — implementing ciphers or protocols from scratch almost always introduces subtle timing, padding, or logic flaws. Use audited libraries (OpenSSL, libsodium, platform crypto APIs) and their high-level, misuse-resistant APIs.

  • Weak or short keys — RSA below 2048 bits, AES-128 in contexts requiring long-term secrecy, or keys derived from low-entropy sources (a short password hashed once) are all crackable with realistic compute budgets.

  • Confusing encryption with hashing for passwords — passwords should be hashed with a slow, salted KDF (bcrypt/scrypt/Argon2), never encrypted (which implies reversibility) or hashed with a fast general-purpose hash (which is brute-forceable at billions of guesses/second on GPUs).

  • No integrity check — encrypting without authentication (e.g. plain AES-CBC with no HMAC) lets an attacker flip ciphertext bits and produce predictable plaintext changes without detection. Always pair encryption with authentication, or use an AEAD mode like GCM that does both.

  • Hardcoding or committing keys — a key in source control is compromised the moment it's pushed, even if the commit is later removed; rotate immediately if this happens, don't just delete the file.

Keep your Encryption knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever