- 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.
52 lines
1.8 KiB
JavaScript
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; } };
|