Cryptography mistakes in application code are rarely about broken algorithms. They are about using the right tool for the wrong job: Base64 used as if it were encryption, a fast hash used for passwords, encryption used where a signature was needed. Each tool answers a different question, and once you know the question, the choice is usually obvious.

In part 4 several defences relied on TLS, signatures and hashes. This part explains what those building blocks are, how they differ and how TLS combines them. The next part shows a real bug that came from confusing two of them.

Five tools, five questions

Tool Question it answers Needs a key? Reversible? Example
Encoding How do I represent this data in another format? No Yes, by anyone Base64, URL encoding, hex
Encryption How do I keep this secret from people without the key? Yes Yes, with the key AES-GCM, ChaCha20-Poly1305
Hashing How do I get a fixed fingerprint of this data? No No SHA-256, SHA-3
MAC (e.g. HMAC) Was this data created by someone with the shared key, unchanged? Yes (shared) No HMAC-SHA256
Digital signature Was this data created by the holder of a private key, unchanged? Yes (key pair) No Ed25519, ECDSA, RSA-PSS

The two most important rules fall straight out of the table: encoding is never security, and hashing is not encryption because nothing can be decrypted.

Encoding: a format, not a secret

Encoding converts data into another representation so it can travel through a system that expects a certain format. Base64 turns bytes into printable characters; URL encoding escapes characters that have meaning in a URL. Anyone can reverse it, without a key.

echo -n 'admin:s3cret' | base64
# YWRtaW46czNjcmV0
echo 'YWRtaW46czNjcmV0' | base64 -d
# admin:s3cret

That example is exactly what HTTP Basic authentication sends. It is safe only because TLS encrypts the connection underneath; the Base64 part adds no protection at all. If you see a value in a config file or database that "looks scrambled", decode it before assuming it is protected.

Encryption: secret with a key

Encryption transforms readable data (plaintext) into unreadable data (ciphertext) using a key, and only someone with the right key can reverse it. There are two families.

Symmetric encryption

The same key encrypts and decrypts. It is fast, and it is what protects bulk data: disks, database columns, backups and the data inside a TLS connection. The modern recommendation is an authenticated encryption mode such as AES-256-GCM or ChaCha20-Poly1305, which both hides the data and detects any tampering with the ciphertext.

# Python, using the "cryptography" package
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = AESGCM.generate_key(bit_length=256)   # store in a key manager, not in code
aead = AESGCM(key)
nonce = os.urandom(12)                      # unique per message, never reused with the same key
ciphertext = aead.encrypt(nonce, b"account 4417 balance 120.50", b"record:4417")
plaintext = aead.decrypt(nonce, ciphertext, b"record:4417")

The hard part of symmetric encryption is not the algorithm but the key: where it is stored, who can use it, how it is rotated. That is why cloud key management services exist.

Asymmetric encryption

Asymmetric cryptography uses a key pair: a public key that can be shared with anyone and a private key that never leaves its owner. Data encrypted with the public key can only be decrypted with the private key. It is much slower than symmetric encryption, so in practice it is used to agree on or protect a symmetric key, not to encrypt large data directly.

Hashing: a one-way fingerprint

A cryptographic hash function turns any input into a fixed-size output. The same input always gives the same output, a tiny change gives a completely different output, and you cannot get the input back from the output.

echo -n 'release-2.4.1.tar.gz contents' | sha256sum

Hashes are used to check file integrity, to deduplicate data and as building blocks for other tools. On their own they prove nothing about who created the data, because anyone can compute a hash. An attacker who replaces a download can also replace the published hash if both live on the same server.

Passwords need a special kind of hash

Fast hashes such as SHA-256 are the wrong tool for passwords. They are designed to be fast, so an attacker with a stolen database can try billions of guesses per second. Password storage needs a deliberately slow, salted function: Argon2id, scrypt or bcrypt. A salt is a random value stored with each hash so that identical passwords produce different hashes and precomputed tables are useless.

MACs and signatures: who sent it, and was it changed?

A message authentication code combines a hash function with a shared secret key. HMAC-SHA256 is the most common. Only someone with the key can produce a valid tag, so a correct tag proves the message came from a key holder and was not altered. Webhook signatures from payment providers are usually HMACs.

import hashlib, hmac

def verify_webhook(secret: bytes, body: bytes, received_tag: str) -> bool:
    expected = hmac.new(secret, body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, received_tag)   # constant-time comparison

A digital signature does the same with a key pair: the private key signs, and anyone with the public key can verify. Because verification does not require the secret, signatures also support non-repudiation. Software releases, container images and JWTs signed with RS256 or EdDSA use signatures.

How TLS puts it all together

A TLS 1.3 connection, simplified, uses almost every tool in this post:

  1. Key exchange: client and server use an asymmetric method (usually elliptic-curve Diffie-Hellman) to agree on a shared secret without ever sending it.
  2. Authentication: the server proves its identity with a certificate, which is its public key signed by a certificate authority. The client verifies that signature chain and that the name matches.
  3. Key derivation: both sides derive symmetric keys from the shared secret using an HMAC-based function.
  4. Data transfer: all application data is protected with symmetric authenticated encryption, such as AES-GCM.

Asymmetric cryptography solves the "how do we share a key with a stranger" problem, signatures solve "is this really the server", and symmetric encryption does the heavy lifting.

Common mistakes

Mistake Why it hurts Do this instead
Base64 "encrypting" secrets in config Anyone can decode it Use a secrets manager or real encryption
SHA-256 or MD5 for passwords Billions of guesses per second Argon2id, scrypt or bcrypt
AES in ECB or unauthenticated CBC mode Leaks patterns, allows tampering AES-GCM or ChaCha20-Poly1305
Reusing a nonce with the same key Can expose plaintext in GCM Random 96-bit nonce per message, or a counter
Comparing MACs with == Timing differences leak information Constant-time comparison function
Writing your own crypto Subtle flaws are almost guaranteed Use a well-reviewed library's high-level API

Key takeaways

  • Encoding changes format and protects nothing; encryption hides data from anyone without the key.
  • Hashes are one-way fingerprints; passwords need slow, salted password-hashing functions.
  • MACs prove a shared-key holder sent unchanged data; signatures prove it with a public key and support non-repudiation.
  • Symmetric encryption protects bulk data, asymmetric cryptography exchanges keys and proves identity.
  • TLS combines key exchange, signatures, key derivation and authenticated encryption in one handshake.

References