Files
flowers/README.md
T
2026-07-18 19:36:23 +02:00

10 KiB

Flowers

Flowers is a small, self-hosted credential vault built with Flask and MariaDB/MySQL. It stores a label, login name, login ID, and secret for each entry, and provides both a server-rendered browser interface and a form-encoded JSON endpoint.

Warning

This is legacy, personal-use software and is not production-ready in its current form. The repository contains hard-coded application/database secrets and committed data exports (db.sql and pins.csv). The application also builds SQL with string interpolation, has no CSRF protection, and relies on an unusual password-derived encryption scheme. Treat every committed dump as sensitive, rotate any real credentials it contains, and review the security notes before exposing the service to a network.

What it does

  • Creates a separate database user and table for each Flowers user.
  • Encrypts the login name, login ID, and secret with Fernet before storing them.
  • Lists the most recent active record for each organization.
  • Supports searching, viewing, adding, and soft-deleting records.
  • Keeps older records when a new version of an organization is added.
  • Locks an IP address out after three failed logins within five minutes.
  • Exposes the same core operations through POST /app for a companion client.

The included client/Daisy.py is only a Kivy “Hello world” scaffold; it is not a working Flowers client.

How the application is organized

flowers.py            Flask application factory
flower_vase.py        Browser routes and the form-based JSON endpoint
FlowerServices.py     Fernet encryption and MariaDB/MySQL operations
AccessControl.py      File-backed failed-login throttling
Config.py             Paths, limits, logging, and URL prefix
templates/            Jinja browser interface
static/               CSS, JavaScript, fonts, and images
flowers.wsgi          Legacy mod_wsgi entry point (currently stale)
apache.conf           Legacy Apache/mod_wsgi example
db.sql, pins.csv      Historical data exports; sensitive, not seed data
Services.py,
SecretServices.py     Older AES-based implementations retained for migration

Data model

Each application username maps to:

  • a database account named <username>_;
  • a table named <username>_ in the Flowers database; and
  • a database password/Fernet key derived by repeating the application password and truncating it to 43 characters, then appending =.

Each table contains:

Column Purpose
organization Plain-text label used to group versions of an entry
myID Fernet-encrypted login ID
myName Fernet-encrypted display/login name
mySecret Fernet-encrypted secret
dateCreated Creation time and version identifier
deleted Soft-delete flag

The organization and timestamp are not encrypted. “Editing” an item inserts a new row, so the latest timestamp becomes the visible version. Deactivation marks a selected row as deleted.

Requirements

  • Python 3
  • MariaDB or MySQL running on localhost
  • Python packages: Flask, Waitress, fernet, PyMySQL, and pytz
  • Apache with mod_proxy only if using the optional reverse-proxy setup

No dependency lock file is included, so the project does not currently define tested package or Python version ranges.

Local setup

1. Create a virtual environment

python3 -m venv .venv
source .venv/bin/activate
pip install flask waitress fernet pymysql pytz

The code imports Fernet from the third-party fernet package, not from cryptography.fernet.

2. Bootstrap MariaDB/MySQL

The application expects a database named Flowers and a provisioning account named flower. With the repository's current defaults, an administrator can create them with:

CREATE DATABASE Flowers;
CREATE USER 'flower'@'localhost'
  IDENTIFIED BY '608f0b988db4a96066af7dd8870de96c';
GRANT CREATE, SELECT, UPDATE, INSERT ON Flowers.*
  TO 'flower'@'localhost' WITH GRANT OPTION;
GRANT CREATE USER, RELOAD ON *.* TO 'flower'@'localhost';
FLUSH PRIVILEGES;

That password is hard-coded in SuperFlower in FlowerServices.py. Change it in both the database and the code before real use. Do not import db.sql as ordinary sample data: it is a historical dump containing user-specific encrypted records.

3. Select the URL prefix

Set URLPREFIX in Config.py:

URLPREFIX = ''          # serve at http://127.0.0.1:5012/
# URLPREFIX = '/flowers'  # serve below /flowers

4. Start the server

For local development:

python flowers.py

For a non-debug application server:

waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app

Open the configured base URL in a browser. To create the first vault user, visit /create under that base URL—for example, http://127.0.0.1:5012/create when URLPREFIX is empty. After creation, return to the home page and log in.

The creation form limits passwords to 10 characters. Because the password is also transformed directly into a Fernet key, use only characters valid in URL-safe Base64 (A-Z, a-z, 0-9, -, and _) with the current implementation.

Browser routes

All routes are relative to URLPREFIX.

Route Methods Purpose
/ GET Login page
/browser GET, POST Login and browser actions (list, show, new, save, deactivate, logout)
/create GET, POST Create a database user, table, and initial demo entry
/update_pwd GET, POST Re-encrypt all records and change the database password
/app POST Form-encoded programmatic interface returning JSON text

Browser sessions expire after 10 minutes of inactivity. Failed logins are recorded in ACCESSFILE; three failures from one IP within five minutes cause temporary denial.

Programmatic endpoint

POST /app accepts application/x-www-form-urlencoded fields. Every request includes:

  • action: login, list, one, save, or deactivate;
  • name: Flowers username; and
  • password: Flowers password.

Additional fields depend on the action:

Action Additional fields
list none
one organization, dateCreated
save organization, myname, myid, secret
deactivate organization, dateCreated

Example login request:

curl -X POST http://127.0.0.1:5012/app \
  --data-urlencode 'action=login' \
  --data-urlencode 'name=demo' \
  --data-urlencode 'password=replace-me'

A typical response is:

{"result": 1, "message": "Access granted"}

For historical compatibility, the list and flower properties are JSON-encoded strings inside the outer JSON response, so clients must decode those properties a second time. The endpoint catches all exceptions and may return only the generic {"result": -1, "message": "Invalid entry"} response when an internal error occurs.

Apache reverse proxy

Waitress can remain bound to localhost while Apache publishes the /flowers path. Set URLPREFIX = '/flowers', enable the proxy modules, and add the proxy rules to the relevant virtual host:

sudo a2enmod proxy proxy_http
ProxyPass        "/flowers" "http://127.0.0.1:5012/flowers"
ProxyPassReverse "/flowers" "http://127.0.0.1:5012/flowers"

Then run Waitress with the command shown above and configure HTTPS on Apache. The checked-in apache.conf and flowers.wsgi describe an older mod_wsgi deployment; flowers.wsgi imports an app object that no longer exists, so use the application factory or update that file before relying on mod_wsgi.

Configuration

Config.py contains the available settings:

Setting Default purpose
LOGFILE Application log path (/tmp/wsgi_flowers.log)
DEBUG Enables debug messages and debug logging when non-zero
INFO Enables informational file logging when non-zero
MAXTEXTLEN Intended maximum text length used by the UI/domain code
MINFIELDLEN Minimum accepted length for organization, login ID, and secret
ACCESSFILE Pickle file used for failed-login throttling
DONOTSETFILTER Sentinel used by the browser search/filter form
URLPREFIX Blueprint prefix, such as '' or '/flowers'

The Flask session signing key is currently hard-coded in flowers.py. Configuration is not loaded from environment variables.

Tests and maintenance scripts

There is currently no automated test suite. A syntax-only check for the primary modules can be run without a database:

python -m py_compile \
  flowers.py flower_vase.py FlowerServices.py \
  AccessControl.py Config.py Log.py

The files test.py, test_fernet.py, import.py, and dumpl.py are ad-hoc scripts with embedded usernames/passwords or destructive/database-dependent behavior. Read and edit them before running them; they are not safe, isolated unit tests.

Security notes

Before any serious deployment, at minimum:

  1. Remove db.sql, pins.csv, and embedded credentials from version history, then rotate every exposed or recoverable credential.
  2. Move the Flask secret key, provisioning database password, database name/host, and filesystem paths to environment-based configuration.
  3. Replace string-formatted SQL with parameterized queries and validate table/account identifiers.
  4. Replace the current password-to-Fernet-key construction with a password KDF such as Argon2id, scrypt, or PBKDF2 using a unique salt and appropriate work factor.
  5. Add CSRF protection, secure cookie settings, explicit error handling, and tests for authentication and authorization.
  6. Run only behind HTTPS and restrict access at the firewall or reverse proxy.
  7. Rework user provisioning so the web process does not hold CREATE USER, RELOAD, and grant privileges during normal operation.
  8. Replace the pickle-backed access-control file with a safe, concurrency-aware rate limiter.

Fernet protects stored field contents from casual database inspection, but it does not compensate for weak/reused passwords, exposed keys, SQL injection, a compromised application host, or unencrypted transport.

License

No license file is currently included. Unless a license is added, normal copyright restrictions apply.