DEV Community

Cover image for Python Security Guide: Password Hashing, Encryption, Digital Signatures, and Secure Coding Practices
Aarav Joshi
Aarav Joshi

Posted on

Python Security Guide: Password Hashing, Encryption, Digital Signatures, and Secure Coding Practices

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!

In my experience working with Python, I've come to appreciate how essential security is in today's digital world. Every application we build handles some form of data, and protecting that data isn't just an option—it's a necessity. I remember building my first web application and realizing how vulnerable user information could be without proper safeguards. That's when I dove into Python's security features, and I want to share what I've learned with you.

Let's start with password hashing. When users create accounts, their passwords need to be stored securely. You should never store passwords in plain text. Instead, we convert them into a scrambled version called a hash. This hash is unique to the password, and it's designed to be irreversible. That means even if someone gets access to your database, they can't figure out the original passwords.

Python's bcrypt library is fantastic for this. It adds something called salt, which is random data mixed with the password before hashing. This prevents attackers from using precomputed tables to crack passwords. The library also uses adaptive hashing, which means it can become slower over time to counter faster computers used in attacks.

Here's a simple example of how I use bcrypt. First, you install it with pip install bcrypt. Then, in your code, you can hash a password like this.

import bcrypt

# Imagine a user signs up with this password
password = b"my_secure_password"
# Generate a salt and hash the password
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password, salt)

# When the user logs in, you check the password
input_password = b"my_secure_password"
if bcrypt.checkpw(input_password, hashed_password):
    print("Login successful!")
else:
    print("Invalid password.")
Enter fullscreen mode Exit fullscreen mode

I always make sure to use bytes for passwords in bcrypt, as it expects byte strings. This small detail can save you from errors. In one project, I forgot this and spent hours debugging why the hashes didn't match.

Moving on to symmetric encryption. This is like having a single key that locks and unlocks a box. You use the same key to encrypt and decrypt data. It's great for situations where you need to protect data at rest, like files on a server, and you can securely share the key with authorized parties.

In Python, the cryptography library provides a easy-to-use tool called Fernet for symmetric encryption. Fernet uses AES under the hood, which is a strong encryption standard. I've used it to secure configuration files and sensitive user data.

Here's how you can implement it. First, install cryptography with pip install cryptography.

from cryptography.fernet import Fernet

# Generate a key – keep this safe!
key = Fernet.generate_key()
cipher_suite = Fernet(key)

# Encrypt a message
message = b"This is a secret message."
encrypted_message = cipher_suite.encrypt(message)
print(f"Encrypted: {encrypted_message}")

# Decrypt it later
decrypted_message = cipher_suite.decrypt(encrypted_message)
print(f"Decrypted: {decrypted_message.decode()}")
Enter fullscreen mode Exit fullscreen mode

I learned the hard way to always store the key securely. In a early project, I accidentally committed the key to a public repository, which could have exposed all encrypted data. Now, I use environment variables or dedicated key management services.

Now, let's talk about asymmetric encryption. This uses two keys: a public key and a private key. The public key can be shared with anyone, and it's used to encrypt data. Only the private key, which you keep secret, can decrypt it. This is perfect for secure communication where you don't want to share a secret key beforehand.

RSA is a common algorithm for this, and Python's cryptography library makes it straightforward. I've used this in systems where servers need to exchange data securely without prior key exchange.

Here's a basic setup.

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# Generate a private key
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

# Save the public key to share it
pem_public = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)
print(f"Public Key:\n{pem_public.decode()}")

# To encrypt with the public key
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

plaintext = b"Confidential data"
ciphertext = public_key.encrypt(
    plaintext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)
print(f"Encrypted: {ciphertext.hex()}")

# Decrypt with the private key
decrypted_text = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)
print(f"Decrypted: {decrypted_text.decode()}")
Enter fullscreen mode Exit fullscreen mode

I recommend using key sizes of at least 2048 bits for RSA to ensure security. Smaller keys might be faster but are vulnerable to attacks. In my experiments, generating keys can take a moment, so I do it once and store them safely.

Digital signatures are another powerful tool. They prove that a message came from a specific sender and hasn't been tampered with. It's like signing a document digitally. The sender uses their private key to sign the message, and anyone with the public key can verify it.

This is crucial for applications like software updates or legal documents where integrity and authenticity matter. I've implemented this in APIs to ensure that requests are from trusted sources.

Here's how you can create and verify a digital signature in Python.

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding

# Assume we have the private and public keys from earlier
message = b"Agreement terms"
signature = private_key.sign(
    message,
    padding.PKCS1v15(),
    hashes.SHA256()
)
print(f"Signature: {signature.hex()}")

# Verification
try:
    public_key.verify(
        signature,
        message,
        padding.PKCS1v15(),
        hashes.SHA256()
    )
    print("Signature is valid.")
except Exception:
    print("Signature verification failed.")
Enter fullscreen mode Exit fullscreen mode

I always use SHA-256 for hashing in signatures because it's currently secure. Older algorithms like SHA-1 have weaknesses. In one case, I saw a system compromised because it used weak hashing, so I stick to recommended standards.

Secure random number generation is often overlooked but vital. Many security features rely on randomness, like generating keys or session tokens. If the random numbers are predictable, attackers can guess them.

Python's secrets module is designed for this. It's better than the random module, which isn't suitable for security purposes. I use it whenever I need unpredictable values.

Here's a practical example.

import secrets

# Generate a secure random token for a session
session_token = secrets.token_bytes(32)
print(f"Session Token: {session_token.hex()}")

# Create a strong password
strong_password = secrets.token_urlsafe(16)
print(f"Generated Password: {strong_password}")

# For random numbers in a range, like OTP
otp = secrets.randbelow(1000000)
print(f"One-Time Password: {otp:06d}")
Enter fullscreen mode Exit fullscreen mode

I make it a habit to use secrets for all security-related randomness. In a past project, I used random and later found out it could be seeded predictably, which was a risk.

Implementing SSL/TLS is key for securing communication over networks. It encrypts data between clients and servers, preventing eavesdropping. Whenever you see HTTPS in a URL, it's using TLS.

Python's ssl module helps wrap sockets with TLS. I've used this to secure custom protocols and web servers.

Here's a simple client example.

import ssl
import socket

# Create a default SSL context
context = ssl.create_default_context()

# Connect to a secure server
try:
    with socket.create_connection(("www.example.com", 443)) as sock:
        with context.wrap_socket(sock, server_hostname="www.example.com") as secure_sock:
            secure_sock.sendall(b"GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n")
            response = secure_sock.recv(4096)
            print(f"Received: {response.decode()[:100]}...")
except Exception as e:
    print(f"Connection error: {e}")
Enter fullscreen mode Exit fullscreen mode

For servers, you'd need a certificate and key. I always use certificates from trusted authorities or set up my own CA for testing. Self-signed certificates can cause warnings in browsers, so for production, invest in proper certs.

Input validation is your first line of defense against attacks. Many security breaches start with malicious input, like SQL injection or cross-site scripting. By cleaning and validating user input, you can prevent these issues.

I use regular expressions and allowlists to filter input. It's better to define what is allowed rather than what is blocked, as attackers find new ways to bypass blocks.

Here's how I validate and sanitize inputs.

import re

def validate_username(username):
    # Only allow alphanumeric characters and underscores
    if re.match(r'^[a-zA-Z0-9_]+$', username):
        return True
    else:
        raise ValueError("Username can only contain letters, numbers, and underscores.")

def sanitize_html(input_text):
    # Remove potential HTML tags to prevent XSS
    clean_text = re.sub(r'<[^>]*>', '', input_text)
    return clean_text

# Example usage
user_input = "user_name123"
if validate_username(user_input):
    print("Username is valid.")

html_input = "<script>alert('XSS')</script>Hello"
safe_text = sanitize_html(html_input)
print(f"Sanitized: {safe_text}")
Enter fullscreen mode Exit fullscreen mode

In my applications, I validate input as early as possible, ideally at the entry point. I once missed validating a form field, and it led to a minor incident. Now, I have checklists for input handling.

Secure coding practices encompass habits that reduce vulnerabilities. This includes managing resources properly, avoiding hardcoded secrets, and using context managers for automatic cleanup.

Python's context managers, like with statements, help ensure files and connections are closed properly, preventing leaks. I also avoid storing secrets in code; instead, I use environment variables.

Here's an example with file handling.

import tempfile
import os

def process_sensitive_data(data):
    # Use a temporary file that auto-deletes
    with tempfile.NamedTemporaryFile(mode='w', delete=True, suffix='.tmp') as tmp_file:
        tmp_file.write(data)
        tmp_file.flush()  # Ensure data is written
        # Simulate processing
        with open(tmp_file.name, 'r') as f:
            content = f.read()
        print(f"Processed: {content}")
    # File is automatically deleted here

# Example with environment variables for secrets
import os
api_key = os.getenv('API_KEY')
if not api_key:
    raise EnvironmentError("API_KEY not set.")

sample_data = "Sensitive information here"
process_sensitive_data(sample_data)
Enter fullscreen mode Exit fullscreen mode

I always use context managers for files and networks. In the past, I left files open, which caused resource exhaustion on servers. Now, it's second nature.

Bringing it all together, these techniques form a robust security framework for Python applications. Start with the basics like input validation and password hashing, then layer in encryption and secure communication. Remember, security is not a one-time task but an ongoing process. Regularly update your libraries and review your code for new threats.

I hope this guide helps you build safer applications. If you have questions, feel free to reach out—I'm always learning from others in the community.

📘 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

Top comments (0)