From 7e64c5b4cf1117a2975e9cccf28475a732f6a6a3 Mon Sep 17 00:00:00 2001 From: Ignace Date: Thu, 14 May 2026 21:16:51 +0200 Subject: [PATCH] removed secrets from the code (reom from chatgpt) --- README.md | 45 +++++++++++++++++++++++++++++++++++ api.py | 4 ++-- auth.py | 59 ++++++++++++---------------------------------- initialize_data.py | 26 ++++++++++++++++---- lapp.py | 27 ++++++++++++++++++--- lists.py | 10 ++------ models.py | 17 ++++++++++++- 7 files changed, 125 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 3444a38..3d998bb 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,48 @@ install 2. create subfolder "instance" to store database file 3. for an initial instance: run python3 initialze_data.py, with the python from the virtual-env 4. add the below config to your apache2 enabled site + +## Secrets and configuration + +LAPP does not store runtime secrets in the source code. Configure them with +environment variables in production: + +```sh +export LAPP_SECRET_KEY="replace-with-a-long-random-secret" +export LAPP_APPLICATION_KEY="replace-with-another-long-random-secret" +export LAPP_DATABASE_URI="sqlite:///app.db" +``` + +`LAPP_SECRET_KEY` signs Flask sessions and auto-login cookies. Keep it stable: +changing it logs users out and invalidates existing auto-login cookies. + +`LAPP_APPLICATION_KEY` is used by `/api/validate_key`. + +`LAPP_DATABASE_URI` is optional. If it is not set, LAPP uses `sqlite:///app.db`. + +For local development, if `LAPP_SECRET_KEY` or `LAPP_APPLICATION_KEY` is not set, +the app creates stable random secrets in: + +```text +instance/secret_key +instance/application_key +``` + +The `instance/` directory is ignored by git, so these generated secrets should +not be committed. + +## Initial data secrets + +`initialize_data.py` creates initial users and groups only when the database has +no users yet. You can provide the initial passwords and group secrets with: + +```sh +export LAPP_INITIAL_ADMIN_PASSWORD="replace-me" +export LAPP_INITIAL_ADMIN_GROUP="replace-me" +export LAPP_INITIAL_USER_PASSWORD="replace-me" +export LAPP_INITIAL_USER_GROUP="replace-me" +./venv/bin/python initialize_data.py +``` + +If these variables are not set, `initialize_data.py` generates random values and +prints them once when it creates the initial data. diff --git a/api.py b/api.py index 36b397a..984f7fd 100644 --- a/api.py +++ b/api.py @@ -1,4 +1,4 @@ -from flask import Blueprint, render_template, redirect, url_for, request +from flask import Blueprint, render_template, redirect, url_for, request, current_app from flask_login import login_required, current_user, login_user from models import db from models import User, Item @@ -14,7 +14,7 @@ def validate_key(): result['data'] = '' if "key" in request.form: key = request.form["key"] - if key == 'NogNietNodigHier': + if key == current_app.config["APPLICATION_KEY"]: result['status'] = 1 else: result['message'] = 'Unknown Application-Key' diff --git a/auth.py b/auth.py index 305b455..e909886 100644 --- a/auth.py +++ b/auth.py @@ -1,64 +1,35 @@ -from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response +from flask import Blueprint, render_template, request, redirect, url_for, flash, make_response, current_app from flask_login import login_user, logout_user, login_required, current_user from models import db from models import User from log import Log -import pickle -from cryptography.fernet import Fernet -import datetime +from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer auth_bp = Blueprint("auth", __name__) -# ===================================================== -# SECRET KEY (only for this program) -# ===================================================== -# Generate once using: Fernet.generate_key() -_SECRET_KEY = b'YFK7QCyTzhyLO4vqrnRxvDAI5uu8mXEYrInEjbRoQgs=' -fernet = Fernet(_SECRET_KEY) +AUTOLOGIN_MAX_AGE_SECONDS = 7 * 24 * 60 * 60 +AUTOLOGIN_SALT = "lapp-autologin" -# ===================================================== -# ENCRYPT -# ===================================================== -def encrypt_object(obj) -> bytes: - """ - Encrypt any Python object and return encrypted bytes. - """ - serialized = pickle.dumps(obj) - encrypted = fernet.encrypt(serialized) - return encrypted - -# ===================================================== -# DECRYPT -# ===================================================== -def decrypt_object(encrypted_data: bytes): - """ - Decrypt bytes back into the original Python object. - """ - decrypted = fernet.decrypt(encrypted_data) - obj = pickle.loads(decrypted) - return obj +def autologin_serializer(): + return URLSafeTimedSerializer(current_app.config["SECRET_KEY"], salt=AUTOLOGIN_SALT) def create_cookie(userid): cookie_data = { - "user": userid, - "datetime": datetime.datetime.now() + "user": userid } - return encrypt_object(cookie_data).decode('utf-8') + return autologin_serializer().dumps(cookie_data) def is_cookie_ok_to_autologin(cookie_content): - user = 0 - autologin = False + if not cookie_content: + return 0 + try: - cookie_data = decrypt_object(cookie_content) - user = cookie_data["user"] - dt = cookie_data["datetime"] - lastweek = datetime.datetime.now() - datetime.timedelta(days=7) - autologin = dt > lastweek - except: + cookie_data = autologin_serializer().loads(cookie_content, + max_age=AUTOLOGIN_MAX_AGE_SECONDS) + return int(cookie_data["user"]) + except (BadSignature, SignatureExpired, KeyError, TypeError, ValueError): Log.info("Cookie error") - if autologin: - return user return 0 #. @auth_bp.route("/", methods=["GET", "POST"]) diff --git a/initialize_data.py b/initialize_data.py index d62c92d..07e48bd 100644 --- a/initialize_data.py +++ b/initialize_data.py @@ -1,25 +1,41 @@ from lapp import create_app +import os +import secrets import sqlalchemy as sa from models import db, User, ListOfItems, Shared, Item, Group +def initial_secret(env_name): + return os.environ.get(env_name) or secrets.token_urlsafe(18) + if __name__ == "__main__": app = create_app() app.app_context().push() query = sa.select(User) users = db.session.scalars(query).all() if len(users) == 0: - g = Group(secret="This is the group just for the admin") + admin_group_secret = initial_secret("LAPP_INITIAL_ADMIN_GROUP") + user_group_secret = initial_secret("LAPP_INITIAL_USER_GROUP") + admin_password = initial_secret("LAPP_INITIAL_ADMIN_PASSWORD") + user_password = initial_secret("LAPP_INITIAL_USER_PASSWORD") + + g = Group(secret=admin_group_secret) db.session.add(g) - g = Group(secret="GroupForIgnaceAndLoversThatDoGroceries") + g = Group(secret=user_group_secret) db.session.add(g) admin = User(name="admin", group_id=1, is_admin=True, is_approved=True, is_private=True) - admin.set_password("suy2025") + admin.set_password(admin_password) db.session.add(admin) user1 = User(name="ignace", group_id=2, is_admin=False, is_approved=True) - user1.set_password("xanderj2") + user1.set_password(user_password) db.session.add(user1) db.session.commit() + print("Created initial credentials:") + print(f"admin password: {admin_password}") + print(f"admin group secret: {admin_group_secret}") + print(f"ignace password: {user_password}") + print(f"ignace group secret: {user_group_secret}") + lol1 = ListOfItems(owner_user_id = user1.id, name="Supermarkt", is_active=True) db.session.add(lol1) lol2 = ListOfItems(owner_user_id = user1.id, name="Klusjes", is_active=True) @@ -35,4 +51,4 @@ if __name__ == "__main__": db.session.add(item3) db.session.add(item4) - db.session.commit() \ No newline at end of file + db.session.commit() diff --git a/lapp.py b/lapp.py index 18fe530..5d88dc1 100644 --- a/lapp.py +++ b/lapp.py @@ -1,3 +1,6 @@ +import os +import secrets +from pathlib import Path from flask import Flask, send_from_directory, url_for from flask_login import LoginManager from models import db @@ -11,10 +14,28 @@ from groups import groups_bp login_manager = LoginManager() +def load_secret(app, env_name, filename): + secret = os.environ.get(env_name) + if secret: + return secret + + instance_path = Path(app.instance_path) + instance_path.mkdir(parents=True, exist_ok=True) + secret_path = instance_path / filename + + if secret_path.exists(): + return secret_path.read_text(encoding="utf-8").strip() + + secret = secrets.token_urlsafe(48) + secret_path.write_text(secret, encoding="utf-8") + secret_path.chmod(0o600) + return secret + def create_app(): - app = Flask(__name__) - app.config["SECRET_KEY"] = "9823740934riurehoiuwerf873487qe78feri" - app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db" + app = Flask(__name__, instance_relative_config=True) + app.config["SECRET_KEY"] = load_secret(app, "LAPP_SECRET_KEY", "secret_key") + app.config["APPLICATION_KEY"] = load_secret(app, "LAPP_APPLICATION_KEY", "application_key") + app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("LAPP_DATABASE_URI", "sqlite:///app.db") db.init_app(app) login_manager.init_app(app) diff --git a/lists.py b/lists.py index 2a8d35f..935a750 100644 --- a/lists.py +++ b/lists.py @@ -37,12 +37,7 @@ def edit(): abort(403) newname = request.form["name"].strip() - double = False - for d in ListOfItems.query.filter_by(owner_user_id=current_user.id, name=newname).all(): - if not d.id == ilist_id: - # there is a duplicate name - double = True - if double: + if not newname or ListOfItems.name_exists_for_owner(current_user.id, newname, exclude_id=ilist_id): return render_template("lists_edit.html", user=current_user, ilists=current_user.my_owned_lists(with_inactive=True), users=not_current_user(current_user.id), @@ -76,8 +71,7 @@ def edit(): # must be a new list newlist = ListOfItems(name=request.form["name"].strip(), owner_user_id=current_user.id) # check if name exists - duplicate = ListOfItems.query.filter_by(owner_user_id=current_user.id, name=newlist.name).first() - if duplicate: + if not newlist.name or ListOfItems.name_exists_for_owner(current_user.id, newlist.name): # there is a duplicate name return render_template("lists_edit.html", user=current_user, ilists=current_user.my_owned_lists(with_inactive=True), diff --git a/models.py b/models.py index 817d610..94f69df 100644 --- a/models.py +++ b/models.py @@ -2,6 +2,7 @@ from flask_sqlalchemy import SQLAlchemy from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime +from sqlalchemy import func db = SQLAlchemy() # Create the extension object @@ -72,12 +73,26 @@ class User(UserMixin, db.Model): return 0 class ListOfItems(db.Model): + __table_args__ = ( + db.UniqueConstraint("owner_user_id", "name", name="uq_list_owner_name"), + ) + id = db.Column(db.Integer, primary_key=True) updated_at = db.Column(db.TIMESTAMP, default=datetime.now(), onupdate=datetime.now(), nullable=False) owner_user_id = db.Column(db.Integer, nullable=False) - name = db.Column(db.String(40), unique=True, nullable=False) + name = db.Column(db.String(40), nullable=False) is_active = db.Column(db.Boolean, default=False) + @classmethod + def name_exists_for_owner(cls, owner_user_id, name, exclude_id=None): + query = cls.query.filter( + cls.owner_user_id == owner_user_id, + func.lower(cls.name) == name.strip().lower(), + ) + if exclude_id is not None: + query = query.filter(cls.id != exclude_id) + return query.first() is not None + def as_dict(self): result = {} for p in ['id','name','owner_user_id','is_active']: