Skip to content

Cryptography

Statement

ClaimGuard relies on modern, well-vetted cryptographic primitives for the three places it holds or transmits sensitive material: user passwords, authentication tokens, and the secrets and data the application processes. Specifically:

  • Passwords are hashed with bcrypt at cost factor 12 before storage. Plaintext passwords are never logged. A separate password_plain column exists today as part of an admin password-recovery feature inherited from earlier prototyping; this is flagged as a known gap below and slated for removal — see Authentication for the implications and remediation plan.
  • Authentication tokens are JWTs signed with HMAC-SHA256 using a ≥32-byte secret loaded from GCP Secret Manager at boot. The server refuses to start without a valid JWT_SECRET.
  • Secrets at rest are stored in GCP Secret Manager, encrypted with Google-managed keys and accessed via the VM's least-privilege service account.
  • Data at rest on GCP persistent disks and Cloud Storage buckets is encrypted by default with AES-256 under Google-managed keys.
  • Cloud-control-plane traffic (every gcloud call, every Secret Manager fetch, every GCS access) uses TLS with Google-issued certificates.

The one place the encryption story is currently incomplete is public user-facing TLS: traffic to the application's API still terminates on the VM's open TCP port. Closing that gap is plan step A1.3 (HTTPS load balancer + Google-managed certificate) and is the reason the status of this control is partial.

Implementation

Password hashing

User passwords are hashed with bcrypt (cost factor 12) at registration and password-change time, and verified with bcrypt.compare at login. The cost factor is centralized as a single constant (BCRYPT_ROUNDS = 12 in server/src/routes/users.js).

  • The bcrypt digest in the password_hash column is the canonical authentication credential — login verifies against it via bcrypt.compare in constant time.
  • Password material is never logged.
  • A separate password_plain column was introduced for an admin password-recovery feature in an earlier phase of the product. It stores the plaintext alongside the bcrypt hash and is exposed via GET /api/users/:id to admins within the same org and to super-admins for any org. This is a known gap — slated for removal so the bcrypt-only story is the actual story; see Authentication for the remediation plan and impact.

The 12-round cost factor is the OWASP-recommended default as of 2026 and gives roughly 250 ms of CPU time per hash on the production VM, which is the right trade-off between brute-force resistance and login latency for an interactive product.

JWT signing

User authentication tokens are JSON Web Tokens (RFC 7519) signed with HMAC-SHA256 (the jsonwebtoken library default) using a key loaded from JWT_SECRET.

  • JWT_SECRET is mandatory at boot: the server in server/src/middleware/auth.js exits non-zero if the variable is unset. There is no development fallback. Tests that need a value set one explicitly.
  • JWT_SECRET is sourced from GCP Secret Manager (secret name claim-guard-jwt-secret) — see Secrets management.
  • Token lifetime defaults to 24 hours (JWT_EXPIRY=24h), configurable per environment.
  • Tokens are stateless — there is no server-side session store. Logout is client-side token disposal.

Asymmetric signing (RS256 / EdDSA) is not currently used. Moving to asymmetric signing would let multiple verifiers (e.g., a future API gateway, a future tools-service) verify tokens without sharing the signing key; tracked as a roadmap item.

Secrets at rest

Runtime secrets — JWT_SECRET, DATABASE_URL, GOOGLE_API_KEY — are stored in GCP Secret Manager. Google encrypts secret payloads at rest with AES-256 under Google-managed keys. The application reads them at boot through the VM's attached service account (claim-guard-vm@train-cvit2.iam.gserviceaccount.com), which holds roles/secretmanager.secretAccessor per secret. See Secrets management for the full bootstrap path and audit trail.

Data at rest

  • VM persistent disk (deepfakebench3, the boot disk of claim-guard-app-1): GCP encrypts every persistent disk by default with AES-256 under a Google-managed key. We do not currently use a customer-managed key (CMEK).
  • Snapshots of the boot disk inherit the same encryption. See Backups.
  • GCS buckets (the application's image-upload buckets used by the c2pa tool): encrypted at rest under Google-managed keys.
  • Postgres on the VM: data files reside on the encrypted boot disk; the database itself does not perform per-row encryption (no TDE). Application-layer column encryption for sensitive PII is not currently in place; see Roadmap.

Data in transit

  • Internal traffic on the VM (Node backend ↔ Python tools) goes over 127.0.0.1 only. It does not leave the VM. Documented as a deliberate design choice in docs/security/PROTOCOL.md §4.
  • GCP control-plane traffic (Secret Manager, Cloud Logging, GCS, Compute APIs) uses TLS with Google-issued certificates, validated against the system trust store. The application has no plaintext outbound paths to GCP.
  • Outbound HTTP to operator-configured destinations (webhooks, upstream APIs) is wrapped by safeFetch (server/src/lib/safeFetch.js), which enforces TLS for https:// URLs, blocks private/link-local/CGNAT ranges, and refuses redirects by default. See SAST for the full invariant set.
  • Public user-facing traffic to the application API currently reaches the VM on a non-TLS port (3001) directly on the public IP. This is the gap closing in plan step A1.3.

Algorithm choices summary

Use Algorithm Notes
Password hashing bcrypt cost-12 OWASP-recommended baseline.
JWT signing HMAC-SHA256 (HS256) jsonwebtoken library default; ≥32-byte secret.
Disk + GCS at rest AES-256-GCM (Google-managed) GCP default.
Secret Manager at rest AES-256 (Google-managed) GCP default.
TLS for outbound TLS 1.2+ (system trust store) Node.js / Python defaults.
Public user-facing TLS planned via Google-managed cert (A1.3) Current gap.

Status

partial — verified 2026-04-29.

What's in place:

  • bcrypt-12 for passwords, end-to-end.
  • JWT HS256 with mandatory secret bootstrap and Secret Manager backing.
  • Secrets at rest in Secret Manager.
  • GCP at-rest encryption for disks, snapshots, GCS, and Secret Manager.
  • TLS for all GCP control-plane traffic.
  • safeFetch enforces TLS for outbound application HTTP.

Known gaps

  • Public user-facing TLS — still waiting on A1.3 (HTTPS LB + managed cert). Until that lands, browsers connect to the application over plain HTTP on port 3001. Mitigated only by the fact that the product is not yet in customer hands.
  • Plaintext password column (password_plain) — present in the users table to support admin password recovery. Documented in detail in Authentication. Tracked for removal; the doc names the migration steps. Until that ships, admins can read peers' plaintext passwords and a database compromise would expose them.
  • CMEK — no customer-managed encryption keys for compute, GCS, or Secret Manager. Tracked as a later compliance cycle item.
  • Application-layer encryption of sensitive PII columns in Postgres is not in place. Risk-rated low at current scale; revisit before the first regulated-customer onboarding.

Roadmap

  • A1.3 — Google-managed TLS certificate behind the application LB, closing the user-facing gap.
  • JWT to RS256 / EdDSA — asymmetric signing once we have more than one verifier.
  • CMEK — for snapshots, GCS, and Secret Manager. Larger effort; revisit at SOC 2 Type II / ISO time.
  • Column-level encryption for high-sensitivity fields, contingent on the first regulated-customer requirement.
  • JWT_SECRET rotation cadence — scheduled in plan step A2.5. Rotation invalidates every issued token, so it requires comms. The operational playbook (when, who, how, blast radius, recovery) is written up in the JWT_SECRET rotation runbook; the first rotation under that runbook lands at the next 90-day boundary or sooner on suspected leak.