feat: implement enterprise RBAC, profile identity, system management, and audit stabilization
This commit is contained in:
105
backend/src/lib/security.ts
Normal file
105
backend/src/lib/security.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import crypto from "crypto";
|
||||
import { env } from "../config/env";
|
||||
|
||||
const PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*";
|
||||
|
||||
type EncryptedEnvelope = {
|
||||
__enc: "v1";
|
||||
iv: string;
|
||||
tag: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
function getEncryptionKey() {
|
||||
const seed = env.SETTINGS_ENCRYPTION_KEY ?? env.JWT_SECRET;
|
||||
return crypto.createHash("sha256").update(seed).digest();
|
||||
}
|
||||
|
||||
function normalizeSecret(value: string) {
|
||||
return crypto.createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
export function generateSecurePassword(length = 20) {
|
||||
const bytes = crypto.randomBytes(length * 2);
|
||||
let output = "";
|
||||
for (let i = 0; i < bytes.length && output.length < length; i += 1) {
|
||||
output += PASSWORD_ALPHABET[bytes[i] % PASSWORD_ALPHABET.length];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function hashToken(token: string) {
|
||||
return normalizeSecret(token);
|
||||
}
|
||||
|
||||
export function timingSafeEqualHash(candidate: string, storedHash: string) {
|
||||
const candidateHash = Buffer.from(normalizeSecret(candidate), "utf8");
|
||||
const knownHash = Buffer.from(storedHash, "utf8");
|
||||
if (candidateHash.length !== knownHash.length) return false;
|
||||
return crypto.timingSafeEqual(candidateHash, knownHash);
|
||||
}
|
||||
|
||||
export function generateRecoveryCodes(count = 8) {
|
||||
const codes: string[] = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const raw = crypto.randomBytes(5).toString("hex").toUpperCase();
|
||||
codes.push(`${raw.slice(0, 5)}-${raw.slice(5, 10)}`);
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
export function hashRecoveryCodes(codes: string[]) {
|
||||
return codes.map((code) => normalizeSecret(code.trim().toUpperCase()));
|
||||
}
|
||||
|
||||
export function consumeRecoveryCode(input: string, hashes: string[]) {
|
||||
const normalized = normalizeSecret(input.trim().toUpperCase());
|
||||
const matchIndex = hashes.findIndex((hash) => hash === normalized);
|
||||
if (matchIndex < 0) {
|
||||
return { matched: false, remainingHashes: hashes };
|
||||
}
|
||||
const remainingHashes = [...hashes.slice(0, matchIndex), ...hashes.slice(matchIndex + 1)];
|
||||
return { matched: true, remainingHashes };
|
||||
}
|
||||
|
||||
function isEncryptedEnvelope(value: unknown): value is EncryptedEnvelope {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
(value as Record<string, unknown>).__enc === "v1" &&
|
||||
typeof (value as Record<string, unknown>).iv === "string" &&
|
||||
typeof (value as Record<string, unknown>).tag === "string" &&
|
||||
typeof (value as Record<string, unknown>).data === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export function encryptJson(value: unknown): EncryptedEnvelope {
|
||||
const key = getEncryptionKey();
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
||||
const payload = Buffer.from(JSON.stringify(value), "utf8");
|
||||
const encrypted = Buffer.concat([cipher.update(payload), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
__enc: "v1",
|
||||
iv: iv.toString("base64"),
|
||||
tag: tag.toString("base64"),
|
||||
data: encrypted.toString("base64")
|
||||
};
|
||||
}
|
||||
|
||||
export function decryptJson<T = unknown>(value: unknown): T {
|
||||
if (!isEncryptedEnvelope(value)) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
const key = getEncryptionKey();
|
||||
const iv = Buffer.from(value.iv, "base64");
|
||||
const tag = Buffer.from(value.tag, "base64");
|
||||
const encrypted = Buffer.from(value.data, "base64");
|
||||
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
||||
return JSON.parse(decrypted) as T;
|
||||
}
|
||||
Reference in New Issue
Block a user