143 lines
5.6 KiB
Markdown
143 lines
5.6 KiB
Markdown
# Flowers
|
|
|
|
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.
|
|
|
|
## Existing database compatibility
|
|
|
|
No data migration is required. The rewritten application continues to use:
|
|
|
|
- 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 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.
|
|
|
|
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.
|
|
|
|
## What changed
|
|
|
|
- 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.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 -r requirements.txt
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Local secrets are loaded from the git-ignored `secrets.yaml` file:
|
|
|
|
```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 '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;
|
|
```
|
|
|
|
Put the same value in `database.admin_password` in `secrets.yaml` before starting Flowers.
|
|
|
|
## Run
|
|
|
|
Development:
|
|
|
|
```bash
|
|
python flowers.py
|
|
```
|
|
|
|
Production application server:
|
|
|
|
```bash
|
|
waitress-serve --threads=6 --host=127.0.0.1 --port=5012 --call flowers:create_app
|
|
```
|
|
|
|
Keep the service behind HTTPS. If it is mounted under `/flowers`, set `FLOWERS_URL_PREFIX=/flowers` before starting it.
|
|
|
|
## Tests
|
|
|
|
The tests do not need a live database; they use the actual legacy Fernet implementation with a MariaDB-compatible fake connection.
|
|
|
|
```bash
|
|
pip install -r requirements-dev.txt
|
|
python -m pytest
|
|
```
|
|
|
|
They can also run using only the standard library test runner:
|
|
|
|
```bash
|
|
python -m unittest discover -s tests -v
|
|
```
|
|
|
|
## Remaining security constraints
|
|
|
|
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:
|
|
|
|
- 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.
|
|
|
|
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.
|
|
|
|
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.
|