Skip to content

fix(auth): provider keys sealed with a publicly-known dev key by default — fail closed at startup #76

Description

@hasitpbhatt

Summary

packages/auth/encryption.py:46-47 seals every provider credential with sha256(b"orcarouter-lite-dev-key") when CREDENTIAL_ENCRYPTION_KEY is unset:

# Dev fallback so test fixtures and `docker compose up` Just Work.
return hashlib.sha256(b"orcarouter-lite-dev-key").digest()

Nothing in the shipped configuration sets a real key — app/config.py defaults it to "", .env.example leaves it commented out (and falsely claims it is "auto-generated on first run"), and docker-compose.yml sets only DATABASE_URL. The documented docker compose up path therefore encrypts every BYOK key with a constant from public source, silently. Anyone who obtains /data/orca.db decrypts every upstream API key in one line.

Secondary issues in the same module:

  • Any non-hex string is stretched into a key with a single unsalted SHA-256 (billions of guesses/sec offline).
  • Ciphertext has no version byte ⇒ rotating the key permanently bricks every stored credential as an uncaught InvalidTag.
  • No warning is ever logged when the fallback is active.

Proposed fix

  1. Versioned ciphertext — v1 format b"\x01" + nonce(12) + ct+tag, with transparent legacy-blob decryption (including the ~0.4% of legacy blobs whose first nonce byte collides with \x01). Round-trips stay compatible; rotation becomes possible later.
  2. Loud warning on first dev-fallback use (insecure_dev_encryption_key, once per process).
  3. Fail-closed startup guard (packages/db.guards.assert_credential_encryption_ready, wired into the lifespan after create_all): refuse to boot when the dev key would protect real credentials — i.e. any existing provider_keys rows, or any non-SQLite database — unless the operator sets ORCA_ALLOW_INSECURE_DEV_KEY=1 explicitly. Fresh SQLite installs keep working with the loud warning.
  4. Fix the stale "auto-generated on first run" comment in config.py; document openssl rand -hex 32.

Deliberately out of scope (follow-ups): AAD binding of ciphertext to (provider, row_id), KDF hardening for passphrase-style keys (changing derivation would brick existing stored keys), automated key-rotation migration.

Acceptance criteria

  • Fresh SQLite install with no provider keys still boots without configuration.
  • Boot refuses (RuntimeError) when provider rows exist or the DB is non-SQLite and no key is configured; ORCA_ALLOW_INSECURE_DEV_KEY=1 overrides.
  • New ciphertexts are version-prefixed; legacy unversioned blobs (including nonce-byte-collision ones) still decrypt.
  • Wrong-key decrypt raises InvalidTag; truncated blobs raise rather than return garbage.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions