Go to file
3nd3r 99859f009f Fix #1+#8: Extract shared config module, unify JWT secret
- Create config.py with shared constants, AES-GCM helpers, and JWT helpers
- app.py and routes.py now import from the single source of truth
- Eliminates JWT secret mismatch (routes.py had hardcoded default)
- Removes all duplicate _issue_jwt, _verify_jwt, _aesgcm_encrypt,
  _aesgcm_decrypt definitions
- start.py also uses shared config loader
2026-04-12 12:49:44 -05:00
static Fix Lobby tab switching and message submission logic 2026-04-12 18:15:44 +01:00
.env.example Initial commit: SexyChat (Aphrodite) v1.0 2026-04-12 17:55:40 +01:00
.gitignore Implement JSON config system and AI PM integration 2026-04-12 18:09:05 +01:00
README.md Add detailed README with full security audit 2026-04-12 12:31:27 -05:00
app.py Fix #1+#8: Extract shared config module, unify JWT secret 2026-04-12 12:49:44 -05:00
config.py Fix #1+#8: Extract shared config module, unify JWT secret 2026-04-12 12:49:44 -05:00
database.py Initial commit: SexyChat (Aphrodite) v1.0 2026-04-12 17:55:40 +01:00
index.html Fix Lobby tab switching and message submission logic 2026-04-12 18:15:44 +01:00
models.py Initial commit: SexyChat (Aphrodite) v1.0 2026-04-12 17:55:40 +01:00
requirements.txt Initial commit: SexyChat (Aphrodite) v1.0 2026-04-12 17:55:40 +01:00
routes.py Fix #1+#8: Extract shared config module, unify JWT secret 2026-04-12 12:49:44 -05:00
start.py Fix #1+#8: Extract shared config module, unify JWT secret 2026-04-12 12:49:44 -05:00

README.md

Aphrodite — SexyChat

A real-time encrypted chat application built with Flask-SocketIO, featuring a public lobby, end-to-end encrypted private messaging, an AI companion ("Violet") powered by Ollama, moderation tools, and a payment-gated premium tier.


Table of Contents


Features

  • Public Lobby — Ephemeral group chat room. Messages are never persisted; they live only in connected clients' browsers.
  • User Registration & Login — Accounts with bcrypt-hashed passwords, JWT session tokens, and moderator-gated verification.
  • End-to-End Encrypted Private Messages — AES-GCM-256 encryption using PBKDF2-derived keys. The server stores only ciphertext and nonces.
  • Violet AI Companion — A flirtatious AI hostess backed by a local Ollama instance. Messages are transit-encrypted (decrypted server-side for inference, re-encrypted before storage and delivery).
  • Freemium AI Access — Free users get a configurable number of AI messages before hitting a paywall. Paying users get unlimited access.
  • Moderation Tools — Kick, ban, kickban, mute/unmute, and manual account verification. Moderator access is granted via a shared admin password at login.
  • Ignore System — Registered users can ignore/unignore other users. Ignored users' messages are hidden client-side, and PM invitations from ignored users are silently blocked.
  • Responsive UI — Glassmorphism design with a deep purple/neon magenta theme. Mobile-friendly with a collapsible nicklist sidebar.
  • Process Managerstart.py wraps Gunicorn with daemon management (start, stop, restart, status, debug).

Architecture Overview

┌──────────────┐        WebSocket / HTTP        ┌─────────────────────┐
│   Browser    │ ◄──────────────────────────────►│   Flask-SocketIO    │
│  (chat.js)   │                                 │     (app.py)        │
│  (crypto.js) │                                 │                     │
└──────────────┘                                 │  ┌───────────────┐  │
                                                 │  │ REST Blueprint │  │
                                                 │  │  (routes.py)   │  │
                                                 │  └───────┬───────┘  │
                                                 │          │          │
                                                 │  ┌───────▼───────┐  │
                                                 │  │  SQLAlchemy    │  │
                                                 │  │ (database.py)  │  │
                                                 │  │ (models.py)    │  │
                                                 │  └───────┬───────┘  │
                                                 │          │          │
                                                 │  ┌───────▼───────┐  │
                                                 │  │  SQLite / PG   │  │
                                                 │  └───────────────┘  │
                                                 │                     │
                                                 │  ┌───────────────┐  │
                                                 │  │  Ollama (AI)   │  │
                                                 │  └───────────────┘  │
                                                 └─────────────────────┘

Key design decisions:

  • Single-process, single-worker — Eventlet async mode with one Gunicorn worker. All in-memory state (connected users, bans, mutes) lives in the process.
  • AI inference queue — A dedicated eventlet greenlet serialises Ollama requests one at a time, broadcasting violet_typing indicators while busy.
  • Lobby is ephemeral — No lobby messages are ever written to the database.
  • PMs are persisted for registered users — Encrypted messages are stored in the messages table. History is capped at 500 messages per conversation pair.

Tech Stack

Layer Technology
Backend framework Flask 3.x
Real-time transport Flask-SocketIO 5.x (eventlet async)
WSGI server Gunicorn 21.x with eventlet worker
Database ORM Flask-SQLAlchemy 3.x
Migrations Flask-Migrate 4.x (Alembic)
Database SQLite (default) or PostgreSQL (via DATABASE_URL)
Password hashing bcrypt 4.x
JWT auth PyJWT 2.x
Server-side crypto cryptography 42.x (AES-GCM)
Client-side crypto Web Crypto API (SubtleCrypto)
AI inference Ollama (local, HTTP API)
Frontend Vanilla JS, Socket.IO 4 client, custom CSS

Project Structure

aprhodite/
├── app.py              # Flask-SocketIO app factory, socket event handlers, AI queue
├── database.py         # SQLAlchemy & Flask-Migrate initialisation, DB seeding
├── models.py           # ORM models: User, Message, UserIgnore
├── routes.py           # REST API blueprint (auth, PM history, AI message, payment)
├── start.py            # Gunicorn process manager (start/stop/restart/status/debug)
├── index.html          # Single-page frontend (join screen, chat, modals)
├── requirements.txt    # Python dependencies
├── config.json         # Optional config file (alternative to env vars)
└── static/
    ├── chat.js         # Frontend logic (socket events, PM/AI flows, UI)
    ├── crypto.js       # AES-GCM-256 encryption wrapper (PBKDF2 key derivation)
    ├── socket.io.min.js # Socket.IO v4 client library
    └── style.css       # Glassmorphism theme (deep purple / neon magenta)

Configuration

Configuration is loaded with the following precedence: Environment Variable → config.json → Default Value.

Key Default Description
SECRET_KEY Random UUID hex Flask session secret
JWT_SECRET Random UUID hex HMAC signing key for JWTs
ADMIN_PASSWORD admin1234 Shared moderator password
DATABASE_URL sqlite:///sexchat.db SQLAlchemy database URI
AI_FREE_LIMIT 3 Free AI messages before paywall
OLLAMA_URL http://localhost:11434 Ollama API base URL
VIOLET_MODEL sam860/dolphin3-llama3.2:3b Ollama model tag for Violet
PAYMENT_SECRET change-me-payment-secret Webhook validation secret
HOST 0.0.0.0 Bind address
PORT 5000 Bind port
SOCKETIO_MESSAGE_QUEUE None Redis URL for multi-process Socket.IO
REDIS_URL None Fallback for SOCKETIO_MESSAGE_QUEUE

Create a config.json in the project root:

{
  "SECRET_KEY": "your-secret-key",
  "JWT_SECRET": "your-jwt-secret",
  "ADMIN_PASSWORD": "a-strong-moderator-password",
  "DATABASE_URL": "postgresql://user:pass@localhost/sexchat",
  "PAYMENT_SECRET": "your-stripe-webhook-secret",
  "OLLAMA_URL": "http://localhost:11434",
  "VIOLET_MODEL": "sam860/dolphin3-llama3.2:3b",
  "AI_FREE_LIMIT": 3
}

Installation & Setup

Prerequisites

  • Python 3.10+
  • pip
  • Ollama (optional — required only for live AI responses)

Install

git clone https://git.computertech.dev/lord3nd3r/aprhodite.git
cd aprhodite
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Database

On first run, database.py calls db.create_all() and seeds the Violet bot user. No manual migration is needed for a fresh start. For schema changes:

flask db migrate -m "description"
flask db upgrade

Ollama (for live AI)

# Install Ollama (https://ollama.com)
ollama pull sam860/dolphin3-llama3.2:3b
ollama serve   # default :11434

If Ollama is unavailable, routes.py falls back to canned responses; app.py returns a placeholder message.


Running the Server

python start.py start     # Start as background daemon
python start.py stop      # Graceful shutdown
python start.py restart   # Stop + start
python start.py status    # Check if running
python start.py debug     # Foreground with debug logging

Direct Gunicorn

gunicorn --worker-class eventlet -w 1 --bind 0.0.0.0:5000 start:application

Development (Flask built-in server)

from app import create_app, socketio
app = create_app()
socketio.run(app, host="0.0.0.0", port=5000, debug=True)

Socket Protocol

Client → Server Events

Event Payload Description
join { mode, username, password?, email?, mod_password? } Authenticate and enter the lobby. mode is guest, login, register, or restore.
message { text } Send a message to the lobby.
pm_open { target } Request a private message room with target user.
pm_accept { room } Accept an incoming PM invitation.
pm_message { room, text? } or { room, ciphertext, nonce, transit_key? } Send a PM. Include transit_key when messaging Violet.
ai_message { ciphertext, nonce, transit_key } Deprecated. Use pm_message to Violet instead.
mod_kick { target } Kick a user (admin only).
mod_ban { target } Ban a user by username + IP (admin only).
mod_kickban { target } Kick and ban simultaneously (admin only).
mod_mute { target } Toggle mute on a user (admin only).
mod_verify { target } Verify a registered user's account (admin only).
user_ignore { target } Add user to ignore list (registered only).
user_unignore { target } Remove user from ignore list (registered only).

Server → Client Events

Event Payload Description
joined { username, is_admin, is_registered, has_ai_access, ai_messages_used, token?, ignored_list? } Successful join confirmation.
nicklist { users: [{ username, is_admin, is_registered, is_verified, is_ai? }] } Updated user list.
message { username, text, is_admin, is_registered, ts } Lobby message.
system { msg, ts } System announcement (joins, parts, mod actions).
error { msg } Error message.
kicked { msg } Notification that the user has been kicked/banned.
pm_invite { from, room } Incoming PM invitation.
pm_ready { with, room } PM room is ready (sent to initiator).
pm_message { from, text?, ciphertext?, nonce?, room, ts } Private message.
violet_typing { busy: bool, room? } Violet AI typing indicator.
ai_response { ciphertext, nonce, ai_messages_used, has_ai_access } or { error } AI response (legacy event).
ai_unlock { msg } AI access unlocked after payment.
ignore_status { target, ignored: bool } Confirmation of ignore/unignore action.

REST API Endpoints

All endpoints are under the /api prefix.

POST /api/auth/register

Create a new account.

Body: { "username": "...", "password": "...", "email": "..." } Response: 201 { "token": "jwt...", "user": { id, username, has_ai_access, ai_messages_used } }

POST /api/auth/login

Authenticate and receive a JWT.

Body: { "username": "...", "password": "..." } Response: 200 { "token": "jwt...", "user": { ... } }

GET /api/pm/history?with=<username>

Retrieve encrypted PM history with another user. Requires Authorization: Bearer <token>.

Response: 200 { "messages": [{ from_me, ciphertext, nonce, ts }] }

POST /api/ai/message

Send an encrypted message to Violet via REST (alternative to socket).

Body: { "ciphertext": "...", "nonce": "...", "transit_key": "..." } Response: 200 { "ciphertext": "...", "nonce": "...", "ai_messages_used": N, "has_ai_access": bool }

POST /api/payment/success

Payment webhook to unlock AI access.

Body: { "secret": "..." } Response: 200 { "status": "ok", "has_ai_access": true }


Encryption Design

User-to-User PMs

  1. Key Derivation — The client derives an AES-GCM-256 key from (password, username) using PBKDF2 with 100,000 iterations and SHA-256. The salt is "sexychat:v1:<lowercase_username>".
  2. Encryption — Messages are encrypted client-side with a random 12-byte nonce before being sent over the socket.
  3. Storage — The server stores only the base64-encoded ciphertext and nonce. It never sees the plaintext or key.
  4. Decryption — The recipient decrypts client-side using their own derived key.

Violet AI (Transit Encryption)

  1. The client encrypts the message with their derived key and also sends the key (transit_key) to the server over HTTPS/WSS.
  2. The server decrypts the message (transit), passes plaintext to Ollama for inference, then re-encrypts the AI response with the same transit key.
  3. The transit key is used only in memory and is never stored.

Moderation System

Moderators authenticate by providing the shared ADMIN_PASSWORD alongside their normal login credentials. Mod powers include:

Action Effect
Kick Disconnects the target. They can rejoin immediately.
Ban Adds username + IP to in-memory ban lists. Persists until server restart.
Kickban Kick + ban in one action.
Mute / Unmute Toggles mute. Muted users cannot send lobby messages.
Verify Marks a registered account as verified, allowing them to log in. New registrations require moderator verification before the account can be used.

Violet AI Companion

Violet is an AI persona backed by a local Ollama model. She is configured as a "flirtatious and sophisticated nightclub hostess" via a system prompt.

  • Model: Configurable via VIOLET_MODEL (default: sam860/dolphin3-llama3.2:3b)
  • Inference: Serialised through an eventlet queue — one request at a time to avoid overloading the local GPU/CPU
  • Typing indicator: violet_typing events are broadcast while Ollama is processing
  • Fallback: If Ollama fails, a graceful fallback message is returned
  • Free tier: Configurable via AI_FREE_LIMIT (default: 3 messages)

Payment & Premium Access

The current payment flow is a stub intended to be replaced with a real payment provider (e.g. Stripe):

  1. Client sends the PAYMENT_SECRET to POST /api/payment/success.
  2. Server validates with constant-time comparison, flips user.has_ai_access = True.
  3. An ai_unlock socket event is pushed to the user's active session.

For production: Replace the secret-comparison logic with stripe.Webhook.construct_event() using the raw request body and Stripe-Signature header.


Database Schema

users

Column Type Notes
id Integer PK Auto-increment
username String(20) Unique, indexed
password_hash String(128) bcrypt
email String(255) Unique, nullable
has_ai_access Boolean Premium flag
ai_messages_used Integer Free trial counter
is_verified Boolean Moderator-verified flag
created_at DateTime UTC timestamp

messages

Column Type Notes
id Integer PK Auto-increment
sender_id FK → users.id
recipient_id FK → users.id
encrypted_content Text Base64 AES-GCM ciphertext
nonce String(64) Base64 AES-GCM nonce/IV
timestamp DateTime Indexed with sender/recipient

user_ignores

Column Type Notes
id Integer PK Auto-increment
ignorer_id FK → users.id
ignored_id FK → users.id
created_at DateTime

Unique composite index on (ignorer_id, ignored_id).


Known Security Issues & Bugs

The following issues have been identified during code review. They are listed by severity.


P0 — Critical (fix immediately)

1. JWT Secret Mismatch Between app.py and routes.py

File: app.py line 84, routes.py line 30

app.py generates a random JWT_SECRET via uuid.uuid4().hex on every server restart. routes.py independently hardcodes "change-me-jwt-secret" as its default. These are two separate values — JWTs issued by the socket layer (app.py) will fail validation in the REST API (routes.py) and vice versa. Authentication is broken across the two entry points.

Impact: Users who log in via sockets cannot use the REST API, and vice versa. In the worst case, if routes.py's default is left in production, its JWT secret is publicly known from the source code, allowing anyone to forge tokens.

Fix: Use a single shared JWT_SECRET loaded once from configuration. Remove the duplicate constant in routes.py and import from app.py or a shared config module.


2. Unauthorised PM Room Join (pm_accept)

File: app.pyon_pm_accept handler

@socketio.on("pm_accept")
def on_pm_accept(data):
    join_room(data.get("room"))  # no validation

Any client can emit pm_accept with an arbitrary room name (e.g. pm:alice:bob) and silently join that room, receiving all future messages. There is no check that the user was actually invited to this room.

Impact: Complete compromise of private messaging confidentiality (for plaintext/guest PMs). For encrypted PMs, the attacker receives ciphertext but not keys — however, it still leaks metadata (who is talking, when, message sizes).

Fix: Maintain a server-side mapping of pending invitations. Only allow pm_accept if the user's SID has a pending invite for that specific room.


3. Client-Exploitable Payment Endpoint

Files: routes.pypayment_success, chat.js lines 560574

The payment "verification" consists of the client sending a secret string to the server. The default PAYMENT_SECRET is "change-me-payment-secret" (known from source). Even if changed, the client-side code in chat.js hardcodes the secret and sends it directly:

const secret = "change-me-payment-webhook-secret";

Any user with a valid JWT can call POST /api/payment/success with the secret and unlock premium AI access for free.

Impact: Complete bypass of the payment system. Any registered user gets unlimited AI access without paying.

Fix: Replace with a server-side Stripe/payment-provider webhook. The payment confirmation must originate from the payment provider's servers, not from the client.


4. PM Invite Broadcast When PMing Violet

File: app.pyon_pm_open handler, lines 607612

When a user opens a PM with Violet, target_sid is None (Violet has no socket connection). The code then executes:

socketio.emit("pm_invite", {"from": user["username"], "room": room}, to=None)

Emitting to=None broadcasts the event to all connected clients, informing everyone that the user is starting a private conversation with Violet.

Impact: Privacy leak — all connected users learn who is chatting with Violet.

Fix: Skip the pm_invite emit when the target is Violet. The initiator already receives pm_ready; no invite is needed for a bot.


P1 — High Severity

5. E2E Encryption Is Fundamentally Broken for User-to-User PMs

File: static/crypto.jsderiveKey

Keys are derived from (password, username) using PBKDF2. When User A sends a PM to User B:

  • User A encrypts with a key derived from A's password and A's username.
  • User B attempts to decrypt with a key derived from B's password and B's username.
  • These are entirely different keys. Decryption will always fail.

The only scenario where this works is when a user decrypts their own messages (e.g. PM history reload), since the key matches. But real-time cross-user PMs cannot be decrypted.

Impact: User-to-user encrypted PMs are non-functional. Users will see decryption errors for every received message.

Fix: Implement a proper key exchange protocol (e.g. ECDH / X25519) where both parties derive a shared secret, or use a server-mediated key-wrapping scheme.


6. Null DOM Element References Crash the Frontend

File: static/chat.js

  • Line 547: $("tab-ai-violet") — references an element ID that does not exist in index.html.
  • Line 44: $("violet-trial-badge") — also missing from the HTML.

These return null, and any property access (e.g. .onclick, .classList) will throw TypeError: Cannot read properties of null.

Impact: JavaScript execution halts at these points, potentially breaking logout, tab switching, or the entire chat UI on load.

Fix: Add the missing elements to index.html, or add null guards before accessing these elements.


7. CORS Wildcard on WebSocket

File: app.py line 381

socketio.init_app(app, cors_allowed_origins="*", ...)

This allows any website on the internet to open WebSocket connections to the server. A malicious page could connect on behalf of a visiting user if cookies/tokens are available.

Impact: Cross-site WebSocket hijacking. An attacker's page could impersonate users, send messages, or eavesdrop on the lobby.

Fix: Restrict cors_allowed_origins to the actual domain(s) serving the frontend.


P2 — Medium Severity

8. Massive Code Duplication Between app.py and routes.py

Both files independently define their own copies of:

  • _aesgcm_encrypt / _aesgcm_decrypt
  • _issue_jwt / _verify_jwt
  • _save_pm / _persist_message
  • Constants: AI_FREE_LIMIT, AI_BOT_NAME, JWT_SECRET

They have already diverged — routes.py uses mock AI responses (random.choice(AI_RESPONSES)) while app.py calls Ollama. The JWT secrets are different (see issue #1).

Impact: Bugs fixed in one file won't be fixed in the other. Behaviour differs depending on whether the user interacts via sockets or REST. Maintenance burden grows over time.

Fix: Extract shared utilities (crypto, JWT, DB helpers, constants) into a common module (e.g. utils.py or config.py). Import from there in both app.py and routes.py.


9. Context Menu Breaks After First Use

File: static/chat.jsshowContextMenu function, lines 453455

const newMenu = contextMenu.cloneNode(true);
contextMenu.replaceWith(newMenu);

The function clones and replaces the context menu DOM node to clear event listeners. However, the module-level contextMenu variable still references the old (now-removed) node. On the second right-click, the code operates on a detached element.

Impact: Context menu stops working after the first use. Users can only PM/ignore/mod-action once per page load.

Fix: Update the variable reference after replacement, or use removeEventListener / event delegation instead of cloning.


10. All Runtime State Is In-Memory Only

File: app.py lines 113118

connected_users, banned_usernames, banned_ips, and muted_users are plain Python dicts/sets. All bans, mutes, and IP blocks are lost on every server restart.

Impact: Banned users can return simply by waiting for a server restart. Moderator actions have no lasting effect.

Fix: Persist bans and mutes to the database. Load them into memory on startup.


P3 — Low Severity

11. Deprecated datetime.utcnow() Usage

File: models.py — all default=datetime.utcnow column definitions

datetime.utcnow() is deprecated as of Python 3.12. It returns a naive datetime with no timezone info, which can cause subtle bugs with timezone-aware code.

Impact: Deprecation warnings on Python 3.12+. Potential timezone-related bugs if the codebase later mixes aware and naive datetimes.

Fix: Use datetime.now(datetime.timezone.utc) or func.now() (SQLAlchemy server default) instead.


12. Default Admin Password

File: app.py line 85

ADMIN_PASSWORD = _get_conf("ADMIN_PASSWORD", "admin1234")

If the operator does not set a custom admin password, anyone who reads the source code or guesses admin1234 gains full moderator access (kick, ban, mute, verify users).

Impact: Unauthorised moderator access in default-configured deployments.

Fix: Require ADMIN_PASSWORD to be explicitly set in config. Refuse to start (or disable mod features) if it's left at the default value.