rewritten after codex recommendations

This commit is contained in:
2026-07-18 20:27:19 +02:00
parent f9dc885937
commit 399f539b4f
22 changed files with 1448 additions and 996 deletions
+87 -175
View File
@@ -1,230 +1,142 @@
# 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.
Flowers is a small self-hosted credential vault built with Flask and MariaDB/MySQL. This version rewrites the legacy server and browser UI while intentionally preserving its on-disk data contract.
> [!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](#security-notes) before exposing the service to a network.
## Existing database compatibility
## What it does
No data migration is required. The rewritten application continues to use:
- 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 `Flowers` database;
- one database account and table named `<username>_` per vault;
- the existing `organization`, `myID`, `myName`, `mySecret`, `dateCreated`, and `deleted` columns;
- the historical password transformation `(password * 10)[:43] + "="` for both the database password and Fernet key;
- Fernet ciphertext in the three protected columns;
- append-only edits, where the newest timestamp is the visible version; and
- `deleted = 1` for deactivation.
The included `client/Daisy.py` is only a Kivy “Hello world” scaffold; it is not a working Flowers client.
The old form-encoded `POST /app` endpoint is also retained. Its `list` and `flower` properties remain JSON strings inside the outer JSON response for existing client compatibility.
## How the application is organized
The legacy key derivation is preserved only because changing it would make existing data unreadable. It is not a modern password KDF. A future KDF upgrade requires an explicit, tested data migration rather than an in-place code change.
```text
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
```
## What changed
### 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.
- All stored values are passed to MariaDB as query parameters.
- Usernames are validated before being used as table/account identifiers.
- Database and runtime settings can be supplied through environment variables.
- Browser forms have CSRF protection.
- Database credentials are stored in server memory behind an opaque session token, not in Flask's signed browser cookie.
- Browser sessions expire after ten minutes of inactivity.
- Failed-login state is JSON rather than unsafe pickle data.
- The browser interface is responsive and no longer depends on jQuery, Pure CSS, or remote assets.
- The Flask application factory and WSGI entry point both work.
- Compatibility tests cover legacy encryption, reads, writes, listing, and the old API format.
## 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
- Python 3.10 or newer
- MariaDB or MySQL on the configured host
- the packages in `requirements.txt`
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install flask waitress fernet pymysql pytz
pip install -r requirements.txt
```
The code imports `Fernet` from the third-party `fernet` package, not from `cryptography.fernet`.
## Configuration
### 2. Bootstrap MariaDB/MySQL
Local secrets are loaded from the git-ignored `secrets.yaml` file:
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:
```yaml
flask:
secret_key: "a-long-random-value"
database:
admin_password: "the-provisioning-account-password"
```
Use `secrets.example.yaml` as a template. `FLOWERS_SECRETS_FILE` can point to a different file. Secret environment variables take precedence over YAML, which is useful for containers and managed deployments.
The other settings remain environment-based:
| Environment variable | Default |
| --- | --- |
| `FLOWERS_SECRETS_FILE` | `secrets.yaml` beside the application |
| `FLOWERS_SECRET_KEY` | overrides `flask.secret_key` from YAML |
| `FLOWERS_DB_HOST` | `localhost` |
| `FLOWERS_DB_PORT` | `3306` |
| `FLOWERS_DB_NAME` | `Flowers` |
| `FLOWERS_DB_ADMIN_USER` | `flower` |
| `FLOWERS_DB_ADMIN_PASSWORD` | overrides `database.admin_password` from YAML |
| `FLOWERS_URL_PREFIX` | empty |
| `FLOWERS_SESSION_MINUTES` | `10` |
| `FLOWERS_COOKIE_SECURE` | `0`; set to `1` behind HTTPS |
| `FLOWERS_ACCESS_FILE` | `/tmp/wsgi_flower_accessfile` |
| `FLOWERS_LOG_FILE` | `/tmp/wsgi_flowers.log` |
For example:
```bash
waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app
```
The provisioning account is needed only by the web-based `/create` flow. Existing vault reads and writes connect with their existing per-user database accounts.
## Database bootstrap for a new installation
An existing Flowers database should be left untouched. For a new installation, create the database and provisioning user, replacing the example password:
```sql
CREATE DATABASE Flowers;
CREATE USER 'flower'@'localhost'
IDENTIFIED BY '608f0b988db4a96066af7dd8870de96c';
CREATE USER 'flower'@'localhost' IDENTIFIED BY 'replace-this';
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.
Put the same value in `database.admin_password` in `secrets.yaml` before starting Flowers.
### 3. Select the URL prefix
## Run
Set `URLPREFIX` in `Config.py`:
```python
URLPREFIX = '' # serve at http://127.0.0.1:5012/
# URLPREFIX = '/flowers' # serve below /flowers
```
### 4. Start the server
For local development:
Development:
```bash
python flowers.py
```
For a non-debug application server:
Production application server:
```bash
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.
Keep the service behind HTTPS. If it is mounted under `/flowers`, set `FLOWERS_URL_PREFIX=/flowers` before starting it.
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.
## Tests
## 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:
The tests do not need a live database; they use the actual legacy Fernet implementation with a MariaDB-compatible fake connection.
```bash
curl -X POST http://127.0.0.1:5012/app \
--data-urlencode 'action=login' \
--data-urlencode 'name=demo' \
--data-urlencode 'password=replace-me'
pip install -r requirements-dev.txt
python -m pytest
```
A typical response is:
```json
{"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:
They can also run using only the standard library test runner:
```bash
sudo a2enmod proxy proxy_http
python -m unittest discover -s tests -v
```
```apache
ProxyPass "/flowers" "http://127.0.0.1:5012/flowers"
ProxyPassReverse "/flowers" "http://127.0.0.1:5012/flowers"
```
## Remaining security constraints
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.
The rewrite removes the immediately exploitable SQL interpolation and browser-cookie credential storage, but Flowers remains a small personal vault with a legacy storage design:
## Configuration
- the historical password-derived Fernet key is intentionally still weak;
- the web process still has provisioning privileges if `/create` is enabled;
- the in-memory credential store is suitable for one application process, not a multi-process cluster; and
- rate-limit state is local to one host.
`Config.py` contains the available settings:
Use a strong unique password made only of Base64-compatible characters, bind Waitress to localhost, and expose it only through an authenticated HTTPS reverse proxy. Set stable provisioning and Flask secrets before deployment.
| 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:
```bash
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:
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.
The old `Services.py`, `SecretServices.py`, and ad-hoc root-level scripts are retained as historical migration references. Do not run them against production data without reviewing them first.