pediatric-ai-scribe-v3/src/utils/passwords.js
Daniel cb17a12172 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

52 lines
1.8 KiB
JavaScript

// ============================================================
// Unified password hashing — argon2id preferred, bcrypt fallback
// Transparent migration: on successful login with a bcrypt hash,
// the row is rehashed as argon2id and updated in place.
// ============================================================
var bcrypt = require('bcryptjs');
var argon2 = null;
try {
argon2 = require('argon2');
} catch (e) {
// argon2 optional — install with `npm install argon2` to enable.
// Until then, registration + change-password fall back to bcrypt(12).
console.warn('[passwords] argon2 not installed — using bcrypt only. Install argon2 to enable migration.');
}
var BCRYPT_ROUNDS = 12;
var ARGON2_OPTS = {
type: 2, // argon2id
memoryCost: 19456, // 19 MiB — OWASP 2023 recommended
timeCost: 2,
parallelism: 1
};
function isArgon2Hash(h) { return typeof h === 'string' && h.indexOf('$argon2') === 0; }
function isBcryptHash(h) { return typeof h === 'string' && /^\$2[aby]\$/.test(h); }
async function hash(password) {
if (argon2) return argon2.hash(password, ARGON2_OPTS);
return bcrypt.hash(password, BCRYPT_ROUNDS);
}
async function verify(password, storedHash) {
if (!storedHash) return false;
if (isArgon2Hash(storedHash)) {
if (!argon2) throw new Error('argon2 hash encountered but argon2 package not installed');
return argon2.verify(storedHash, password);
}
if (isBcryptHash(storedHash)) {
return bcrypt.compare(password, storedHash);
}
return false;
}
// Returns a new argon2 hash if migration is desired, else null.
async function maybeRehash(password, storedHash) {
if (!argon2) return null;
if (isArgon2Hash(storedHash)) return null; // already argon2
return argon2.hash(password, ARGON2_OPTS);
}
module.exports = { hash: hash, verify: verify, maybeRehash: maybeRehash, hasArgon2: function(){ return !!argon2; } };