pediatric-ai-scribe-v3/src/utils/redact.js
Daniel 263bd1a1c7 Security hardening: PHI encryption, argon2, DOMPurify, SRI
- App-layer AES-256-GCM crypto helper (src/utils/crypto.js)
- Nextcloud tokens encrypted at rest; transparent migration on next use
- Audio backups encrypted at rest (version byte 0x01 envelope); legacy
  rows still decrypt as-is until overwritten
- argon2id password hashing via src/utils/passwords.js with bcrypt
  fallback; bcrypt hashes rehashed to argon2id on next successful login.
  argon2 package is optional — server keeps running with bcrypt only
  until npm install adds the native dep
- PHI redactor for audit log details (src/utils/redact.js) — strips SSN,
  phone, email, DoB, long IDs; caps at 500 chars; detects note bodies
- DOMPurify (cdnjs, SRI-pinned) replaces custom regex sanitizer in
  Learning Hub content rendering
- SRI integrity hashes added for Font Awesome CSS and Chart.js
- Magic-byte file-type verification on document uploads
  (src/utils/fileType.js)
- Generic 500 error responses via src/utils/errors.js applied to
  nextcloud and audioBackups; full detail still logged server-side
- DATA_ENCRYPTION_KEY env documented in .env.example

Deploy: requires rebuild of the container image to pick up the new
files and `npm install` (adds argon2). Existing users keep working
because bcrypt stays available and crypto helpers pass through
plaintext when the key is not yet set in dev.
2026-04-14 02:49:38 +02:00

34 lines
1.3 KiB
JavaScript

// ============================================================
// PHI redactor for audit log details
// ============================================================
// Audit details should describe *what happened*, not contain clinical
// text. This defensively strips obvious PHI patterns and caps length.
// ============================================================
var MAX_LEN = 500;
function redact(text) {
if (text == null) return text;
var s = typeof text === 'string' ? text : JSON.stringify(text);
// SSN
s = s.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN]');
// US phone numbers
s = s.replace(/\b\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g, '[PHONE]');
// Email addresses (except domain-only)
s = s.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[EMAIL]');
// Dates of birth / MRN-ish long digit runs
s = s.replace(/\b\d{2}\/\d{2}\/\d{4}\b/g, '[DATE]');
s = s.replace(/\b\d{8,}\b/g, '[ID]');
// Note-body signal: many newlines or very long prose → indicates the
// caller passed a clinical note as `details`. Truncate aggressively.
var newlines = (s.match(/\n/g) || []).length;
if (newlines > 4 || s.length > MAX_LEN * 2) {
s = s.slice(0, 120) + ' [TRUNCATED:possible-note-body]';
}
if (s.length > MAX_LEN) s = s.slice(0, MAX_LEN) + '…';
return s;
}
module.exports = { redact: redact };