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

232 lines
10 KiB
Markdown

# 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](#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
```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
```
### 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
```bash
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:
```sql
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`:
```python
URLPREFIX = '' # serve at http://127.0.0.1:5012/
# URLPREFIX = '/flowers' # serve below /flowers
```
### 4. Start the server
For local development:
```bash
python flowers.py
```
For a non-debug 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.
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:
```bash
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:
```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:
```bash
sudo a2enmod proxy proxy_http
```
```apache
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:
```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:
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.