Cryptography Explained — Complete Beginner's Guide
Cryptography is the science of securing information by transforming it into unreadable formats using encryption, ensuring that only authorized parties can access the original data.
What You’ll Learn
By the end of this tutorial, you’ll understand symmetric vs asymmetric encryption, how hashing works, what digital signatures are, and you’ll write Python code to encrypt data and verify file integrity.
Why Cryptography Matters
Every time you visit an HTTPS website, send a WhatsApp message, or use a password manager, you’re relying on cryptography. At DodaTech, Doda Browser uses TLS encryption for all connections, and Durga Antivirus Pro uses cryptographic signatures to verify update authenticity. Understanding cryptography helps you build secure systems and protect user data.
Cryptography Learning Path
flowchart LR
A[Security Basics] --> B[Network Security]
B --> C[Web Security]
C --> D[Cryptography]
D --> E[Ethical Hacking]
E --> F[Pen Testing]
D --> G{You Are Here}
style G fill:#f90,color:#fff
What Is Cryptography? (The “Why” First)
Think of cryptography like a locked box with a secret message. You write a message, lock it in a box, and send it. Only the person with the right key can open the box and read the message. Cryptography does the same thing with digital data.
But here’s the challenge: how do you get the key to the other person without someone intercepting it? This is the fundamental problem cryptography solves — and it uses clever math to do it.
Symmetric Encryption — One Key to Rule Them All
Symmetric encryption uses the same key to encrypt and decrypt data. It’s like a lockbox with one key — you lock it, send it, and the receiver uses the same key to unlock it.
How It Works
Plain Text: "Hello" + Key: "secret123" → Encrypted: "hYx8s2Q="
Encrypted: "hYx8s2Q=" + Key: "secret123" → Decrypted: "Hello"Python Example with Fernet (Symmetric)
# symmetric_encryption.py
# Requires: pip install cryptography
from cryptography.fernet import Fernet
def generate_key():
"""Generate a random encryption key."""
key = Fernet.generate_key()
print(f"Key (save this securely!): {key.decode()}")
return key
def encrypt_message(key, message):
"""Encrypt a message using the key."""
cipher = Fernet(key)
encrypted = cipher.encrypt(message.encode())
print(f"Original: {message}")
print(f"Encrypted: {encrypted.decode()}")
return encrypted
def decrypt_message(key, encrypted_data):
"""Decrypt a message using the key."""
cipher = Fernet(key)
decrypted = cipher.decrypt(encrypted_data)
print(f"Decrypted: {decrypted.decode()}")
return decrypted.decode()
# Run it
if __name__ == "__main__":
key = generate_key()
msg = "This is a secret message!"
encrypted = encrypt_message(key, msg)
decrypted = decrypt_message(key, encrypted)
print(f"\nSuccess: {msg == decrypted}")Expected output:
Key (save this securely!): dGhpcyBpcyBhbiBleGFtcGxlIGtleQ==
Original: This is a secret message!
Encrypted: gAAAAABmZ29...
Decrypted: This is a secret message!
Success: TrueWhy this matters: The encrypted output looks like random garbage. Anyone who intercepts it cannot read the original message without the key. This is how HTTPS protects your web traffic.
The Key Distribution Problem
Here’s the catch with symmetric encryption: how do you share the key securely? If you email the key, an attacker could intercept it. If you mail it on a USB drive, it could get lost. This problem led to the invention of asymmetric encryption.
Asymmetric Encryption — Two Keys Are Better Than One
Asymmetric encryption (also called public-key cryptography) uses two different keys:
- Public key: Shared openly with everyone
- Private key: Kept secret, never shared
Think of it like a mailbox on a street corner. Anyone can drop mail in (using the public key), but only you have the key to open it (the private key).
How It Works
- Alice generates a public/private key pair
- Alice gives her public key to Bob
- Bob encrypts a message using Alice’s public key
- Only Alice’s private key can decrypt it
- Even Bob cannot decrypt his own message (he used the public key)
Python Example with RSA (Asymmetric)
# asymmetric_encryption.py
# Requires: pip install cryptography
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
def generate_keys():
"""Generate an RSA key pair."""
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
return private_key, public_key
def encrypt_with_public_key(public_key, message):
ciphertext = public_key.encrypt(
message.encode(),
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
return ciphertext
def decrypt_with_private_key(private_key, ciphertext):
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
return plaintext.decode()
if __name__ == "__main__":
print("Generating RSA key pair...")
private_key, public_key = generate_keys()
message = "This is a secret message!"
print(f"Original: {message}")
encrypted = encrypt_with_public_key(public_key, message)
print(f"Encrypted (hex): {encrypted.hex()[:50]}...")
decrypted = decrypt_with_private_key(private_key, encrypted)
print(f"Decrypted: {decrypted}")
# Save keys to files (practical use)
with open("private_key.pem", "wb") as f:
f.write(private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
))
print("Private key saved to private_key.pem")Expected output:
Generating RSA key pair...
Original: This is a secret message!
Encrypted (hex): 8f3a2b1c4d5e...
Decrypted: This is a secret message!
Private key saved to private_key.pemHashing — One-Way Encryption
Hashing is a one-way function: you can turn data into a hash, but you can’t turn a hash back into the original data. Think of it like a blender — you can turn fruit into a smoothie, but you can’t turn a smoothie back into whole fruit.
Hash Properties
- Deterministic: Same input always produces the same hash
- One-way: You cannot reverse a hash
- Collision-resistant: Two different inputs shouldn’t produce the same hash
- Avalanche effect: Changing one character changes the hash completely
Python Example with hashlib
# hashing_example.py
import hashlib
def hash_data(data, algorithm="sha256"):
"""Hash data using the specified algorithm."""
h = hashlib.new(algorithm)
h.update(data.encode())
return h.hexdigest()
# Test different inputs
print("=== SHA-256 Hashing Demo ===")
message1 = "Hello, World!"
message2 = "Hello, World?"
message3 = "hello, world!"
h1 = hash_data(message1)
h2 = hash_data(message2)
h3 = hash_data(message3)
print(f"Input 1: {message1}")
print(f"Hash 1: {h1}")
print(f"\nInput 2: {message2}")
print(f"Hash 2: {h2}")
print(f"\nInput 3: {message3}")
print(f"Hash 3: {h3}")
# Show avalanche effect
print(f"\n--- Avalanche Effect ---")
print(f"Input 1 and 2 differ by one character (! vs ?)")
diff_count = sum(1 for a, b in zip(h1, h2) if a != b)
print(f"Different hex characters: {diff_count}/64 ({diff_count*100/64:.1f}%)")
# Verify integrity
print(f"\n--- Integrity Check ---")
known_good_hash = h1
provided_data = "Hello, World!"
if hash_data(provided_data) == known_good_hash:
print("INTEGRITY VERIFIED: Data matches the known hash.")
else:
print("WARNING: Data has been modified!")Expected output:
=== SHA-256 Hashing Demo ===
Input 1: Hello, World!
Hash 1: dffd6021bb2d...
Input 2: Hello, World?
Hash 2: 8f7c8c9e5e1a...
Input 3: hello, world!
Hash 3: 4a1c2f5b7e9d...
--- Avalanche Effect ---
Input 1 and 2 differ by one character (! vs ?)
Different hex characters: 62/64 (96.9%)
--- Integrity Check ---
INTEGRITY VERIFIED: Data matches the known hash.Notice how a single character change (! vs ? and H vs h) produces completely different hashes. This is the avalanche effect — a fundamental property of cryptographic hash functions.
Common Hashing Algorithms
| Algorithm | Output Size | Security | Use Case |
|---|---|---|---|
| MD5 | 128 bits | Broken (collisions found) | Legacy systems only |
| SHA-1 | 160 bits | Weak (deprecated) | Avoid |
| SHA-256 | 256 bits | Strong | General purpose, file verification |
| SHA-3 | Variable | Very strong | Future-proof applications |
| bcrypt | Variable | Strong | Password hashing |
| Argon2 | Variable | Very strong | Modern password hashing |
Never use MD5 or SHA-1 for security purposes. They’re cryptographically broken. Always use SHA-256 or SHA-3 for general hashing, and bcrypt or Argon2 for passwords.
Digital Signatures — Proving Who Sent What
A digital signature proves that a message came from a specific person and hasn’t been tampered with. Think of it like a handwritten signature on a legal document — it verifies both the signer’s identity and the document’s integrity.
How Digital Signatures Work
- Alice writes a message
- Alice creates a hash of the message
- Alice encrypts the hash with her private key — this is the signature
- Alice sends the message + signature to Bob
- Bob decrypts the signature using Alice’s public key to get the hash
- Bob computes the hash of the received message
- If both hashes match, the message is authentic and untampered
# digital_signature.py
# Requires: pip install cryptography
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
def sign_message(private_key, message):
signature = private_key.sign(
message.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
return signature
def verify_signature(public_key, message, signature):
try:
public_key.verify(
signature,
message.encode(),
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH,
),
hashes.SHA256(),
)
return True
except Exception:
return False
# Example usage
private_key, public_key = generate_keys() # Using our previous function
message = "Transfer $1000 to Alice"
signature = sign_message(private_key, message)
print(f"Signature (hex): {signature.hex()[:40]}...")
# Test verification
print(f"Verification (original): {verify_signature(public_key, message, signature)}")
# Tampered message
tampered = "Transfer $9999 to Eve"
print(f"Verification (tampered): {verify_signature(public_key, tampered, signature)}")Expected output:
Signature (hex): a1b2c3d4e5f6...
Verification (original): True
Verification (tampered): FalseCommon Cryptography Mistakes
1. Rolling Your Own Crypto
Never write your own encryption algorithm. Professional cryptographers spend years designing and testing algorithms. Use well-audited libraries like cryptography (Python), libsodium, or OpenSSL.
2. Using ECB Mode (Electronic Codebook)
ECB mode encrypts identical plaintext blocks into identical ciphertext blocks. Patterns leak through. Always use CBC or GCM mode.
3. Hardcoding Encryption Keys
Keys should never be in source code. Use environment variables, key management services (AWS KMS, HashiCorp Vault), or secure hardware.
4. Using Broken Algorithms
MD5, SHA-1, DES, and RC4 are all broken. Stick to modern algorithms: SHA-256/3, AES-256-GCM, ChaCha20-Poly1305.
5. Not Authenticating Encrypted Data
Encryption ensures confidentiality but not integrity. An attacker can modify encrypted data without knowing the key. Always use authenticated encryption (GCM, ChaCha20-Poly1305).
6. Storing Passwords with Simple Hash
Never store passwords with just SHA-256. Use a slow, salted hashing algorithm like bcrypt (cost factor 10+) or Argon2id.
7. Forgetting to Rotate Keys
Keys should be rotated regularly. If a key is compromised, all data encrypted with it is at risk. Implement key rotation policies.
Common Mistakes Beginners Make
1. Skipping the Fundamentals
Many beginners jump straight to advanced topics without mastering the basics. Take time to understand the core concepts before moving on.
2. Not Practicing Enough
Reading tutorials without writing code leads to shallow understanding. Code along with every example and experiment on your own.
3. Ignoring Error Messages
Error messages tell you exactly what went wrong. Read them carefully — they usually point to the line and type of issue.
4. Copy-Pasting Without Understanding
It’s tempting to copy code from tutorials, but typing it yourself and understanding each line builds real skill.
5. Giving Up Too Early
Every developer hits frustrating bugs. Take breaks, ask for help, and remember that struggling is part of learning.
Practice Questions
1. What’s the difference between symmetric and asymmetric encryption?
Symmetric uses one key for both encryption and decryption. Asymmetric uses a public key for encryption and a private key for decryption. Symmetric is faster; asymmetric solves the key distribution problem.
2. Why can’t you “decrypt” a hash?
Hashing is a one-way function that discards information. There’s no mathematical operation to reverse it. This is different from encryption, which is designed to be reversible with the correct key.
3. What’s a digital signature?
A cryptographic technique that proves a message’s origin and integrity. The sender signs a hash of the message with their private key; anyone can verify it with the sender’s public key.
4. Why is AES-256-GCM better than AES-256-ECB?
GCM mode provides authenticated encryption (integrity + confidentiality) and uses a unique initialization vector. ECB mode leaks patterns because identical plaintext blocks produce identical ciphertext blocks.
5. Challenge: Write a script that hashes a file and compares it to a provided checksum.
import hashlib
import sys
def file_hash(filepath):
h = hashlib.sha256()
with open(filepath, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
h.update(chunk)
return h.hexdigest()
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python check_hash.py <file> <expected_sha256>")
sys.exit(1)
actual = file_hash(sys.argv[1])
expected = sys.argv[2].lower()
if actual == expected:
print("HASH MATCH — File integrity verified")
else:
print(f"HASH MISMATCH!\nExpected: {expected}\nActual: {actual}")Real-World Task: Password Hashing
# password_storage.py
# For Python 3.10+
import hashlib
import secrets
def hash_password(password: str) -> str:
"""Hash a password with a random salt using SHA-256."""
salt = secrets.token_hex(16)
hash_obj = hashlib.sha256()
hash_obj.update((password + salt).encode())
return f"{salt}${hash_obj.hexdigest()}"
def verify_password(password: str, stored: str) -> bool:
"""Verify a password against a stored hash."""
salt, expected_hash = stored.split("$")
hash_obj = hashlib.sha256()
hash_obj.update((password + salt).encode())
return hash_obj.hexdigest() == expected_hash
# Example
stored = hash_password("my_secure_password")
print(f"Stored hash (salt+hash): {stored}")
print(f"Verify correct: {verify_password('my_secure_password', stored)}")
print(f"Verify wrong: {verify_password('wrong_password', stored)}")Note: In production, use bcrypt or argon2-cffi instead of SHA-256 for passwords. These algorithms are designed to be slow, making brute-force attacks impractical. This is exactly how Durga Antivirus Pro stores its configuration passwords.
FAQ
Try It Yourself
Create a simple file encryption tool in Python:
# file_crypt.py
# Requires: pip install cryptography
from cryptography.fernet import Fernet
import sys
import os
def generate_key():
key = Fernet.generate_key()
with open("secret.key", "wb") as f:
f.write(key)
print("Key saved to secret.key — keep this safe!")
def load_key():
return open("secret.key", "rb").read()
def encrypt_file(filename):
key = load_key()
cipher = Fernet(key)
with open(filename, "rb") as f:
data = f.read()
encrypted = cipher.encrypt(data)
with open(filename + ".encrypted", "wb") as f:
f.write(encrypted)
print(f"Encrypted: {filename} → {filename}.encrypted")
def decrypt_file(filename):
key = load_key()
cipher = Fernet(key)
with open(filename, "rb") as f:
data = f.read()
decrypted = cipher.decrypt(data)
output_name = filename.replace(".encrypted", ".decrypted")
with open(output_name, "wb") as f:
f.write(decrypted)
print(f"Decrypted: {filename} → {output_name}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python file_crypt.py <command> [filename]")
print("Commands: genkey, encrypt, decrypt")
sys.exit(1)
cmd = sys.argv[1]
if cmd == "genkey":
generate_key()
elif cmd == "encrypt" and len(sys.argv) == 3:
encrypt_file(sys.argv[2])
elif cmd == "decrypt" and len(sys.argv) == 3:
decrypt_file(sys.argv[2])
else:
print("Invalid command or missing filename.")This tool uses the same principles that Doda Browser uses to encrypt your saved passwords and Durga Antivirus Pro uses to securely update its virus definitions.
What’s Next
What’s Next
Congratulations on completing this Cryptography tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro