VirtualPR/backend/src/services/cryptoPii.js

125 lines
4.2 KiB
JavaScript

import crypto from 'node:crypto';
import { env } from '../config/env.js';
function keyMaterial() {
const raw = env.PII_ENCRYPTION_KEY;
if (!raw) {
if (env.NODE_ENV !== 'development') {
throw new Error('Set PII_ENCRYPTION_KEY (32-byte base64) outside development.');
}
return crypto.createHash('sha256').update(`dev-pii|${env.JWT_SECRET}`).digest();
}
const fromB64 = Buffer.from(raw, 'base64');
if (fromB64.length === 32) return fromB64;
return crypto.createHash('sha256').update(raw).digest();
}
function pepper() {
if (env.PII_PEPPER) return env.PII_PEPPER;
if (env.NODE_ENV !== 'development') throw new Error('Set PII_PEPPER outside development.');
return `dev-pepper|${env.JWT_SECRET}`;
}
export function isCipherPayload(value) {
return Boolean(value && typeof value === 'object' && value.v && value.iv && value.tag && value.ct);
}
export function encryptPii(plain) {
if (plain == null || plain === '') return plain;
const text = String(plain);
const iv = crypto.randomBytes(12);
const key = keyMaterial();
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return {
v: 1,
kid: env.PII_KEY_ID || 'k1',
iv: iv.toString('base64'),
tag: tag.toString('base64'),
ct: ct.toString('base64'),
};
}
export function decryptPii(value) {
if (value == null || value === '') return value == null ? value : '';
if (typeof value === 'number') return value;
if (typeof value === 'string') return value;
if (!isCipherPayload(value)) return typeof value === 'object' ? '' : value;
try {
const key = keyMaterial();
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(value.iv, 'base64'));
decipher.setAuthTag(Buffer.from(value.tag, 'base64'));
const plain = Buffer.concat([decipher.update(Buffer.from(value.ct, 'base64')), decipher.final()]);
return plain.toString('utf8');
} catch (err) {
console.error('[pii] decrypt failed', err.message);
return '';
}
}
export function hashMobile(mobile) {
const digits = String(mobile || '').replace(/\D/g, '');
if (!digits) return '';
return crypto.createHmac('sha256', pepper()).update(digits).digest('hex');
}
export function hashName(name) {
const normalized = String(name || '').trim().toLowerCase();
if (!normalized) return '';
return crypto.createHmac('sha256', pepper()).update(normalized).digest('hex');
}
export function isMobileNumber(mobile) {
return /^\d{10}$/.test(String(mobile || '').trim());
}
export function applyOnboardedPii(target, fields) {
if (fields.name != null) {
target.name = encryptPii(fields.name);
target.nameHash = hashName(fields.name);
}
if (fields.mobile != null) {
target.mobile = encryptPii(fields.mobile);
target.mobileHash = hashMobile(fields.mobile);
}
if (fields.age !== undefined) {
target.age = fields.age == null || fields.age === '' ? null : encryptPii(String(fields.age));
}
if (fields.gender !== undefined) target.gender = fields.gender ? encryptPii(fields.gender) : '';
if (fields.lungCondition !== undefined) {
target.lungCondition = fields.lungCondition ? encryptPii(fields.lungCondition) : '';
}
if (fields.otherHealthCondition !== undefined) {
target.otherHealthCondition = fields.otherHealthCondition ? encryptPii(fields.otherHealthCondition) : '';
}
return target;
}
export function readOnboardedPii(item) {
if (!item || typeof item !== 'object') {
return { name: '', mobile: '', age: null, gender: '', lungCondition: '', otherHealthCondition: '' };
}
const agePlain = decryptPii(item.age);
const age = agePlain == null || agePlain === '' ? null : Number(agePlain);
return {
name: decryptPii(item.name) || '',
mobile: decryptPii(item.mobile) || '',
age: Number.isNaN(age) ? null : age,
gender: decryptPii(item.gender) || '',
lungCondition: decryptPii(item.lungCondition) || '',
otherHealthCondition: decryptPii(item.otherHealthCondition) || '',
};
}
export function encryptSecret(value) {
if (!value) return '';
if (isCipherPayload(value)) return value;
return encryptPii(value);
}
export function decryptSecret(value) {
if (!value) return '';
return decryptPii(value) || '';
}