- 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.
34 lines
1.3 KiB
JavaScript
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 };
|