Description
A CRITICAL security vulnerability exists in `backend/secuscan/config.py` at line 94 and `backend/secuscan/vault.py` at lines 22–31. When `SECUSCAN_VAULT_KEY` is not set in the environment, the application silently falls back to a hardcoded string `"secuscan-dev-key"` that is publicly visible in the source code. Additionally, the `VaultCrypto` class uses a homemade XOR stream cipher that cycles a 32-byte SHA-256 keystream — any secret longer than 32 characters is encrypted with a repeating keystream, making it trivially breakable via crib-dragging.
Impact
Any attacker who can read the SQLite database file (backup, shared volume, accidental leak) can decrypt every row in the `credential_vault` table using the publicly known default key. Even users who set a custom key are protected by a broken cipher that repeats its keystream every 32 bytes — a classic Vigenère-style weakness that can be defeated with frequency analysis for sufficiently long secrets.
Steps to Reproduce
- Clone the repo and run `./setup.sh` without setting `SECUSCAN_VAULT_KEY` in `.env`.
- Store any secret via `PUT /api/v1/vault/my-secret`.
- Obtain the SQLite file at `backend/data/secuscan.db` (or any backup).
- Run the following script (all logic is derived from public source code):
```python
import base64, hashlib, hmac
from itertools import cycle
seed = "secuscan-dev-key"
raw_key = base64.urlsafe_b64encode(hashlib.sha256(seed.encode()).digest())
blob = base64.urlsafe_b64decode(STOLEN_CIPHERTEXT)
nonce, sig, ciphertext = blob[:16], blob[16:48], blob[48:]
stream_key = hashlib.sha256(raw_key + nonce).digest()
plaintext = bytes(b ^ k for b, k in zip(ciphertext, cycle(stream_key))).decode()
print(plaintext) # Secret recovered
```
- Observed result: the plaintext secret is recovered in full without knowing any user-supplied credential.
Expected Behaviour
The application should refuse to start if `SECUSCAN_VAULT_KEY` is not explicitly configured. Vault encryption should use a standard AEAD cipher (AES-256-GCM) that provides proper confidentiality regardless of secret length.
Proposed Fix
- Remove the `"secuscan-dev-key"` fallback in `config.py:94` — raise `ValueError` at startup if neither `SECUSCAN_VAULT_KEY` nor a secure alternative is provided.
- Replace the custom XOR cipher in `vault.py` with AES-256-GCM using the `cryptography` package (already a transitive dependency of `uvicorn[standard]`).
- Add `cryptography` to `backend/requirements.txt` as an explicit dependency.
Labels: `type:security` `level:advanced` `gssoc:approved`
Please assign this issue to me under GSSoC 2026. I will open a PR with a complete fix covering all affected files, proper test coverage, and verification steps.
Description
A CRITICAL security vulnerability exists in `backend/secuscan/config.py` at line 94 and `backend/secuscan/vault.py` at lines 22–31. When `SECUSCAN_VAULT_KEY` is not set in the environment, the application silently falls back to a hardcoded string `"secuscan-dev-key"` that is publicly visible in the source code. Additionally, the `VaultCrypto` class uses a homemade XOR stream cipher that cycles a 32-byte SHA-256 keystream — any secret longer than 32 characters is encrypted with a repeating keystream, making it trivially breakable via crib-dragging.
Impact
Any attacker who can read the SQLite database file (backup, shared volume, accidental leak) can decrypt every row in the `credential_vault` table using the publicly known default key. Even users who set a custom key are protected by a broken cipher that repeats its keystream every 32 bytes — a classic Vigenère-style weakness that can be defeated with frequency analysis for sufficiently long secrets.
Steps to Reproduce
```python
import base64, hashlib, hmac
from itertools import cycle
seed = "secuscan-dev-key"
raw_key = base64.urlsafe_b64encode(hashlib.sha256(seed.encode()).digest())
blob = base64.urlsafe_b64decode(STOLEN_CIPHERTEXT)
nonce, sig, ciphertext = blob[:16], blob[16:48], blob[48:]
stream_key = hashlib.sha256(raw_key + nonce).digest()
plaintext = bytes(b ^ k for b, k in zip(ciphertext, cycle(stream_key))).decode()
print(plaintext) # Secret recovered
```
Expected Behaviour
The application should refuse to start if `SECUSCAN_VAULT_KEY` is not explicitly configured. Vault encryption should use a standard AEAD cipher (AES-256-GCM) that provides proper confidentiality regardless of secret length.
Proposed Fix
Labels: `type:security` `level:advanced` `gssoc:approved`
Please assign this issue to me under GSSoC 2026. I will open a PR with a complete fix covering all affected files, proper test coverage, and verification steps.