As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
Let’s talk about making things secret and safe with code. I use Python for this all the time. It feels like having a well-stocked toolbox for building vaults, sealed envelopes, and unbreakable locks, but with text and numbers. I’ll show you some of the most useful tools I keep in my kit.
We’ll start with a common problem: you have a piece of information, like a message or a file, that you need to keep hidden. Yo…
As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
Let’s talk about making things secret and safe with code. I use Python for this all the time. It feels like having a well-stocked toolbox for building vaults, sealed envelopes, and unbreakable locks, but with text and numbers. I’ll show you some of the most useful tools I keep in my kit.
We’ll start with a common problem: you have a piece of information, like a message or a file, that you need to keep hidden. You want to lock it up so only someone with the right key can open it. This is called symmetric encryption. Think of it like a single key that can both lock and unlock a box.
In Python, a great starting point is something called Fernet. It handles a lot of the tricky details for you. It not only locks the data but also seals it, so you can tell if someone tried to tamper with it. Here’s how it works in practice.
from cryptography.fernet import Fernet
# First, you need a key. Fernet can generate a strong one for you.
key = Fernet.generate_key()
cipher = Fernet(key)
# Your secret message. Notice the 'b' before the string; this means we're working with bytes, which is what these functions need.
my_secret = b"The secret recipe is two cups of sugar."
locked_message = cipher.encrypt(my_secret)
# The output is a jumble of bytes. It's unreadable on purpose.
print(f"Locked up: {locked_message[:30]}...")
# To get the secret back, you use the same key.
unlocked_message = cipher.decrypt(locked_message)
print(f"Back to normal: {unlocked_message.decode()}")
What happens if someone messes with the locked message? The seal breaks. Fernet is built to detect that.
# Let's simulate tampering by changing the end of the encrypted data.
tampered_message = locked_message[:-5] + b"XXXXX"
try:
cipher.decrypt(tampered_message)
except Exception as e:
print(f"Caught! The message was altered. Error: {type(e).__name__}")
That’s useful for one key that you can share safely. But what if you’ve never met the person you want to communicate with? How do you get a shared secret to them without someone else intercepting it? This is where asymmetric encryption comes in. It uses a pair of keys: one public, one private.
Imagine a mailbox on the street. The address (the public key) is known to everyone. Anyone can drop a letter in the slot. But only the person with the key to the mailbox (the private key) can open it and read the letters. You can give your public key to anyone, even over an insecure channel.
A common system for this is called RSA. Let’s see how to create this key pair.
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
# Generate your private key. This is the one you keep absolutely secure.
private_key = rsa.generate_private_key(
public_exponent=65537, # This is a standard, efficient value.
key_size=2048 # The size of the key. 2048 bits is a good baseline.
)
# The public key is mathematically derived from the private one.
public_key = private_key.public_key()
# Now, someone can use your public key to lock a message for you.
message_for_me = b"Meet me at the usual spot at noon."
ciphertext = public_key.encrypt(
message_for_me,
padding.OAEP( # This padding scheme makes the encryption more secure.
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
# Only I can unlock it with my private key.
original_message = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
print(f"Secret message received: {original_message.decode()}")
Asymmetric encryption is powerful, but it’s also slow. It’s often used to do something very important: create a digital signature. A signature proves that a message came from you and that it hasn’t been changed. It’s like signing a document with a unique, unforgeable pen.
Instead of RSA, we often use a method called ECDSA for signatures because it’s faster and uses smaller keys. Here’s how you sign a document.
from cryptography.hazmat.primitives.asymmetric import ec
# Generate a special key pair for signing.
signing_private_key = ec.generate_private_key(ec.SECP384R1())
signing_public_key = signing_private_key.public_key()
document = b"Official Statement: The quarterly results are positive."
# You sign with your private key.
signature = signing_private_key.sign(
document,
ec.ECDSA(hashes.SHA256())
)
# Anyone with your public key can verify the signature.
try:
signing_public_key.verify(
signature,
document,
ec.ECDSA(hashes.SHA256())
)
print("The signature is valid. This document is authentic.")
except Exception:
print("Warning! The signature is invalid. Do not trust this document.")
Now, let’s talk about passwords. You should never, ever store a user’s actual password. Instead, you store a "hash" of it. A hash is a one-way street. You can turn a password into a hash, but you can’t turn the hash back into the password. When a user logs in, you hash the password they type and compare it to the stored hash.
But simple hashes aren’t enough anymore. Computers are too fast. We need slow, computationally expensive hashes designed specifically for passwords, like bcrypt or Argon2. They are deliberately slow to stop attackers who try billions of guesses.
import bcrypt
import time
# When a user creates a password, you hash it like this.
password = "MyStr0ng!P@sswrd"
# The 'gensalt()' function adds random data (a "salt") to make each hash unique.
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
print(f"Hashed password to store in DB: {hashed_password}")
# Later, during login:
login_attempt = "MyStr0ng!P@sswrd"
if bcrypt.checkpw(login_attempt.encode(), hashed_password):
print("Login successful.")
else:
print("Wrong password.")
# Let's see why slowness is a feature, not a bug.
start = time.time()
bcrypt.hashpw("anotherPassword".encode(), bcrypt.gensalt(rounds=14)) # More rounds = slower
end = time.time()
print(f"Hashing took {end-start:.3f} seconds. That's good for security!")
We have symmetric encryption (one key) and asymmetric encryption (a key pair). How do we get a shared secret key for symmetric encryption if we’ve only ever exchanged public keys? This classic problem is solved by the Diffie-Hellman key exchange. It allows two people to create a shared secret over a public channel, without ever sending the secret itself.
It’s like two people publicly mixing their own secret colors with a common public color. They end up with the same final color, but an observer can’t figure out what it is.
from cryptography.hazmat.primitives.asymmetric import dh
# Both parties agree on some public parameters first.
parameters = dh.generate_parameters(generator=2, key_size=2048)
# Person A (let's call her Alice) creates her key pair.
alice_private = parameters.generate_private_key()
alice_public = alice_private.public_key()
# Person B (Bob) does the same.
bob_private = parameters.generate_private_key()
bob_public = bob_private.public_key()
# The magic happens here. They exchange public keys.
# Alice uses her private key and Bob's public key.
alice_shared_secret = alice_private.exchange(bob_public)
# Bob uses his private key and Alice's public key.
bob_shared_secret = bob_private.exchange(alice_public)
# They now have identical secrets, without ever transmitting them!
print(f"Shared secret established: {alice_shared_secret == bob_shared_secret}")
This shared secret can then be used to generate keys for symmetric encryption. We’ve solved the key exchange problem. Now, when we use that key for symmetric encryption, we should use a mode that also guarantees integrity. This is called authenticated encryption.
A popular choice is AES in GCM mode. It encrypts the data and also produces an authentication tag. If the data is altered during transmission, the tag won’t match, and the decryption will fail. It does both jobs at once.
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import secrets
# Generate a random 256-bit key for AES-GCM.
key = AESGCM.generate_key(bit_length=256)
# The data we want to protect.
plaintext = b"Credit Card Number: 4111-1111-1111-1111"
# We also have associated data we want to authenticate but not encrypt, like a header.
associated_data = b"transaction_id:TX1001"
# Create a cipher object.
aesgcm = AESGCM(key)
# We need a "nonce" - a number used once. It must be random and never reused with the same key.
nonce = secrets.token_bytes(12)
# Encrypt and authenticate.
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# To send this, you'd send the nonce and the ciphertext.
# The recipient needs the same key, nonce, ciphertext, and associated_data.
decrypted_text = aesgcm.decrypt(nonce, ciphertext, associated_data)
print(f"Decrypted: {decrypted_text}")
# What if the associated data is wrong?
try:
aesgcm.decrypt(nonce, ciphertext, b"wrong_header")
except Exception as e:
print(f"Rejected! The authenticated data was tampered with.")
All these techniques rely on good randomness. Keys, nonces, salts—they all need to be unpredictable. You should never use Python’s basic random module for security. Use the secrets module instead. It’s designed for this.
import secrets
import string
# Generate a secure random key for an API.
api_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
print(f"Secure API Key: {api_key}")
# Generate a random token for a session.
session_token = secrets.token_urlsafe(32)
print(f"Session Token: {session_token}")
# Generate a one-time password.
otp = ''.join(secrets.choice(string.digits) for _ in range(6))
print(f"Your OTP is: {otp}")
There’s another subtle attack to watch for: timing attacks. If your code checks a password character by character and stops at the first wrong one, an attacker can time how long the check takes to guess the password slowly. We need constant-time comparison.
from cryptography.hazmat.primitives import constant_time
secret_api_key = b"Real-Secret-Key-12345"
user_provided_key = b"Real-Secret-Key-12345"
fake_key = b"Real-Secret-Key-12344"
# This function always takes the same amount of time, regardless of input.
def safe_compare(a, b):
return constant_time.bytes_eq(a, b)
print(f"Correct key matches: {safe_compare(secret_api_key, user_provided_key)}")
print(f"Wrong key matches: {safe_compare(secret_api_key, fake_key)}")
Finally, how do you know a public key really belongs to the person or website you think it does? This is the problem of trust. In the physical world, we use IDs and notaries. In the digital world, we use certificates.
An X.509 certificate is a digital document that says, "I, a trusted Certificate Authority (CA), certify that this public key belongs to example.com." Your browser and operating system come with a list of trusted CAs. It’s a chain of trust.
You can create your own small chain for internal systems. Here’s a simplified look at how certificates are built and signed.
from cryptography import x509
from cryptography.x509.oid import NameOID
from datetime import datetime, timedelta, timezone
# First, we create a root Certificate Authority (CA). This is the ultimate trust anchor.
ca_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
# We define who the CA is.
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "My Company Internal CA"),
x509.NameAttribute(NameOID.COMMON_NAME, "myca.internal.com"),
])
# Now we build the CA's own self-signed certificate.
ca_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
ca_private_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.now(timezone.utc)
).not_valid_after(
datetime.now(timezone.utc) + timedelta(days=3650) # CA certs last long
).add_extension(
x509.BasicConstraints(ca=True, path_length=1), # This marks it as a CA.
critical=True
).sign(ca_private_key, hashes.SHA256())
print(f"CA Certificate created. Issuer: {ca_cert.issuer}")
# Now, let's say a web server needs a certificate.
server_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
# It creates a Certificate Signing Request (CSR).
csr = x509.CertificateSigningRequestBuilder().subject_name(
x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, "myserver.internal.com"),
])
).sign(server_private_key, hashes.SHA256())
# The CA signs the CSR, producing the server's certificate.
server_cert = x509.CertificateBuilder().subject_name(
csr.subject
).issuer_name(
ca_cert.subject # The issuer is now our CA.
).public_key(
csr.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.now(timezone.utc)
).not_valid_after(
datetime.now(timezone.utc) + timedelta(days=365)
).add_extension(
x509.SubjectAlternativeName([x509.DNSName("myserver.internal.com")]),
critical=False
).sign(ca_private_key, hashes.SHA256()) # Signed with the CA's private key.
print(f"Server Certificate issued. It was signed by: {server_cert.issuer}")
When a client connects to myserver.internal.com, the server presents server_cert. The client checks that it is signed by the ca_cert that it already trusts. This establishes a trusted connection, which is the basis for HTTPS.
Putting it all together, you might start a secure connection with a protocol like TLS, which uses certificates for trust, a method like Diffie-Hellman to establish a session key, and then AES-GCM to encrypt all the data that flows between client and server. Each technique we’ve discussed is a building block in that process.
My advice is to start simple. Use high-level libraries like cryptography.fernet for data at rest. Understand the concepts behind the other techniques so you can configure web frameworks and databases correctly. Security is a process of layering these tools thoughtfully, always keeping your keys safe, and staying updated, because the field never stands still. This isn’t about making something perfectly unhackable—that’s nearly impossible. It’s about putting up enough well-understood barriers that an attacker will move on to an easier target.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva