For a long time I’ve been bouncing between Python tutorials — following along, copying code, feeling like I understood it, then realizing days later that I couldn’t write any of it from scratch. The cure was obvious: stop reading and start building. So I built a small password manager. Nothing fancy: a CLI tool that generates strong passwords, scores their strength, encrypts them with Fernet, and stores them locally. So I built an encrypted password manager in Python — a small CLI tool that generates strong passwords, scores their strength, encrypts them with Fernet, and stores them locally.
This post walks through how it works, what I learned about symmetric cryptography along the way, and — importantly — why this is a learning project, not something you should actually use to store your real passwords.
The full source code is on GitHub: password-tool.
How the Encrypted Password Manager Works
The tool has four main features, all accessed through a simple interactive CLI menu:
- Password Strength Checker — Scores a given password from 0 to 100 based on length and character variety (lowercase, uppercase, digits, symbols), and returns a human-readable label: Weak, Medium, Strong, or Very Strong.
- Customizable Password Generator — Generates a password with configurable length and character types.
- Encrypted Storage — Saves generated passwords to a local file, encrypted with Fernet symmetric encryption.
- Retrieve and Decrypt — Lists all saved passwords, decrypted on demand using the stored key.
The interface is intentionally minimal:
========================================
Password Tool v1.0
by Judy Ahmad
========================================
=== Password Tool ===
1. Check password strength
2. Generate new password
3. List saved passwords
4. Quit
Choose (1-4):
How It Works
Strength Scoring
The strength checker rewards two things: length, and character variety. Length gives the biggest boost — long passwords are exponentially harder to brute-force than short ones with complex characters.
def check_strength(password):
score = 0
has_lower = has_upper = has_digit = has_special = False
for char in password:
if char.islower():
has_lower = True
elif char.isupper():
has_upper = True
elif char.isdigit():
has_digit = True
else:
has_special = True
if has_lower: score += 15
if has_upper: score += 15
if has_digit: score += 15
if has_special: score += 15
if len(password) >= 8: score += 20
if len(password) >= 12: score += 20
return score
A password like password (8 lowercase characters) scores 35 — long enough for the first length bonus, but with only one character class. MyStr0ngP@ss! scores 80 — same length range, but uses all four character types. Above 12 characters with all four types, you hit 100.
Password Generation
The generator builds a pool of allowed characters based on the user’s preferences, then picks randomly until it reaches the desired length:
def generate_password(length=12, include_uppercase=True, include_digits=True, include_symbols=True):
pool = string.ascii_lowercase
if include_uppercase: pool += string.ascii_uppercase
if include_digits: pool += string.digits
if include_symbols: pool += string.punctuation
password = ""
for _ in range(length):
password += random.choice(pool)
return password
Note: random is fine for a learning project, but for actual security-sensitive randomness you’d use secrets. That’s one of the things I’d improve in a future version.
Encryption with Fernet
This was the most interesting part for me. Fernet is a high-level recipe for symmetric encryption from the cryptography library. Under the hood it uses AES-128 in CBC mode for confidentiality and HMAC-SHA256 for integrity — so an encrypted token isn’t just unreadable, it’s also tamper-evident.
The flow is straightforward:
from cryptography.fernet import Fernet
key = Fernet.generate_key() # generated once, then reused
cipher = Fernet(key)
token = cipher.encrypt(b"MyPassword") # encrypt (bytes in, bytes out)
plaintext = cipher.decrypt(token) # decrypt
In the tool, the key is generated on first run and saved to a secret.key file. Every subsequent run loads that same key — because Fernet is symmetric, the same key both encrypts and decrypts. Lose the key, and all your stored passwords become permanently unreadable.
Passwords are then stored in passwords.txt as name | encrypted_token lines:
Gmail | gAAAAABh-...
GitHub | gAAAAABh-...
Bank | gAAAAABh-...
Anyone who opens the file without the key sees only ciphertext.
Security Considerations
This is where I want to be honest: this tool is a learning project, not a real password manager. Here’s why.
What Fernet gives you:
- Confidentiality — an attacker without the key can’t read encrypted tokens.
- Integrity — tokens can’t be modified without the modification being detected.
- Authenticity — the HMAC ensures tokens were created with your key, not forged.
What it doesn’t give you: protection if the attacker has both the encrypted data and the key. And in this tool, the key sits in secret.key right next to passwords.txt. Anyone with file access has everything.
A real password manager solves this by deriving the encryption key from a master password using a key derivation function like PBKDF2 or Argon2. The key is never stored on disk — it’s regenerated from the master password each session. If the data file is stolen, the attacker still needs to crack the master password before they can decrypt anything.
That’s the next significant upgrade I’d make. The project’s .gitignore excludes both secret.key and passwords.txt so they never end up in the repo, but that’s defense against a different threat (accidental disclosure on GitHub), not against an attacker with local file access.
For real password storage, use a real password manager: Bitwarden, 1Password, or KeePass. They’ve thought through the threat model far more carefully than a learning project can.
What I’d Do Differently
A few things I noted along the way as “next time” or “in a future version”:
- Master password + PBKDF2 key derivation, so the key is never stored on disk.
- Force at least one character from each selected type in the generator. Right now, even if you ask for digits and symbols, a 16-character password might end up with none by sheer randomness.
- Use
secretsinstead ofrandomfor the generator.randomis not cryptographically secure;secretsis purpose-built for security-sensitive randomness. - JSON storage with metadata (
created_at,last_modified, optional notes) instead of a flatname | tokenformat. argparsefor non-interactive use, so the tool can be scripted.- Search and filter saved passwords by name.
Closing
I built this in a focused session, writing every line myself rather than copy-pasting from a finished example. The point was less the tool itself (Bitwarden exists; I don’t need to reinvent it) and more the process: applying Python fundamentals to something tangible, learning what Fernet actually does under the hood, and developing enough security awareness to know what my own tool isn’t protecting against.
If you’re learning Python or cryptography, building something like this teaches more than any tutorial. The bugs you make and fix are the lessons that stick.
Source: github.com/judyahmadd/password-tool License: MIT
