Cybersecurity for Developers
ConceptBeginner 25 min· Foundations · 3 of 13

Cryptography basics: hashing, encryption and TLS

Learn the difference between hashing, encryption, signing and TLS, when to use each, and how to use them safely in code without inventing your own cryptography.

Cryptography is the maths that keeps data secret, unchanged and trustworthy. As a developer you will almost never invent cryptography (you must not). You will choose and use it: hash a password, encrypt a file, serve a site over HTTPS, sign a token. Most failures come from picking the wrong tool for the job, not from broken maths.

This lesson explains the four tools you need to tell apart: hashing, encryption, signing and TLS, with small Python examples you can run.

Four tools, four jobs

Which tool for which job

ToolQuestion it answersReversible?Uses a key?Typical use
HashIs this data the same as before?NoNoChecksums, password storage (with a special slow hash)
EncryptionCan I hide data so only the right person reads it?Yes, with the keyYesFiles, databases, messages
MAC / signatureDid the right party create this, unchanged?No (verify only)YesTokens, webhooks, signed updates
TLSCan two computers talk privately over the internet?n/aCertificates and session keysHTTPS

Hash, encrypt, encode

The most common mistake is mixing these up. Hashing is not encryption: you cannot get the original back from a hash. Encoding (such as Base64) is not encryption either: it is just a format change that anyone can undo without a key.

Hashing

A hash function takes any input and produces a fixed-size fingerprint. The same input always gives the same output. A tiny change gives a completely different output. You cannot work backwards from the fingerprint to the input.

SHA-256 is a standard, secure hash. MD5 and SHA-1 are broken for security use because attackers can create different inputs with the same hash (a collision). Use them at most for non-security purposes such as a quick duplicate check.

hash_demo.py
import hashlib

print(hashlib.sha256(b"masala-chai").hexdigest())
print(hashlib.sha256(b"masala-chaI").hexdigest())

Run it with python hash_demo.py. The two outputs are 64 hex characters each and look completely unrelated, even though the inputs differ by one letter.

Good uses: checking a downloaded file matches the publisher's checksum, detecting changes, building cache keys. A plain fast hash like SHA-256 is not how you store passwords, because it is designed to be fast and attackers can try billions of guesses per second. Passwords need a deliberately slow, salted algorithm. That is covered in *Passwords, authentication and MFA done right*.

Check a downloaded file
# Linux / macOS / Git Bash
sha256sum setup.zip

# Windows PowerShell
Get-FileHash setup.zip -Algorithm SHA256

Compare the printed hash with the one on the publisher's official page. If they differ, do not run the file.

Encryption: symmetric and asymmetric

Encryption turns readable data (plaintext) into unreadable data (ciphertext) using a key. Only someone with the right key can reverse it.

Symmetric encryption uses one shared key to lock and unlock. It is fast. AES is the standard algorithm. The hard part is sharing the key safely.

Asymmetric encryption uses a key pair: a public key anyone can have and a private key you keep secret. Data locked with the public key can only be unlocked with the private key. It is slower, so it is mostly used to exchange a symmetric key or to make signatures. RSA and elliptic-curve algorithms are common examples.

Analogy: symmetric is a house key you copy for a friend. Asymmetric is a letterbox: anyone can drop a letter in (public key), only you can open it (private key).

In code, do not call raw AES yourself. Use a high-level library that picks safe settings. In Python, the cryptography package offers Fernet, which does authenticated symmetric encryption (it also detects tampering). Install it with pip install cryptography.

encrypt_demo.py
from cryptography.fernet import Fernet, InvalidToken

key = Fernet.generate_key()      # store this safely, not in your code
f = Fernet(key)

token = f.encrypt(b"phone=9876543210")
print(token)                     # unreadable ciphertext
print(f.decrypt(token))          # b'phone=9876543210'

# Tampering is detected
bad = token[:-2] + b"AA"
try:
    f.decrypt(bad)
except InvalidToken:
    print("Tampered or wrong key")

Prints a long base64 token, then the original bytes, then Tampered or wrong key. The key must be kept secret: whoever has it can read everything. How to store it is in *Managing secrets and API keys*.

Encryption at rest protects stored data (a database, a disk, a backup). Encryption in transit protects data moving over a network. You usually want both. Remember the limit: encryption protects against someone who steals the disk or listens on the network. It does not help if your app itself has a bug that shows the data to the wrong user, because the app decrypts it for you.

Signing and MACs

Sometimes you do not need to hide data, only to prove it was not changed and came from someone who knows a secret. A MAC (message authentication code) such as HMAC does this with a shared secret. A digital signature does this with a private key, and anyone with the public key can verify it.

Real examples: a payment gateway signs a webhook so your server knows it really came from them; JWT tokens carry a signature so the server can detect edits to the claims.

hmac_demo.py
import hmac, hashlib, secrets

secret = secrets.token_bytes(32)
message = b"order=42&total=330"

sig = hmac.new(secret, message, hashlib.sha256).hexdigest()

def verify(secret, message, sig):
    expected = hmac.new(secret, message, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)   # constant-time compare

print(verify(secret, message, sig))                       # True
print(verify(secret, b"order=42&total=1", sig))           # False

Prints True then False. Changing even one character of the message makes verification fail. compare_digest avoids leaking information through comparison timing.

Randomness: use the right generator

Keys, tokens and reset codes must be unpredictable. Python's random module is predictable and is meant for games and simulations. For security use the secrets module (or os.urandom).

Secure random values
import secrets

print(secrets.token_urlsafe(32))   # good for API tokens and reset links
print(secrets.token_hex(16))       # good for random ids

Each run prints different values. Never use random.randint or the current time to make a token.

TLS and HTTPS

TLS (Transport Layer Security) is the protocol behind the padlock in your browser. HTTPS means HTTP carried inside TLS. It gives three things: encryption (eavesdroppers see scrambled bytes), integrity (changes in transit are detected) and authentication of the server (you are really talking to brew-and-bean.example, not an impostor).

The server proves its identity with a certificate issued by a certificate authority (CA) that your browser already trusts. In simplified form, a connection starts like this.

What happens when you open an HTTPS site

  1. 1The browser says hello and lists the TLS versions and ciphers it supports.
  2. 2The server replies with its certificate (which contains its public key and name, signed by a CA).
  3. 3The browser checks the certificate: is it signed by a trusted CA, is it for this domain, has it expired?
  4. 4Both sides use asymmetric cryptography to agree on a fresh shared key without sending it.
  5. 5From then on, the data is encrypted with fast symmetric encryption using that key.

Certificates are free and automatic from Let's Encrypt, and most hosting platforms (Vercel, Netlify, Render and others) set up HTTPS for you. There is rarely a good reason to serve a site over plain HTTP today. Use TLS 1.2 or newer; older versions and SSL are obsolete.

Inspect a certificate
# See the certificate a site presents (OpenSSL is included with Git Bash and Linux/macOS)
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

Prints the certificate subject (the domain), the issuer (the CA) and the start and end dates. An expired certificate is why browsers show a warning page.

Do not disable verification

Never turn off certificate checking to make an error go away, for example requests.get(url, verify=False) or NODE_TLS_REJECT_UNAUTHORIZED=0. It silently removes the server-authentication part of TLS. Fix the real cause (wrong date on your machine, missing CA, self-signed test certificate).

Rules of thumb for developers

Practical checklist

DoDo not
Use HTTPS everywhere, redirect HTTP to HTTPSSend passwords or tokens over plain HTTP
Use a vetted library (cryptography, libsodium, your framework)Invent your own algorithm or mix primitives yourself
Use secrets for tokens and keysUse random or timestamps
Use SHA-256 or better for integrityUse MD5 or SHA-1 for anything security related
Keep keys out of source code and rotate themHard-code keys or commit them to Git
Collect and keep less sensitive dataEncrypt everything and assume that is enough

India note: the DPDP Act expects organisations to protect personal data with reasonable security safeguards. Encrypting personal data such as phone numbers and using HTTPS are basic steps. This is a general pointer, not legal advice.

How this comes up in interviews

Expect: "What is the difference between hashing and encryption?" (one-way fingerprint versus reversible with a key). "Symmetric versus asymmetric?" "How does HTTPS work?" (walk through the steps above). "How do you store passwords?" (slow, salted hash, never encryption and never plain text). "What is a salt?" is covered in the authentication lesson.

Try it: Spot the mistake

Each line below has a crypto mistake. Say what is wrong and what you would use instead.

1. stored = hashlib.md5(password.encode()).hexdigest() 2. token = str(random.randint(100000, 999999)) for a password reset link 3. requests.get(url, verify=False) in production 4. A database column phone holding Base64 of the number, described as 'encrypted' 5. AES_KEY = "abc123" inside settings.py committed to GitHub

Crypto review

  1. Site is served over HTTPS and HTTP redirects to it.
  2. Passwords use a slow, salted hash (Argon2, bcrypt or similar).
  3. Tokens come from secrets or the framework's secure generator.
  4. No MD5 or SHA-1 for security purposes.
  5. Encryption keys are not in the repository.
  6. Certificate verification is never disabled.

Key takeaways

  1. Hashing is one-way, encryption is reversible with a key, signing proves origin and integrity, TLS protects data in transit.
  2. Encoding such as Base64 is not encryption.
  3. Use vetted libraries and never invent your own cryptography.
  4. Use secrets, not random, for anything security related.
  5. Passwords need slow, salted hashing, not plain SHA-256 or encryption.
  6. HTTPS gives encryption, integrity and server authentication; do not disable certificate verification.

Check yourself

Finishing the quiz marks the lesson as done.

1. A teammate says passwords are 'encrypted' with SHA-256 in the database. What is the best correction?

2. Which statement about Base64 is correct?

3. What does the certificate in an HTTPS connection prove?

4. Which is the right way to generate a password-reset token in Python?

5. Why do webhooks from a payment service include an HMAC signature?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free