As a project to better my skills in Python, I wrote a password manager. It's a website that has user login/logout functionality, the ability to add websites and passwords, and displays the websites/passwords when the user logs in. Since this is my first project integrating Python with Flask
, I would like feedback concerning security for the most part. I don't work with websites often, so the measures I've taken to secure this website might not be good enough.
File Structure
Password_Manager
| __pycache__
| creds
| creds.txt
| usernames.txt
| data
| ...generated folders and saved websites/passwords...
| templates
| data.html
| index.html
| login.html
| signup.html
| config.py
| encryption.py
| server.py
| utils.py
server.py
"""
Written by: Ben Antonellis
Github: https://github.com/Linnydude3347
Sever Module for starting and stopping the server, and for dealing with
user signin, signout, adding websites to password list, and other server
functions
"""
import os
from functools import wraps
from flask import Flask, render_template, request, redirect, url_for, session, flash
from config import APP_KEY
from encryption import encrypt, decrypt
from utils import generate_uid, divide_data
APP = Flask(__name__)
APP.secret_key = APP_KEY
LOGGED_IN_USER = None
def login_required(func):
"""
A decorator that requires the user to be logged in
in order to access home and logout functionality
"""
@wraps(func)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return func(*args, **kwargs)
flash("You need to login first.")
return redirect(url_for("login"))
return wrap
@APP.route("/")
def main():
"""
Function that returns the homepage for the
password manager
"""
return render_template("index.html")
@APP.route("/login", methods=["GET", "POST"])
def login():
"""
Function for logging in the user
"""
global LOGGED_IN_USER
error = None
if request.method == "POST":
username = request.form['username']
password = request.form['password']
if not user_login(username, password):
error = "Invalid username/password. Please try again."
else:
session['logged_in'] = True
LOGGED_IN_USER = username
flash("You were just logged in!")
return redirect(url_for("main"))
return render_template("login.html", error=error)
@APP.route("/logout")
@login_required
def logout():
"""
Logs out the user
"""
global LOGGED_IN_USER
session.pop("logged_in", None)
LOGGED_IN_USER = None
flash("You were just logged out!")
return redirect(url_for("main"))
@APP.route("/signup", methods=["GET", "POST"])
def signup():
"""
Adds a user to the credentials file
"""
error = None
if request.method == "POST":
with open("creds/creds.txt", "ab") as creds_file:
username = request.form["username"]
password = request.form["password"]
if username == password:
flash("Username cannot equal password!")
return redirect(url_for("signup"))
if not username or not password:
flash("You must provide both a usernamd and password!")
return redirect(url_for("signup"))
if not duplicate_user(username):
creds_file.write(encrypt(username))
creds_file.write(b"\n")
creds_file.write(encrypt(password))
creds_file.write(b"\n")
with open("creds/usernames.txt", "a") as usernames_file:
usernames_file.write(f"{username} {generate_uid()}\n")
else:
flash("There is already a user with that username. Try again.")
return redirect(url_for("signup"))
flash("You were signed up!")
return redirect(url_for("login"))
return render_template("signup.html", error=error)
@APP.route("/data", methods=["GET", "POST"])
@login_required
def data():
"""
This returns a website that contains all the saved websites and passwords
for those websites. It gets the UUID from the currently logged in user,
gets the websites and passwords associated with that user, and passes them
to the website to be displayed
"""
if request.method == "POST":
website = request.form['website']
password = request.form['password']
uuid = get_user_uuid()
path = f"data/{uuid}/"
filename = "data.txt"
if not os.path.isdir(f"data/{uuid}"):
os.mkdir(path)
new_file = open(path + filename, "w")
new_file.close()
with open(path + filename, "wb") as data_file:
data_file.write(encrypt(website))
data_file.write(b"\n")
data_file.write(encrypt(password))
data_file.write(b"\n")
return redirect(url_for("data"))
uuid = get_user_uuid()
sites, pswds = get_data(uuid)
return render_template(
"data.html",
websites=sites,
passwords=pswds
)
def user_login(username: str, password: str) -> bool:
"""
Returns a boolean if a username and password pair match
and in the credentials file
"""
with open("creds/creds.txt", "rb") as creds_file:
try:
data_ = creds_file.read().split(b"\n")
credentials = list(divide_data(data_))
for pair in credentials:
if username == decrypt(pair[0]) and password == decrypt(pair[1]):
return True
return False
except Exception as _:
return False
def duplicate_user(username: str) -> bool:
"""
Returns a boolean based on if the passed username already
exists in the credentials file
"""
with open("creds/usernames.txt", "r") as usernames_file:
data_ = usernames_file.read().split("\n")
usernames = [user.split(" ")[0] for user in data_]
return username in usernames
def get_user_uuid() -> int:
"""
Gets the UUID for the currently logged in user
"""
with open("creds/usernames.txt", "r") as usernames_file:
data_ = usernames_file.read().split("\n")
if data_:
usernames = [user.split(" ")[0] for user in data_ if user != '']
uuids = [uuid.split(" ")[1] for uuid in data_ if uuid != '']
for username, uuid in zip(usernames, uuids):
if username == LOGGED_IN_USER:
return uuid
return None
def get_data(uuid) -> list:
"""
Gathers all the websites and passwords for those
websites and returns them
"""
path = f"data/{uuid}/"
filename = "data.txt"
if not os.path.isdir(f"data/{uuid}"):
os.mkdir(path)
new_file = open(path + filename, "w")
new_file.close()
with open(path + filename, "rb") as data_file:
data_ = data_file.read().split(b"\n")
print(data_)
websites = [decrypt(data_[0]) for site in data_ if site != b'']
passwords = [decrypt(data_[1]) for password in data_ if password != b'']
return websites, passwords
if __name__ == '__main__':
APP.run(debug=True, host="0.0.0.0", port=80)
encryption.py
"""
Written by: Ben Antonellis
Github: https://github.com/Linnydude3347
Module for handing encryption and decryption of data
"""
from cryptography.fernet import Fernet
KEY = b'aQKbfHRvFtvN3QJwPWywwmcQ-0h_JwoOo3k-MjVUecw='
FERNET = Fernet(KEY)
CIPHER_SUITE = FERNET
def encrypt(data_: str) -> bytes:
"""
Returns an encrypted version of the passed data
"""
return CIPHER_SUITE.encrypt(bytes(data_, encoding="utf-8"))
def decrypt(data_: bytes) -> str:
"""
Returns a decrypted version of the passed data
"""
return CIPHER_SUITE.decrypt(data_).decode("utf-8")
utils.py
"""
Written by: Ben Antonellis
Github: https://github.com/Linnydude3347
Utility functions
"""
import random
def generate_uid() -> int:
"""
Returns a random number between 10,000,000 and 99,999,999
"""
return random.randint(10000000, 99999999)
def divide_data(data_: list) -> None:
"""
Returns a 2D list of lists that contain a
username and password, encrypted
"""
for i in range(0, len(data_), 2):
yield data_[i:i + 2]
config.py
"""
Written by: Ben Antonellis
Github: https://github.com/Linnydude3347
Configuration file for containing the Flask secret_key
"""
APP_KEY = "w456789oKNbvCDE456789oKNBVfr56YHGvGT6tf"