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_plaincolumn 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
gcloudcall, 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_hashcolumn is the canonical authentication credential — login verifies against it viabcrypt.comparein constant time. - Password material is never logged.
- A separate
password_plaincolumn 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 viaGET /api/users/:idto 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_SECRETis mandatory at boot: the server inserver/src/middleware/auth.jsexits non-zero if the variable is unset. There is no development fallback. Tests that need a value set one explicitly.JWT_SECRETis sourced from GCP Secret Manager (secret nameclaim-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 ofclaim-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.1only. It does not leave the VM. Documented as a deliberate design choice indocs/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 forhttps://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.
safeFetchenforces 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 theuserstable 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.