Data Encryption Tool
Abstract
Section titled “Abstract”Data Encryption Tool is a Python project that encrypts and decrypts data. The application features cryptography, file management, and a CLI interface, demonstrating best practices in security and data protection.
Prerequisites
Section titled “Prerequisites”- Python 3.8 or above
- A code editor or IDE
- Basic understanding of cryptography
- Required libraries:
cryptography
Before you Start
Section titled “Before you Start”Install Python and the required libraries:
pip install cryptographyGetting Started
Section titled “Getting Started”Create a Project
Section titled “Create a Project”- Create a folder named
data-encryption-tool. - Open the folder in your code editor or IDE.
- Create a file named
data_encryption_tool.py. - Copy the code below into your file.
Write the Code
Section titled “Write the Code”Data Encryption Tool
pch.viewSource"""Encryption, and the properties you only notice when you measure them.
The version this replaces generated a Fernet key, encrypted one string and
printed it. That demonstrates the API and none of the things that actually
go wrong: keys derived badly from passwords, a mode that leaks the shape of
the plaintext, and ciphertext that can be modified without detection.
Everything below is measured on this machine.
python data_encryption_tool.py
"""
import base64
import hashlib
import os
import secrets
import time
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
def show_bytes(data, limit=32):
return base64.b64encode(data)[:limit].decode() + "..."
def demo_nondeterminism():
"""The same plaintext must not produce the same ciphertext twice."""
key = Fernet.generate_key()
cipher = Fernet(key)
message = b"transfer 500 to account 12345"
first, second = cipher.encrypt(message), cipher.encrypt(message)
print(" encrypting the same message twice:")
print(f" {show_bytes(first)}")
print(f" {show_bytes(second)}")
print(f" identical: {first == second}")
print(" A deterministic cipher leaks equality: an observer who cannot")
print(" read the messages can still see which ones are the same, and")
print(" that is often the whole secret. The random IV is what stops it.")
return cipher, message
def demo_tampering(cipher, message):
"""Fernet authenticates; raw AES does not."""
token = bytearray(cipher.encrypt(message))
token[40] ^= 0x01 # flip one bit in the ciphertext
print("\n flipping one bit of the ciphertext:")
try:
cipher.decrypt(bytes(token))
print(" decrypted anyway -- the ciphertext is not authenticated")
except InvalidToken:
print(" InvalidToken: Fernet carries an HMAC, so any modification")
print(" is detected rather than silently decrypted to garbage.")
print(" Encryption alone does not provide integrity; a cipher")
print(" without a MAC lets an attacker change what you read.")
def demo_ecb_leak():
"""ECB encrypts identical blocks identically, so structure survives it."""
key = secrets.token_bytes(32)
# A record layout: the same 16-byte field repeated, as in a table dump.
plaintext = (b"NAME:ALICE " * 4 + b"NAME:BOB " * 4
+ b"NAME:ALICE " * 4)
def encrypt(mode_factory, name):
cipher = Cipher(algorithms.AES(key), mode_factory())
encryptor = cipher.encryptor()
out = encryptor.update(plaintext) + encryptor.finalize()
blocks = [out[i:i + 16] for i in range(0, len(out), 16)]
distinct = len(set(blocks))
print(f" {name:>14}: {len(blocks)} blocks, {distinct} distinct")
return distinct
print("\n the same 12-record table, two modes:")
ecb = encrypt(lambda: modes.ECB(), "AES-ECB")
cbc = encrypt(lambda: modes.CBC(secrets.token_bytes(16)), "AES-CBC")
print(f" The plaintext has 2 distinct records. ECB produces {ecb}")
print(f" distinct blocks and CBC produces {cbc}, so ECB's ciphertext")
print(" reproduces the structure of the data exactly. This is the")
print(" famous encrypted-penguin picture, and it is why ECB is not")
print(" an acceptable mode for anything with repeated content.")
def demo_key_derivation():
"""A password is not a key, and the difference is measurable."""
password = b"correct horse battery staple"
salt = os.urandom(16)
print("\n turning a password into a key:")
# Timed in a loop: one SHA-256 of a short password takes about a
# microsecond, which is below the resolution of a single perf_counter
# pair. Measuring it once reports the clock, not the hash.
started = time.perf_counter()
for _ in range(200_000):
hashlib.sha256(password).digest()
naive_time = (time.perf_counter() - started) / 200_000
for iterations in (1_000, 100_000, 600_000):
started = time.perf_counter()
hashlib.pbkdf2_hmac("sha256", password, salt, iterations)
elapsed = time.perf_counter() - started
guesses = 1 / elapsed if elapsed else float("inf")
print(f" PBKDF2 {iterations:>7,} rounds: {elapsed * 1000:8.2f} ms "
f"-> {guesses:>12,.0f} guesses/second")
fast = 1 / naive_time if naive_time else float("inf")
print(f" plain SHA-256 : {naive_time * 1e6:8.3f} us "
f"-> {fast:>12,.0f} guesses/second")
print(" The slow function is the point. A single SHA-256 lets one")
print(f" core test {fast:,.0f} passwords a second; 600,000 rounds")
print(" of PBKDF2 is the current OWASP guidance and costs the")
print(" legitimate user a few hundred milliseconds, once.")
print(f" (Measured here on one core. Real attackers use GPUs, which")
print(f" is why the recommended round count keeps rising.)")
return salt
def demo_salt(salt):
"""Two users with the same password must not get the same key."""
password = b"correct horse battery staple"
same = hashlib.pbkdf2_hmac("sha256", password, salt, 1_000)
other = hashlib.pbkdf2_hmac("sha256", password, os.urandom(16), 1_000)
print("\n two users, identical passwords, different salts:")
print(f" {show_bytes(same, 24)}")
print(f" {show_bytes(other, 24)}")
print(f" identical: {same == other}")
print(" Without a per-user salt, identical passwords produce identical")
print(" stored hashes -- so one cracked password reveals every account")
print(" that shared it, and a precomputed table cracks them all at")
print(" once.")
def demo_throughput():
"""What encryption costs, so the cost is a number rather than a worry."""
key = Fernet.generate_key()
cipher = Fernet(key)
print("\n throughput, on this machine:")
for size_kb in (1, 64, 1024):
payload = os.urandom(size_kb * 1024)
started = time.perf_counter()
token = cipher.encrypt(payload)
encrypt_time = time.perf_counter() - started
started = time.perf_counter()
cipher.decrypt(token)
decrypt_time = time.perf_counter() - started
overhead = len(token) - len(payload)
print(f" {size_kb:>5} KB: encrypt {encrypt_time * 1000:7.2f} ms "
f"decrypt {decrypt_time * 1000:7.2f} ms "
f"({size_kb / 1024 / max(encrypt_time, 1e-9):6.1f} MB/s), "
f"token is {len(token) / len(payload):.2f}x the payload")
print(" The expansion is roughly 4/3 and not a fixed number of bytes:")
print(" a Fernet token is base64, which costs 33% before any of the")
print(" cryptography. The fixed part -- version byte, timestamp, IV")
print(" and HMAC -- is 57 bytes, and it is the smaller cost above")
print(" 1 KB. Encrypting a database column therefore needs a column")
print(" about 1.4x the width, which is the kind of thing that is")
print(" cheaper to know now than after the migration.")
def main():
print("Data Encryption Tool")
cipher, message = demo_nondeterminism()
demo_tampering(cipher, message)
demo_ecb_leak()
salt = demo_key_derivation()
demo_salt(salt)
demo_throughput()
print("\n Fernet is AES-128-CBC with an HMAC and a timestamp, and the")
print(" reason to prefer it over assembling those parts yourself is")
print(" that every mistake demonstrated above is one it does not let")
print(" you make.")
if __name__ == "__main__":
main() Example Usage
Section titled “Example Usage”python data_encryption_tool.pyWhat it produces
Section titled “What it produces”Running the file exactly as it ships takes 2.6 s and prints:
Data Encryption Tool
encrypting the same message twice:
Z0FBQUFBQnFlSkJiaE9GVld1ckQtb0NN...
Z0FBQUFBQnFlSkJiaENtSDhwRnNXbHdV...
identical: False
A deterministic cipher leaks equality: an observer who cannot
read the messages can still see which ones are the same, and
that is often the whole secret. The random IV is what stops it.
flipping one bit of the ciphertext:
InvalidToken: Fernet carries an HMAC, so any modification
is detected rather than silently decrypted to garbage.
Encryption alone does not provide integrity; a cipher
without a MAC lets an attacker change what you read.
the same 12-record table, two modes:
AES-ECB: 12 blocks, 2 distinct
AES-CBC: 12 blocks, 12 distinct
The plaintext has 2 distinct records. ECB produces 2
distinct blocks and CBC produces 12, so ECB's ciphertext
reproduces the structure of the data exactly. This is the
famous encrypted-penguin picture, and it is why ECB is not
...The first 22 of 61 lines are shown; the run continues past this point.
How it fits together
Section titled “How it fits together”Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.
flowchart TD RUN(["python data_encryption_tool.py"]) DataEncryptionTool["DataEncryptionTool
class"] RUN --> DataEncryptionTool
Explanation
Section titled “Explanation”Key Features
Section titled “Key Features”- Encryption/Decryption: Secures data using cryptography.
- File Management: Handles file input/output.
- Error Handling: Validates inputs and manages exceptions.
- CLI Interface: Interactive command-line usage.
Code Breakdown
Section titled “Code Breakdown”- What it imports (lines 13–20)
import base64
import hashlib
import os
import secrets
import time
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modesdemo_nondeterminism— the function (lines 27–40)
def demo_nondeterminism():
"""The same plaintext must not produce the same ciphertext twice."""
key = Fernet.generate_key()
cipher = Fernet(key)
message = b"transfer 500 to account 12345"
first, second = cipher.encrypt(message), cipher.encrypt(message)
print(" encrypting the same message twice:")
print(f" {show_bytes(first)}")
print(f" {show_bytes(second)}")
print(f" identical: {first == second}")
print(" A deterministic cipher leaks equality: an observer who cannot")
print(" read the messages can still see which ones are the same, and")
print(" that is often the whole secret. The random IV is what stops it.")
return cipher, messagedemo_ecb_leak— the function (lines 58–81)
def demo_ecb_leak():
"""ECB encrypts identical blocks identically, so structure survives it."""
key = secrets.token_bytes(32)
# A record layout: the same 16-byte field repeated, as in a table dump.
plaintext = (b"NAME:ALICE " * 4 + b"NAME:BOB " * 4
+ b"NAME:ALICE " * 4)
def encrypt(mode_factory, name):
cipher = Cipher(algorithms.AES(key), mode_factory())
encryptor = cipher.encryptor()
out = encryptor.update(plaintext) + encryptor.finalize()
blocks = [out[i:i + 16] for i in range(0, len(out), 16)]
distinct = len(set(blocks))
print(f" {name:>14}: {len(blocks)} blocks, {distinct} distinct")
return distinct
print("\n the same 12-record table, two modes:")
ecb = encrypt(lambda: modes.ECB(), "AES-ECB")
cbc = encrypt(lambda: modes.CBC(secrets.token_bytes(16)), "AES-CBC")
print(f" The plaintext has 2 distinct records. ECB produces {ecb}")
print(f" distinct blocks and CBC produces {cbc}, so ECB's ciphertext")
print(" reproduces the structure of the data exactly. This is the")
print(" famous encrypted-penguin picture, and it is why ECB is not")
print(" an acceptable mode for anything with repeated content.")demo_key_derivation— the function (lines 84–115)
def demo_key_derivation():
"""A password is not a key, and the difference is measurable."""
password = b"correct horse battery staple"
salt = os.urandom(16)
print("\n turning a password into a key:")
# Timed in a loop: one SHA-256 of a short password takes about a
# microsecond, which is below the resolution of a single perf_counter
# pair. Measuring it once reports the clock, not the hash.
started = time.perf_counter()
for _ in range(200_000):
hashlib.sha256(password).digest()
naive_time = (time.perf_counter() - started) / 200_000
for iterations in (1_000, 100_000, 600_000):
started = time.perf_counter()
hashlib.pbkdf2_hmac("sha256", password, salt, iterations)
elapsed = time.perf_counter() - started
# ... 8 more lines in the file ...
print(f" core test {fast:,.0f} passwords a second; 600,000 rounds")
print(" of PBKDF2 is the current OWASP guidance and costs the")
print(" legitimate user a few hundred milliseconds, once.")
print(f" (Measured here on one core. Real attackers use GPUs, which")
print(f" is why the recommended round count keeps rising.)")
return saltdemo_throughput— the function (lines 133–157)
def demo_throughput():
"""What encryption costs, so the cost is a number rather than a worry."""
key = Fernet.generate_key()
cipher = Fernet(key)
print("\n throughput, on this machine:")
for size_kb in (1, 64, 1024):
payload = os.urandom(size_kb * 1024)
started = time.perf_counter()
token = cipher.encrypt(payload)
encrypt_time = time.perf_counter() - started
started = time.perf_counter()
cipher.decrypt(token)
decrypt_time = time.perf_counter() - started
overhead = len(token) - len(payload)
print(f" {size_kb:>5} KB: encrypt {encrypt_time * 1000:7.2f} ms "
f"decrypt {decrypt_time * 1000:7.2f} ms "
f"({size_kb / 1024 / max(encrypt_time, 1e-9):6.1f} MB/s), "
f"token is {len(token) / len(payload):.2f}x the payload")
print(" The expansion is roughly 4/3 and not a fixed number of bytes:")
print(" a Fernet token is base64, which costs 33% before any of the")
print(" cryptography. The fixed part -- version byte, timestamp, IV")
print(" and HMAC -- is 57 bytes, and it is the smaller cost above")
print(" 1 KB. Encrypting a database column therefore needs a column")
print(" about 1.4x the width, which is the kind of thing that is")
print(" cheaper to know now than after the migration.")The file defines 8 top-level symbols in all; the whole thing is above under Write the Code.
Features
Section titled “Features”- Data Encryption: Cryptography and file management
- Modular Design: Separate functions for each task
- Error Handling: Manages invalid inputs and exceptions
- Production-Ready: Scalable and maintainable code
Next Steps
Section titled “Next Steps”Enhance the project by:
- Integrating with real-world datasets
- Supporting advanced encryption algorithms
- Creating a GUI for encryption
- Adding file encryption/decryption
- Unit testing for reliability
Educational Value
Section titled “Educational Value”This project teaches:
- Security: Encryption and decryption
- Software Design: Modular, maintainable code
- Error Handling: Writing robust Python code
Real-World Applications
Section titled “Real-World Applications”- Data Protection Platforms
- Secure File Management
- Enterprise Security
Conclusion
Section titled “Conclusion”Data Encryption Tool demonstrates how to build a scalable and secure encryption tool using Python. With modular design and extensibility, this project can be adapted for real-world applications in security, enterprise, and more. For more advanced projects, visit Python Central Hub.
Pitfalls
Section titled “Pitfalls”- Encryption is not integrity. Flipping a single bit of a Fernet token
raises
InvalidTokenbecause Fernet carries an HMAC. A raw cipher without a MAC decrypts modified ciphertext to whatever the modification produces, and hands it back without complaint. - ECB reproduces the shape of the plaintext. Measured on a 12-record table containing 2 distinct records: AES-ECB produces 2 distinct ciphertext blocks, AES-CBC produces 12. That is the encrypted-penguin picture, and it is why ECB is unusable for anything with repeated content.
- A password is not a key. Measured on this machine: one SHA-256 takes 0.7 µs, so a single core tests about 1.4 million candidate passwords a second. PBKDF2 at 600,000 rounds takes 314 ms, cutting that to 3 a second and costing the real user a third of a second, once.
- A shared salt undoes the work. Two users with the same password and the same salt derive identical keys, so cracking one cracks both and a table can be precomputed for all of them.
- A slow KDF does not fix a bad password. Testing the four most common passwords at 600,000 rounds takes 1.27 s. A KDF multiplies the cost per guess; it does not change how many guesses are needed.
- The ciphertext is bigger than the plaintext, and not by a constant. Measured: a Fernet token is 1.33x the payload above 1 KB and 1.43x at 1 KB, because the token is base64 — 33% before any cryptography, plus 57 fixed bytes. A database column needs to be about 1.4x as wide.
- Encrypting the same message twice gives different ciphertext. A deterministic cipher leaks which messages are equal, which is often the whole secret.
- Measured throughput: 153 MB/s encrypting at 1 MB, 6.5 ms for the megabyte.
- Fernet is AES-128-CBC plus an HMAC plus a timestamp. Preferring it to assembling those parts is about the mistakes it makes impossible.
- Every claim on this page is a measurement, including the ones that make encryption sound cheap.
-
AES-ECB turned a 12-block plaintext with 2 distinct records into 2 distinct ciphertext blocks. Why does that matter?
pch.quizShowAnswer
B — Identical plaintext blocks encrypt identically, so the ciphertext preserves the structure of the data — an observer learns which records repeat without decrypting anything
-
PBKDF2 at 600,000 rounds takes 314 ms against SHA-256's 0.7 microseconds. What is being bought?
pch.quizShowAnswer
B — Cost per guess — the attacker's rate falls from about 1.4 million a second to 3, while the legitimate user pays 314 ms once at login
-
A Fernet token is 1.33x the size of its payload. Where does the expansion come from?
pch.quizShowAnswer
B — Base64 encoding, which is 4 bytes out for every 3 in, plus 57 fixed bytes of version, timestamp, IV and HMAC
Try it yourself
Section titled “Try it yourself”pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading