Secure Portal QA Checklist
This checklist documents the security engineering verification for the HSDI Secure Encrypted Case Submission Portal. It covers crypto correctness, absence of false security claims, link integrity, threat model coverage, and schema completeness. Items marked PARTIAL have disclosed residual risks. Items marked INTEGRATION POINT are explicitly not yet implemented and are labeled as such in the UI.
Crypto Correctness
6 itemsSubmission payload encrypted with AES-GCM 256-bit before transmission
Evidence: Web Crypto API: crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }). Key exported as raw hex and shown once to user. Ciphertext transmitted; key never sent to server.
Passphrase-derived key uses PBKDF2 with 310,000 iterations and random 16-byte salt
Evidence: deriveKeyFromPassphrase(): PBKDF2, SHA-256, 310,000 iterations, crypto.getRandomValues(16-byte salt). Salt stored server-side as base64 for re-derivation.
Status token has 256-bit entropy; only SHA-256 hash stored server-side
Evidence: Server: crypto.randomBytes(32) → hex token shown to user once. SHA-256 hash stored in DB. Token never stored in plaintext.
Notification channel encrypted client-side with AES-GCM 256-bit
Evidence: Fixed in audit pass v2. Previously used btoa() (Base64 only — not encryption). Now uses crypto.subtle.generateKey + encrypt. Key embedded in stored record; HSDI cannot read without submitter cooperation.
Identity hash is reproducible from original statement (no timestamp salt)
Evidence: Fixed in audit pass v2. Previously hashed identityStatement + Date.now() — making hash non-reproducible. Now hashes identityStatement only via SHA-256, enabling evidentiary verification.
IV (initialization vector) is unique per encryption operation
Evidence: crypto.getRandomValues(new Uint8Array(12)) called fresh for each encryptPayload() and encryptString() invocation.
No False Security Claims
7 itemsTor compatibility is not claimed; limitations disclosed in UI
Evidence: LimitationsPanel: 'This portal does not guarantee Tor compatibility. The server receives the IP address of the last network hop.' Displayed before user can proceed.
Blockchain anchoring labeled as integration point, not implemented
Evidence: LimitationsPanel: 'INTEGRATION POINT — not yet implemented.' Verified pathway form: 'Blockchain anchoring is an integration point only and is not yet active.' Both locations consistent.
TLS metadata leakage disclosed (timing, payload size, IP)
Evidence: LimitationsPanel: 'the platform operator can observe traffic metadata (timing, payload size, IP addresses) but not the plaintext content.'
Court order risk disclosed for IP hash
Evidence: LimitationsPanel: 'This does not prevent a court order compelling the platform operator to provide server logs.'
No claim of 'end-to-end encryption' (which would imply server-side decryption capability)
Evidence: UI uses 'client-side encryption' throughout. The phrase 'end-to-end encryption' does not appear in the portal. The distinction is correct: the server stores ciphertext; only the submitter holds the key.
No claim of 'SecureDrop-like anonymity' or 'Tor-grade anonymity'
Evidence: No such claims appear in the UI. Anonymous pathway is labeled 'MAX PRIVACY' with explicit caveats about Tor and IP hashing limitations.
Notification channel encryption claim matches implementation
Evidence: Fixed in audit pass v2. UI now accurately describes AES-GCM encryption with locally generated key. Warning added that HSDI cannot read the channel without submitter cooperation.
Link Integrity
3 itemsStatus check link in 'done' step points to /portal-status
Evidence: Fixed in audit pass v2. Previously pointed to /secure-portal?tab=status (broken). Now correctly links to /portal-status.
Portal Status page (/portal-status) is routed and accessible without login
Evidence: App.tsx: <Route path='/portal-status' component={PortalStatus} />. Page uses publicProcedure (trpc.securePortal.checkStatus). No auth guard.
Navbar links to /secure-portal and /portal-status are present and correct
Evidence: Navbar.tsx Governance group: 'Secure Portal' → /secure-portal, 'Submission Status' → /portal-status.
Threat Model
8 itemsContent interception in transit: mitigated
Evidence: AES-GCM 256-bit client-side encryption + TLS. Server sees only ciphertext.
Residual risk: Low — server sees only ciphertext.
Identity disclosure via IP: partially mitigated
Evidence: SHA-256 IP hashing with server-side salt. Raw IP not written to disk.
Residual risk: Medium — court order can compel server logs. Tor Browser use advised for high-risk submitters.
Traffic metadata analysis: no application-layer mitigation
Evidence: Disclosed in LimitationsPanel. No padding or timing obfuscation implemented.
Residual risk: Medium — traffic analysis possible. Tor Browser use advised.
Retaliation via submission content: mitigated
Evidence: Content encrypted; server cannot read it without submitter's key.
Residual risk: Low if key is protected.
Spam / abuse: rate limited
Evidence: 5 submissions per IP per hour. 20 status checks per IP per 15 minutes.
Residual risk: Low.
Token brute-force: negligible risk
Evidence: 256-bit entropy tokens. Only SHA-256 hash stored server-side.
Residual risk: Negligible.
Key loss: high residual risk; user warned
Evidence: Key shown once with prominent red warning. Download and copy options provided. Acknowledgement checkbox required before submission. No server-side recovery possible.
Residual risk: High if user loses key — no recovery mechanism exists.
Blockchain anchoring: integration point only
Evidence: Not yet implemented. Verified submissions receive server timestamp attestation only. Blockchain anchoring will be labeled clearly when available.
Residual risk: Evidentiary weight of verified submissions is limited to server timestamp until blockchain integration is active.
Schema Completeness
3 itemsSecureSubmission table: encryptedPayload, encryptionIv, encryptionSalt, pathway, status, submissionRef, statusTokenHash, tokenExpiresAt, identityHash, attestationRef, consentLegalHold, consentResearch, encryptedNotificationChannel, ipHash, rateLimitBucket
Evidence: drizzle/schema.ts: secureSubmissions table with all required fields.
PortalAuditLog table: submissionId, action, actorType, actorId, metadata, timestamp
Evidence: drizzle/schema.ts: portalAuditLog table.
PortalConsentFlag table: submissionId, consentType, grantedAt, revokedAt
Evidence: drizzle/schema.ts: portalConsentFlags table.