Skip to content

fix(vault): replace XOR stream cipher with AES-256-GCM, remove hardcoded fallback key#279

Open
advikdivekar wants to merge 2 commits into
utksh1:mainfrom
advikdivekar:security/hardcoded-vault-key
Open

fix(vault): replace XOR stream cipher with AES-256-GCM, remove hardcoded fallback key#279
advikdivekar wants to merge 2 commits into
utksh1:mainfrom
advikdivekar:security/hardcoded-vault-key

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

What is the problem

Closes #200

The credential vault had two critical weaknesses:

  1. Broken cipher: VaultCrypto used a custom XOR stream cipher backed by a 32-byte keystream. Stream ciphers of this form are vulnerable to crib-dragging — an attacker with two ciphertexts encrypted under the same key can XOR them together and recover plaintext without knowing the key.

  2. Hardcoded fallback key: config.resolved_vault_key fell back to "secuscan-dev-key" when SECUSCAN_VAULT_KEY was unset. Every default-config deployment shared this key; any attacker who obtained a vault database could decrypt all credentials offline.

What was changed

File Change
backend/secuscan/vault.py Replaced XOR cipher with AES-256-GCM (cryptography.hazmat.primitives.ciphers.aead.AESGCM). Fresh 12-byte nonce per encrypt() call. GCM 16-byte auth tag provides authenticated encryption — tampering always raises ValueError.
backend/secuscan/config.py resolved_vault_key now raises RuntimeError when neither SECUSCAN_VAULT_KEY nor SECUSCAN_PLUGIN_SIGNATURE_KEY is set. Hardcoded "secuscan-dev-key" fallback removed.
backend/requirements.txt Added cryptography>=42.0.0
.env.example Marked SECUSCAN_VAULT_KEY as required with generation instructions
testing/backend/conftest.py Sets vault_key = "test-vault-key-for-unit-tests-only" in test environment
testing/backend/unit/test_vault_failure_messages.py Updated byte offsets to match AES-GCM blob layout (nonce(12) + ciphertext + auth_tag(16))
testing/backend/unit/test_vault_security.py 15 new tests: GCM nonce uniqueness, all tamper detection paths, wrong-key rejection, truncated blob handling, key configuration enforcement

Why this approach

  • AES-256-GCM is a standard AEAD cipher — confidentiality and integrity in one primitive, no separate HMAC needed
  • Random nonce per encrypt() prevents nonce reuse; GCM authentication tag makes any modification detectable
  • RuntimeError on missing key is a startup-time failure, not a silent security degradation
  • cryptography package is already widely used in the Python ecosystem; it ships with pre-built wheels and requires no system crypto libraries

How to test

# Generate a vault key
export SECUSCAN_VAULT_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")

# Start backend — should succeed
cd backend && python -m secuscan

# Unset the key — should raise RuntimeError on vault access
unset SECUSCAN_VAULT_KEY

Edge cases covered

  • Same plaintext produces different ciphertexts on each call (unique nonces)
  • Flipping any byte in nonce, ciphertext, or auth tag raises ValueError("integrity verification failed")
  • Wrong key always rejects
  • Truncated blob raises, not silently returns garbage
  • resolved_vault_key raises immediately when unconfigured (no lazy failures)
  • plugin_signature_key still accepted as a fallback for deployments that set it

Verification checklist

  • pytest testing/backend/ — 253 passed (1 pre-existing failure in test_route_rejects_task_when_limiter_full, confirmed failing on main too)
  • 15 new vault security tests — all pass
  • All existing vault roundtrip and failure-message tests — pass
  • VaultCrypto roundtrip with settings.resolved_vault_key — pass

…ded fallback key

The credential vault previously used a custom XOR stream cipher that could be
broken via crib-dragging attacks, and fell back to the hardcoded key
"secuscan-dev-key" when SECUSCAN_VAULT_KEY was unset — meaning every
default-config deployment shared the same encryption key.

Changes:
- vault.py: AES-256-GCM via cryptography.hazmat.primitives.ciphers.aead.AESGCM;
  fresh 12-byte nonce per encrypt(); 16-byte GCM auth tag provides integrity
- config.py: resolved_vault_key raises RuntimeError when neither vault_key nor
  plugin_signature_key is configured; hardcoded "secuscan-dev-key" fallback removed
- requirements.txt: add cryptography>=42.0.0
- .env.example: mark SECUSCAN_VAULT_KEY as required with generation instructions
- conftest.py: set vault_key in test environment so existing tests pass
- test_vault_failure_messages.py: update byte offsets to match AES-GCM blob layout
- test_vault_security.py: 15 new tests covering GCM properties, nonce uniqueness,
  tamper detection, and key configuration enforcement

Fixes utksh1#200 (hardcoded vault encryption key "secuscan-dev-key").
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b5e896b471

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread backend/secuscan/vault.py
Comment on lines +52 to +53
nonce = blob[: self._NONCE_LEN]
ciphertext = blob[self._NONCE_LEN :]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve backward decryption for existing vault records

This change switches the wire format to nonce(12) || ciphertext || tag(16) but decrypt() now unconditionally parses every stored blob with that layout, so any secret written before this commit (old format: 16-byte nonce + 32-byte HMAC + ciphertext) will fail authentication and become unreadable after upgrade. In environments with existing credential_vault rows, /vault/{name} will start returning decryption failures until each secret is manually re-entered; add legacy-format decryption or a migration path to avoid data loss on upgrade.

Useful? React with 👍 / 👎.

@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review it and I have asked to claim few issues, please assign those issues to me under GSSoC 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Hardcoded Vault Encryption Key "secuscan-dev-key" — All Stored Credentials Decryptable Offline

1 participant