rewritten after codex recommendations
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user