From 9fd8744ec4e356dedb7a3b13c26c36ead8afd4a6 Mon Sep 17 00:00:00 2001
From: brainx <12695242+brainx@users.noreply.github.com>
Date: Sun, 2 Aug 2026 20:56:13 +0200
Subject: [PATCH 1/3] feat: replace Streamlit with polished local web app
---
.github/CONTRIBUTING.md | 2 +-
.github/workflows/ci.yml | 76 +-
.github/workflows/release.yml | 18 +-
.gitignore | 5 -
.streamlit/config.toml | 7 -
CHANGELOG.md | 11 +
README.md | 55 +-
api_app.py | 44 +-
crypto_core.py | 13 +-
docs/API.md | 17 +
docs/RELEASE_CHECKLIST.md | 4 +-
docs/SCREENSHOTS.md | 24 +-
docs/SECURITY.md | 2 +-
docs/THREAT_MODEL.md | 2 +-
.../custom-web-encrypt-workflow.png | Bin 136505 -> 94974 bytes
.../screenshots/custom-web-mobile-inspect.png | Bin 70493 -> 49048 bytes
docs/screenshots/decrypt-file-workflow.jpg | Bin 63202 -> 0 bytes
docs/screenshots/encrypt-file-workflow.jpg | Bin 62132 -> 0 bytes
.../generate-keys-backend-warning.jpg | Bin 69833 -> 0 bytes
docs/screenshots/key-utilities-workflow.jpg | Bin 54640 -> 0 bytes
package-lock.json | 1397 +++++++++++++++--
package.json | 16 +-
pqc_app.py | 492 ------
pyproject.toml | 3 +-
requirements-dev-lock.txt | 672 +-------
requirements-lock.txt | 876 +----------
requirements.txt | 3 -
scripts/api_client.test.mjs | 72 +-
scripts/ui_native_smoke.mjs | 111 ++
scripts/ui_smoke.mjs | 375 ++++-
start.sh | 6 -
tests/test_api_app.py | 25 +
tests/test_crypto_core.py | 11 +
tests/test_startup.py | 43 +
tsconfig.json | 2 +-
ui_helpers.py | 2 +-
vite.config.ts | 8 +
web/src/App.tsx | 632 --------
web/src/{api.ts => api/client.ts} | 71 +-
web/src/api/contracts.ts | 68 +
web/src/api/errors.ts | 18 +
web/src/api/index.ts | 13 +
web/src/app/App.test.tsx | 130 ++
web/src/app/App.tsx | 84 +
web/src/app/AppShell.test.tsx | 59 +
web/src/app/AppShell.tsx | 99 ++
web/src/app/navigation.ts | 8 +
web/src/components/ActionButton.tsx | 14 +
web/src/components/CapabilityStatus.tsx | 60 +
web/src/components/FilePicker.tsx | 69 +
web/src/components/Notice.tsx | 18 +
web/src/components/PasswordField.tsx | 35 +
web/src/components/TechnicalDetails.tsx | 14 +
web/src/components/WorkflowLayout.tsx | 39 +
web/src/components/components.test.tsx | 83 +
.../features/decrypt/DecryptWorkflow.test.tsx | 306 ++++
web/src/features/decrypt/DecryptWorkflow.tsx | 298 ++++
.../features/encrypt/EncryptWorkflow.test.tsx | 238 +++
web/src/features/encrypt/EncryptWorkflow.tsx | 268 ++++
.../generate/GenerateKeysWorkflow.test.tsx | 257 +++
.../generate/GenerateKeysWorkflow.tsx | 223 +++
.../features/generate/passwordPolicy.test.ts | 35 +
web/src/features/generate/passwordPolicy.ts | 44 +
.../inspect/InspectKeyWorkflow.test.tsx | 33 +
.../features/inspect/InspectKeyWorkflow.tsx | 68 +
web/src/hooks/useKeyInspection.test.tsx | 131 ++
web/src/hooks/useKeyInspection.ts | 80 +
web/src/lib/download.ts | 19 +
web/src/lib/filenames.test.ts | 20 +
web/src/lib/filenames.ts | 19 +
web/src/lib/format.test.ts | 10 +
web/src/lib/format.ts | 13 +
web/src/lib/workflow.test.ts | 16 +
web/src/lib/workflow.ts | 6 +
web/src/main.tsx | 6 +-
web/src/styles.css | 564 -------
web/src/styles/components.css | 609 +++++++
web/src/styles/global.css | 29 +
web/src/styles/tokens.css | 50 +
web/src/test/fixtures.ts | 26 +
web/src/test/setup.ts | 7 +
81 files changed, 5786 insertions(+), 3497 deletions(-)
delete mode 100644 .streamlit/config.toml
delete mode 100644 docs/screenshots/decrypt-file-workflow.jpg
delete mode 100644 docs/screenshots/encrypt-file-workflow.jpg
delete mode 100644 docs/screenshots/generate-keys-backend-warning.jpg
delete mode 100644 docs/screenshots/key-utilities-workflow.jpg
delete mode 100644 pqc_app.py
create mode 100644 scripts/ui_native_smoke.mjs
create mode 100644 tests/test_startup.py
delete mode 100644 web/src/App.tsx
rename web/src/{api.ts => api/client.ts} (77%)
create mode 100644 web/src/api/contracts.ts
create mode 100644 web/src/api/errors.ts
create mode 100644 web/src/api/index.ts
create mode 100644 web/src/app/App.test.tsx
create mode 100644 web/src/app/App.tsx
create mode 100644 web/src/app/AppShell.test.tsx
create mode 100644 web/src/app/AppShell.tsx
create mode 100644 web/src/app/navigation.ts
create mode 100644 web/src/components/ActionButton.tsx
create mode 100644 web/src/components/CapabilityStatus.tsx
create mode 100644 web/src/components/FilePicker.tsx
create mode 100644 web/src/components/Notice.tsx
create mode 100644 web/src/components/PasswordField.tsx
create mode 100644 web/src/components/TechnicalDetails.tsx
create mode 100644 web/src/components/WorkflowLayout.tsx
create mode 100644 web/src/components/components.test.tsx
create mode 100644 web/src/features/decrypt/DecryptWorkflow.test.tsx
create mode 100644 web/src/features/decrypt/DecryptWorkflow.tsx
create mode 100644 web/src/features/encrypt/EncryptWorkflow.test.tsx
create mode 100644 web/src/features/encrypt/EncryptWorkflow.tsx
create mode 100644 web/src/features/generate/GenerateKeysWorkflow.test.tsx
create mode 100644 web/src/features/generate/GenerateKeysWorkflow.tsx
create mode 100644 web/src/features/generate/passwordPolicy.test.ts
create mode 100644 web/src/features/generate/passwordPolicy.ts
create mode 100644 web/src/features/inspect/InspectKeyWorkflow.test.tsx
create mode 100644 web/src/features/inspect/InspectKeyWorkflow.tsx
create mode 100644 web/src/hooks/useKeyInspection.test.tsx
create mode 100644 web/src/hooks/useKeyInspection.ts
create mode 100644 web/src/lib/download.ts
create mode 100644 web/src/lib/filenames.test.ts
create mode 100644 web/src/lib/filenames.ts
create mode 100644 web/src/lib/format.test.ts
create mode 100644 web/src/lib/format.ts
create mode 100644 web/src/lib/workflow.test.ts
create mode 100644 web/src/lib/workflow.ts
delete mode 100644 web/src/styles.css
create mode 100644 web/src/styles/components.css
create mode 100644 web/src/styles/global.css
create mode 100644 web/src/styles/tokens.css
create mode 100644 web/src/test/fixtures.ts
create mode 100644 web/src/test/setup.ts
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 67bcc0a..9896c7a 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -84,7 +84,7 @@ By participating in this project, you agree to abide by our Code of Conduct.
- Run mypy to check static typing:
```bash
.venv/bin/python -m mypy \
- crypto_config.py crypto_core.py pqc_agent_tools.py pqc_app.py \
+ api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py ui_helpers.py \
tests/test_agent_tools.py tests/test_crypto_core.py
```
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5eb54a4..83539df 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,13 +32,13 @@ jobs:
run: python -m pip install -r requirements-dev.txt
- name: Check formatting
- run: python -m black --check api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py setup.py tests/test_agent_tools.py tests/test_api_app.py tests/test_crypto_core.py tests/test_ui_helpers.py
+ run: python -m black --check api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py ui_helpers.py setup.py tests/test_agent_tools.py tests/test_api_app.py tests/test_crypto_core.py tests/test_ui_helpers.py
- name: Lint
- run: python -m flake8 api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py setup.py tests/test_agent_tools.py tests/test_api_app.py tests/test_crypto_core.py tests/test_ui_helpers.py
+ run: python -m flake8 api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py ui_helpers.py setup.py tests/test_agent_tools.py tests/test_api_app.py tests/test_crypto_core.py tests/test_ui_helpers.py
- name: Type check
- run: python -m mypy api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py tests/test_agent_tools.py tests/test_api_app.py tests/test_crypto_core.py tests/test_ui_helpers.py
+ run: python -m mypy api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py ui_helpers.py tests/test_agent_tools.py tests/test_api_app.py tests/test_crypto_core.py tests/test_ui_helpers.py
- name: Unit tests without native liboqs
run: >
@@ -93,15 +93,21 @@ jobs:
/tmp/qe-wheel-test/bin/quantum-encryptor-agent health --json
/tmp/qe-wheel-test/bin/python -I - <<'PY'
from pathlib import Path
+ from importlib.metadata import requires
+ from importlib.util import find_spec
import sys
import api_app
import pqc_agent_tools
+ import ui_helpers
prefix = Path(sys.prefix).resolve()
- for module in (api_app, pqc_agent_tools):
+ for module in (api_app, pqc_agent_tools, ui_helpers):
module_path = Path(module.__file__).resolve()
assert module_path.is_relative_to(prefix), (module.__name__, module_path, prefix)
+ assert find_spec("pqc_app") is None
+ package_requirements = requires("quantum-encryptor") or ()
+ assert all(not requirement.lower().startswith("streamlit") for requirement in package_requirements)
PY
- name: Smoke installed web UI
@@ -155,7 +161,7 @@ jobs:
run: npm audit --package-lock-only --audit-level=high
- name: Run Python security lint
- run: python -m bandit -q -r api_app.py crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py
+ run: python -m bandit -q -r api_app.py crypto_core.py pqc_agent_tools.py ui_helpers.py
web:
name: Custom web UI
@@ -262,9 +268,9 @@ jobs:
run: |
git init .ci/liboqs
git -C .ci/liboqs remote add origin https://github.com/open-quantum-safe/liboqs.git
- # liboqs 0.15.0
- git -C .ci/liboqs fetch --depth=1 origin 97f6b86b1b6d109cfd43cf276ae39c2e776aed80
- git -C .ci/liboqs checkout --detach 97f6b86b1b6d109cfd43cf276ae39c2e776aed80
+ # liboqs 0.16.0
+ git -C .ci/liboqs fetch --depth=1 origin 5a1a854b0dc9f2141bdc771c555ee60c37950183
+ git -C .ci/liboqs checkout --detach 5a1a854b0dc9f2141bdc771c555ee60c37950183
cmake -S .ci/liboqs -B .ci/liboqs/build \
-GNinja \
-DBUILD_SHARED_LIBS=ON \
@@ -273,7 +279,22 @@ jobs:
cmake --install .ci/liboqs/build
- name: Install Python dependencies
- run: python -m pip install -r requirements-dev.txt
+ run: python -m pip install --require-hashes -r requirements-dev-lock.txt
+
+ - name: Set up Node
+ uses: actions/setup-node@v7
+ with:
+ node-version: "22"
+ cache: npm
+
+ - name: Install frontend dependencies
+ run: npm ci
+
+ - name: Build frontend
+ run: npm run build
+
+ - name: Install Playwright Chromium
+ run: npx playwright install --with-deps chromium
- name: Verify native backend is active
run: |
@@ -314,3 +335,40 @@ jobs:
--private-key agent-private.pem \
--output agent-output.txt
cmp agent-input.txt agent-output.txt
+
+ - name: Native browser encryption round trip
+ run: |
+ SKIP_WEB_BUILD=1 PYTHON=python ./start.sh &
+ server_pid=$!
+ trap 'kill "$server_pid" 2>/dev/null || true; wait "$server_pid" 2>/dev/null || true' EXIT
+ python - <<'PY'
+ import json
+ import time
+ from urllib.request import urlopen
+
+ required = ("generate", "encrypt", "decrypt")
+ deadline = time.monotonic() + 30
+
+ while (remaining := deadline - time.monotonic()) > 0:
+ try:
+ with urlopen("http://127.0.0.1:4000/api/health", timeout=min(1, remaining)) as response:
+ health = json.load(response)
+ except Exception:
+ health = None
+
+ ready = bool(
+ health
+ and health.get("ok")
+ and health.get("backendReady")
+ and all(health.get("capabilities", {}).get(name, {}).get("available") for name in required)
+ )
+ if ready:
+ raise SystemExit(0)
+
+ remaining = deadline - time.monotonic()
+ if remaining > 0:
+ time.sleep(min(0.25, remaining))
+
+ raise SystemExit("Native local app did not become ready within 30 seconds.")
+ PY
+ npm run ui-native
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 815f3c2..3bc944b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -44,7 +44,7 @@ jobs:
run: python -m pip_audit -r requirements-dev-lock.txt
- name: Run Python security lint
- run: python -m bandit -q -r crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py api_app.py
+ run: python -m bandit -q -r crypto_core.py pqc_agent_tools.py ui_helpers.py api_app.py
- name: Install frontend dependencies
run: npm ci
@@ -82,15 +82,21 @@ jobs:
/tmp/qe-release-test/bin/quantum-encryptor-agent health --json
/tmp/qe-release-test/bin/python -I - <<'PY'
from pathlib import Path
+ from importlib.metadata import requires
+ from importlib.util import find_spec
import sys
import api_app
import pqc_agent_tools
+ import ui_helpers
prefix = Path(sys.prefix).resolve()
- for module in (api_app, pqc_agent_tools):
+ for module in (api_app, pqc_agent_tools, ui_helpers):
module_path = Path(module.__file__).resolve()
assert module_path.is_relative_to(prefix), (module.__name__, module_path, prefix)
+ assert find_spec("pqc_app") is None
+ package_requirements = requires("quantum-encryptor") or ()
+ assert all(not requirement.lower().startswith("streamlit") for requirement in package_requirements)
PY
- name: Smoke installed web UI
@@ -177,9 +183,9 @@ jobs:
run: |
git init .ci/liboqs
git -C .ci/liboqs remote add origin https://github.com/open-quantum-safe/liboqs.git
- # liboqs 0.15.0
- git -C .ci/liboqs fetch --depth=1 origin 97f6b86b1b6d109cfd43cf276ae39c2e776aed80
- git -C .ci/liboqs checkout --detach 97f6b86b1b6d109cfd43cf276ae39c2e776aed80
+ # liboqs 0.16.0
+ git -C .ci/liboqs fetch --depth=1 origin 5a1a854b0dc9f2141bdc771c555ee60c37950183
+ git -C .ci/liboqs checkout --detach 5a1a854b0dc9f2141bdc771c555ee60c37950183
cmake -S .ci/liboqs -B .ci/liboqs/build \
-GNinja \
-DBUILD_SHARED_LIBS=ON \
@@ -188,7 +194,7 @@ jobs:
cmake --install .ci/liboqs/build
- name: Install Python dependencies
- run: python -m pip install -r requirements-dev.txt
+ run: python -m pip install --require-hashes -r requirements-dev-lock.txt
- name: Verify migration backends are active
run: |
diff --git a/.gitignore b/.gitignore
index 8cb241b..aba43b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,11 +108,6 @@ ENV/
env.bak/
venv.bak/
-# Streamlit local state and secrets. Keep committed UI theme config.
-.streamlit/*
-!.streamlit/
-!.streamlit/config.toml
-
# Spyder project settings
.spyderproject
.spyproject
diff --git a/.streamlit/config.toml b/.streamlit/config.toml
deleted file mode 100644
index d76f1b1..0000000
--- a/.streamlit/config.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-[theme]
-base = "dark"
-primaryColor = "#2DD4BF"
-backgroundColor = "#000000"
-secondaryBackgroundColor = "#111827"
-textColor = "#FFFFFF"
-font = "sans serif"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 349bea6..efd4998 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,17 @@ This project follows a practical semantic-versioning style.
- ML-KEM-768 + X25519 composite key generation and format-v4 encrypted containers.
- SHA3-256 hybrid key combiner binding both key shares, X25519 context, suite identifier, and application domain.
+- Polished monochrome local web workflows with progressive technical details,
+ responsive navigation, component tests, accessibility checks, and a native
+ browser encryption round trip.
+
+### Changed
+
+- Retired the Streamlit application, temporary startup fallback, packaging
+ surface, and superseded reference screenshots. `./start.sh` now serves the
+ React/Vite interface through the loopback-only Python API as the sole GUI.
+- Preserved decrypt-only compatibility for authenticated earlier containers and
+ private-key formats, including the bounded ML-KEM/Kyber migration path.
### Security
diff --git a/README.md b/README.md
index 127b1a6..947f795 100644
--- a/README.md
+++ b/README.md
@@ -10,14 +10,14 @@ A post-quantum cryptography tool for file encryption. New files combine ML-KEM-7
- Dark local web interface for ML-KEM-768 + X25519 key generation, file encryption, decryption, and PEM key inspection. + Monochrome local web interface for ML-KEM-768 + X25519 key generation, file encryption, decryption, and PEM key inspection.
## Features @@ -25,12 +25,12 @@ A post-quantum cryptography tool for file encryption. New files combine ML-KEM-7 - **Post-Quantum/Traditional Security**: Combines ML-KEM-768 with X25519 so confidentiality does not depend on one key-establishment algorithm - **Authenticated File Encryption**: Derives AES-256-GCM keys from both ML-KEM and X25519 shared secrets - **Password-Protected Keys**: Private keys are always encrypted with scrypt-derived AES-256-GCM keys -- **User-Friendly Interface**: Custom local web UI with a Python ASGI API +- **User-Friendly Interface**: Custom local web UI with progressive technical details and a Python ASGI API - **PEM Key Format**: Keys stored in PEM-like format with quantum algorithm extensions ## Screenshots -The backend readiness warning shown here is expected when native `liboqs` is not installed in the local environment. Click any image to open the full screenshot page. +The current browser smoke captures show the responsive Encrypt and Inspect key workflows. Click either image to open the full screenshot page.
@@ -111,21 +111,22 @@ See [docs/SCREENSHOTS.md](docs/SCREENSHOTS.md) for the dedicated screenshot page
PYTHON=.venv/bin/python ./test.sh
```
- To run the legacy Streamlit UI during transition:
- ```bash
- LEGACY_STREAMLIT=1 ./start.sh
- ```
-
Frontend development can run Vite separately on `127.0.0.1:4001`:
```bash
npm run dev
```
-2. Open the web interface in your browser. You can:
- - Generate a new post-quantum key pair
- - Encrypt files using a recipient's public key
- - Decrypt files using your private key
- - Access key utilities
+2. Open the web interface in your browser. Choose the intent that matches your task:
+ - **Encrypt**: protect a file for the holder of a recipient public key.
+ - **Decrypt**: recover a file with the matching encrypted private key and password.
+ - **Generate keys**: create a new public key and password-protected private key.
+ - **Inspect key**: check supported key metadata without exposing key material.
+
+ Each workflow starts with plain-language guidance. Expand **Technical details** only when you need suite, format, or key-policy information.
+
+### Local-only interface privacy
+
+The custom interface processes selected files through the local Python service at `127.0.0.1`. It does not use browser persistence, telemetry, remote fonts, or remote application assets. The UI does not display plaintext previews, passwords, private-key content, or the local API token.
## Verification
@@ -138,8 +139,9 @@ Run the Python test suite:
Run the custom frontend checks:
```bash
-npm run build
+npm run test:unit
npm run check
+npm run build
```
With the app already running on `127.0.0.1:4000`, run the browser smoke test:
@@ -150,16 +152,24 @@ npm run ui-smoke
The UI smoke test writes ignored screenshots under `tmp/ui-smoke/`.
+When a native `liboqs` installation is available to the running app, also run the real browser encryption round trip:
+
+```bash
+npm run ui-native
+```
+
+Do not treat the browser smoke test as proof that the native cryptographic backend is installed; it verifies the built interface against the local API contract. `npm run ui-native` verifies key generation, encryption, and decryption through the available native backend.
+
### Key Generation
-1. Select "Generate Keys" from the sidebar
+1. Select "Generate keys" from the workflow navigation
2. Enter and confirm a strong private-key password
3. Generate the keys and download both public and private key files
4. Share your public key with others who want to send you encrypted files
### File Encryption
-1. Select "Encrypt File" from the sidebar
+1. Select "Encrypt" from the workflow navigation
2. Upload the file you want to encrypt
3. Upload the recipient's public key (.pem file)
4. Specify the output filename
@@ -167,15 +177,15 @@ The UI smoke test writes ignored screenshots under `tmp/ui-smoke/`.
### File Decryption
-1. Select "Decrypt File" from the sidebar
+1. Select "Decrypt" from the workflow navigation
2. Upload the encrypted file (.pqc file)
3. Upload your private key (.pem file)
4. Enter your private-key password
5. Download the decrypted file
-## Agent Usage
+## Automation Usage
-Local automation agents can use the deterministic JSON CLI instead of driving the Streamlit UI. Run commands from the repository workspace and pass only workspace-relative paths. Absolute paths, `..` traversal, symlink escapes, and accidental output overwrites are rejected.
+Automation tools can use the deterministic JSON CLI instead of driving the browser interface. Run commands from the repository workspace and pass only workspace-relative paths. Absolute paths, `..` traversal, symlink escapes, and accidental output overwrites are rejected.
```bash
mkdir -p keys data
@@ -229,7 +239,7 @@ The CLI prints JSON only and never includes plaintext, private keys, passwords,
- State-changing local web API requests require a per-process API token and reject non-local browser origins when an `Origin` header is present
- The local agent CLI accepts only workspace-relative paths, returns machine-readable JSON without secret material, and writes private keys plus decrypted outputs with owner-only permissions on POSIX systems; non-overwrite output creation uses exclusive file creation
- Native `liboqs` is loaded lazily and missing backend support disables key generation/encryption instead of crashing the app
-- CI runs Python formatting, linting, type checks, unit tests, custom web UI build/type checks, API client tests, browser UI smoke, isolated installed-wheel checks, Python/npm dependency audits, locked runtime install, and a native `liboqs` integration test job pinned to the matching 0.15.0 release commit; repository CodeQL default setup provides static analysis
+- CI runs Python formatting, linting, type checks, unit tests, custom web UI build/type checks, API client tests, browser UI smoke, isolated installed-wheel checks, Python/npm dependency audits, locked runtime install, and a native `liboqs` integration test job pinned to the matching 0.16.0 release commit; repository CodeQL default setup provides static analysis
- See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) for repository trust boundaries, assets, abuse cases, and invariants
- **Disclaimer**: This software has not undergone an independent security audit and should be reviewed before production use
@@ -238,8 +248,7 @@ The CLI prints JSON only and never includes plaintext, private keys, passwords,
- `crypto_config.py` - Configuration parameters for cryptographic operations
- `crypto_core.py` - Core cryptographic functions (key generation, encryption, decryption)
- `api_app.py` - Local ASGI API and static web UI server
-- `pqc_agent_tools.py` - Local JSON CLI for agentic workflows
-- `pqc_app.py` - Legacy Streamlit web application interface
+- `pqc_agent_tools.py` - Local JSON CLI for automation workflows
- `web/` - React frontend source for the custom UI
- `package.json` / `vite.config.ts` - Frontend build configuration
- `ui_helpers.py` - UI-safe filename helpers
diff --git a/api_app.py b/api_app.py
index 8d9826c..465d1d4 100644
--- a/api_app.py
+++ b/api_app.py
@@ -299,23 +299,53 @@ async def _read_upload_text(upload: UploadFile, max_bytes: int, label: str) -> s
raise ApiError(400, "invalid_text", f"{label} must be a UTF-8 text file.") from exc
+def _capability(available: bool, reason: str = "") -> dict[str, bool | str]:
+ return {"available": available, "reason": "" if available else reason}
+
+
def _health_payload() -> dict[str, Any]:
try:
active_kem_component = core.resolve_kem_algorithm(cfg.KEM_ALG)
- backend_ready = True
- backend_message = "Post-quantum backend ready."
+ current_backend_ready = True
except Exception as exc:
active_kem_component = cfg.KEM_ALG
- backend_ready = False
- backend_message = (
+ current_backend_ready = False
+ logger.warning("Post-quantum backend readiness check failed: %s", exc)
+
+ try:
+ decrypt_ready = bool(core.available_decryption_kem_algorithms())
+ except Exception as exc:
+ decrypt_ready = False
+ logger.warning("Decryption backend readiness check failed: %s", exc)
+
+ capabilities = {
+ "inspect": _capability(True),
+ "generate": _capability(
+ current_backend_ready,
+ "ML-KEM-768 is unavailable for new key generation.",
+ ),
+ "encrypt": _capability(
+ current_backend_ready,
+ "ML-KEM-768 is unavailable for new encryption.",
+ ),
+ "decrypt": _capability(
+ decrypt_ready,
+ "No supported post-quantum decryption backend is available.",
+ ),
+ }
+ backend_message = (
+ "Post-quantum backend ready."
+ if current_backend_ready
+ else (
"The ML-KEM backend is not ready, so new keys and ciphertexts cannot be created. "
- "Compatible legacy archives may still be decryptable."
+ "Compatible encrypted archives may still be decryptable."
)
- logger.warning("Post-quantum backend readiness check failed: %s", exc)
+ )
return {
- "backendReady": backend_ready,
+ "backendReady": current_backend_ready,
"backendMessage": backend_message,
+ "capabilities": capabilities,
"formatVersion": cfg.FORMAT_VERSION,
"kem": cfg.HYBRID_KEM_ALG,
"kemComponent": active_kem_component,
diff --git a/crypto_core.py b/crypto_core.py
index 737dcb9..9c5aff1 100644
--- a/crypto_core.py
+++ b/crypto_core.py
@@ -220,6 +220,16 @@ def resolve_kem_algorithm(kem_alg: Optional[str] = None) -> str:
)
+def available_decryption_kem_algorithms() -> Tuple[str, ...]:
+ """Return enabled, application-supported KEM identities in preference order."""
+ enabled = set(_enabled_kem_mechanisms())
+ return tuple(
+ candidate
+ for candidate in (cfg.KEM_ALG, *cfg.LEGACY_KEM_ALGS)
+ if candidate in enabled
+ )
+
+
def resolve_decryption_kem_algorithms(suite: str) -> Tuple[str, ...]:
"""Resolve exact KEM identities allowed to decrypt a key/container suite."""
if suite == cfg.HYBRID_KEM_ALG:
@@ -229,8 +239,7 @@ def resolve_decryption_kem_algorithms(suite: str) -> Tuple[str, ...]:
if suite != cfg.LEGACY_HYBRID_KEM_ALG:
raise UnsupportedAlgorithmError(f"Unsupported key or container suite: {suite!r}")
- enabled = set(_enabled_kem_mechanisms())
- candidates = tuple(candidate for candidate in (cfg.KEM_ALG, *cfg.LEGACY_KEM_ALGS) if candidate in enabled)
+ candidates = available_decryption_kem_algorithms()
if candidates:
return candidates
raise CryptoDependencyError(
diff --git a/docs/API.md b/docs/API.md
index b6c27f9..0ea8946 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -39,6 +39,23 @@ The custom web UI is served by `api_app.py` on `127.0.0.1` by default. `GET /api
When a browser sends an `Origin` header on a state-changing API request, the server only accepts local origins beginning with `http://127.0.0.1:` or `http://localhost:`. Requests without the local API token are rejected before route handlers parse uploaded files or form data.
+### Health capability contract
+
+`GET /api/health` includes additive, operation-specific readiness information. New clients should use these capabilities to decide whether to enable each workflow:
+
+```
+{
+ "capabilities": {
+ "inspect": { "available": true, "reason": "" },
+ "generate": { "available": true, "reason": "" },
+ "encrypt": { "available": true, "reason": "" },
+ "decrypt": { "available": true, "reason": "" }
+ }
+}
+```
+
+`backendReady` and `backendMessage` remain in the response for compatibility, while new clients use operation-specific capabilities. A capability `reason` is a safe user-facing summary of an unavailable operation; it is not a raw backend exception. The health response is marked `Cache-Control: no-store` and `Pragma: no-cache` because it includes the per-process local API token.
+
### Agent JSON Contract
Successful command output:
diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md
index 9c96c51..f5283a1 100644
--- a/docs/RELEASE_CHECKLIST.md
+++ b/docs/RELEASE_CHECKLIST.md
@@ -26,8 +26,8 @@ Use this checklist before publishing a new release.
python -m pip install -r requirements-dev.txt
npm ci
python -m black --check .
-python -m flake8 api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py setup.py tests
-python -m mypy api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py pqc_app.py ui_helpers.py tests
+python -m flake8 api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py ui_helpers.py setup.py tests
+python -m mypy api_app.py crypto_config.py crypto_core.py pqc_agent_tools.py ui_helpers.py tests
./test.sh --cov=crypto_core --cov=pqc_agent_tools --cov=ui_helpers --cov=api_app --cov-report=term-missing --cov-fail-under=80
npm run build
npm run check
diff --git a/docs/SCREENSHOTS.md b/docs/SCREENSHOTS.md
index 29f7949..7acda34 100644
--- a/docs/SCREENSHOTS.md
+++ b/docs/SCREENSHOTS.md
@@ -1,31 +1,11 @@
# Application Screenshots
-These screenshots show the custom local web UI. They were captured from the local app with native `liboqs` unavailable, so the backend readiness warning and disabled key generation or file processing controls are expected.
+These screenshots show the current monochrome custom local web UI. The interface uses progressive disclosure: plain-language task guidance appears first, while suite, format, and key-policy information stays inside **Technical details** until it is needed. The desktop Encrypt capture shows format version 4 and the `ML-KEM-768+X25519-v2` hybrid suite; the mobile Inspect key capture shows responsive workflow navigation and public-key metadata only.
## Custom Web Encrypt Workflow
-
+
## Custom Web Mobile Inspect

-
-## Legacy Streamlit Reference
-
-The default `./start.sh` path serves the custom local web UI. These older screenshots remain as a reference for the legacy Streamlit interface available through `LEGACY_STREAMLIT=1 ./start.sh`.
-
-### Generate Keys
-
-
-
-### Encrypt File
-
-
-
-### Decrypt File
-
-
-
-### Key Utilities
-
-
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index 08941fd..2c5a62b 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -76,7 +76,7 @@ The application is designed to protect against the following threats:
- Legacy hybrid public keys are rejected for encryption and must be regenerated under the current suite.
- The UI and core encryption path enforce a 100 MiB plaintext in-memory file limit; decryption accepts only the bounded encrypted-container size needed for header, KEM ciphertext, nonce, and tag overhead.
- The local web API requires a per-process `X-Quantum-Encryptor-Token` for state-changing `/api/*` requests and rejects non-local browser origins when an `Origin` header is present.
-- Download filenames are reduced to local filenames before being passed to Streamlit.
+- Download filenames are reduced to local filenames before the API returns them to the browser.
- Private-key password protection requires at least 16 characters, at least 5 unique characters, and rejects known weak values in the core, UI, and agent CLI.
- Unencrypted private keys are rejected in the core, UI, and agent CLI.
- The core module defines its own logger but leaves root logging configuration to application entry points.
diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md
index e0825b8..c70c3ab 100644
--- a/docs/THREAT_MODEL.md
+++ b/docs/THREAT_MODEL.md
@@ -15,7 +15,7 @@ Quantum Encryptor protects local files with post-quantum key encapsulation and a
## Trust Boundaries
-- Streamlit uploads are untrusted user-controlled files.
+- Browser uploads to the local web API are untrusted user-controlled files.
- Agent CLI arguments and environment variables are untrusted automation inputs.
- Public/private PEM files and `.pqc` files are attacker-controlled until parsed and authenticated.
- Native `liboqs` and Python `cryptography` are trusted dependencies but may be unavailable or misconfigured.
diff --git a/docs/screenshots/custom-web-encrypt-workflow.png b/docs/screenshots/custom-web-encrypt-workflow.png
index 2b36c3dbda0eba28f0cd358a9197d3a49f41bd05..9a423ed3b85b59c02abca9078d0208584352d930 100644
GIT binary patch
literal 94974
zcmd43Wmr{v^eqf3pdcUuqNIS5B1%e2Bc0Nq(jeW9AW{M%rF5rscStDRAl(g{?zoe4
zJpcE-&vU=rZ+CrRvtjMM)-UE9bBr;UpRBYfCORQH5)u;TD={H?BqWpsB&6$;H?P4v
zww!c$NJw{(UJ3ClI>f9`plB!=kfCk)Y?_5j-x5|-yu+1WWzf2wyJFMNk#5ErTanq%
zQB_ri%YTDEg(#(J>awPF;?U_9^6oFhX+@QDm-UH8{pnd3M|X&lf`Seb>(`u|ocMUM
z>+n@@T!iZX`vM7x+sEVYn@A4W$Y_7x-Xgt*_4nm8&kf?gFYBFezWDpn de{_(Jz0(zUwhqWs%5G!OMqM+TJ+XmiBf<1%>NG
zYZQ!(mr~SmWnmsf(+UksM0Eg_9KskI@w|gR5b&R^zw5JO8T}kKkmF-QjPF0PzN^n?
zc60(i?nC~rtM)CryH9CM!R!S6=PE238$Z!?xpy#Vm7MwB-TU2LIuKA-efZEAc?PHq
zKiQ1mp3>K^x6NBn{$mm$_(ixRm*+4!=vV-NIg-VT`0-OjUJy-hDgW`C@Vl}|@WgKY
z5ej$Eyofq}IpC7v;Z-Ly4{%s7*0{wsg;!wE2+hvT
zRR|aw-bW#pbRhqflf!`f2`kLZLWeeN20fP3(F%nzFFnayNJyy9>h|s1x({scV8(Ax%$D@AvQDeuN_c`f=kw
z&0;H&3knJzkl!ZZ{8dqld)foZsp#M^Jaz4KZ>3^C4^y2jFPTXyf4b_AqRP%(
$Hy
;fSV6GHQ(aM9<+nT6(dDHdVqK>FbyqBNGB`yE2n+~s-@2u_
zd-|!bo!0I2NH!`tAp
?A0O#c`63y)a^;!&rB4WThzQ|2IpU5M7xfi-5#tY3D_xiBp?Gpy?9Z*`H6(!p
zeLs`PGbKW66px1r
Wp<6rtHbF#ADE%U>%`Zq4eOm)cP#~keJSMe>P
zSQTiHG#vNt7$Bv?W
k!n=4;JoWO