xypriss-security
Version:
Advanced High-Performance Security Framework. Military-grade encryption, post-quantum resilience, and fortified data structures.
582 lines • 28.2 kB
JavaScript
;
/***************************************************************************
* XyPriss Security - Advanced Hyper-Modular Security Framework
*
* @author NEHONIX (Nehonix-Team - https://github.com/Nehonix-Team)
* @license Nehonix Open Source License (NOSL)
*
* Copyright (c) 2025 NEHONIX. All rights reserved.
****************************************************************************/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PasswordManager = void 0;
const crypto_1 = require("crypto");
const path_1 = require("path");
const Password_1 = require("./Password");
const Random_1 = require("./Random");
const PasswordMDict_1 = require("../mods/PasswordMDict");
const DEFAULT_STRENGTH_OPTIONS = {
minLength: 8,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSymbols: true,
preventRepeats: true,
preventSequences: true,
checkDictionary: true,
};
// ─── PasswordManager ──────────────────────────────────────────────────────────
/***************************************************************************
* ### PasswordManager
*
* A configurable, instance-based password manager.
* Unlike the static `Password` class, `PasswordManager` is instantiated
* once with all options pre-configured, so callers only need to pass
* the raw password string at call time.
*
* This is the recommended pattern for large-scale projects:
* configure once in a dedicated file, export the instance, and reuse
* everywhere without repeating options.
*
* @example
* // config/security.ts
* export const passwords = new PasswordManager({
* algorithm: "argon2id",
* memoryCost: 65536,
* parallelism: 4,
* iterations: 3,
* pepper: process.env.PASSWORD_PEPPER,
* });
*
* // anywhere in the app:
* const hash = await passwords.hash("user-raw-password");
* const valid = await passwords.verify("user-raw-password", hash);
* const temp = passwords.generate({ length: 24, symbols: true });
* const passphrase = passwords.generatePassphrase({ wordCount: 5 });
* const pin = passwords.generatePin(6);
* const info = passwords.strength("MyP@ss1!");
* const breach = await passwords.isBreached("hunter2");
* const stale = passwords.needsRehash(storedHash);
*/
class PasswordManager {
algo;
memoryCost;
iterations;
parallelism;
pepper;
strengthOptions;
/** Cached wordlist for the strength method (preloaded if checkDictionary is enabled) */
cachedStrengthWordlist;
/** Lazy cache for passphrase wordlists to avoid repeated disk reads */
passphraseWordlistCache = new Map();
constructor(options = {}) {
this.algo = (options.algorithm ?? "argon2id").toLowerCase();
this.memoryCost = options.memoryCost ?? 65536; // 64 MiB
this.iterations = options.iterations ?? 3;
this.parallelism = options.parallelism ?? 4;
this.pepper = options.pepper;
// Merge provided strength options with secure defaults
this.strengthOptions = {
...DEFAULT_STRENGTH_OPTIONS,
...(options.strength || {}),
};
if (this.strengthOptions.checkDictionary) {
this.cachedStrengthWordlist = (0, PasswordMDict_1.getWordlist)({ allowFallback: "silent" });
}
}
// ─── Hashing ───────────────────────────────────────────────────────────────
/**
* Hashes a password using the instance's pre-configured options.
*
* @param password - The plain-text password to hash.
* @param overrides - Optional per-call overrides for any constructor option.
* @returns The encoded hash string, ready to be stored.
*/
async hash(password, overrides = {}) {
const pepper = overrides.pepper ?? this.pepper;
const finalPassword = pepper ? password + pepper : password;
return Password_1.Password.hash(finalPassword, {
algorithm: overrides.algorithm ?? this.algo,
memoryCost: overrides.memoryCost ?? this.memoryCost,
iterations: overrides.iterations ?? this.iterations,
parallelism: overrides.parallelism ?? this.parallelism,
});
}
/**
* Verifies a plain-text password against a stored hash.
*
* @param password - The password to verify.
* @param hash - The stored hash to compare against.
* @param overrides - Optional per-call override for the pepper.
* @returns `true` if the password matches the hash, `false` otherwise.
*/
async verify(password, hash, overrides = {}) {
const pepper = overrides.pepper ?? this.pepper;
const finalPassword = pepper ? password + pepper : password;
return Password_1.Password.verify(finalPassword, hash);
}
/**
* Checks if a string is a valid XyPriss hash, optionally matching
* this manager's current algorithm.
*
* @param hash - The string to check.
* @param strict - If `true`, only returns `true` if the hash matches the manager's algorithm.
* @returns `true` if it's a valid hash, `false` otherwise.
*/
isHashed(hash, strict = true) {
return Password_1.Password.isHashed(hash, strict ? this.algo : undefined);
}
// ─── Generation ────────────────────────────────────────────────────────────
/**
* Generates a cryptographically secure random password matching the given
* criteria, with **guaranteed character-type coverage**.
*
* Security properties:
* - Every enabled character type appears **at least once** in the output.
* - `extra` characters are **injected at cryptographically random positions**
* rather than appended, ensuring they don't cluster at the end.
* - The final array is **Fisher-Yates shuffled** via `Random.Int` to remove
* any positional bias introduced during construction.
* - `length` is clamped to [8, 512] to prevent misuse.
* - Throws `RangeError` if no character type is enabled (empty charset).
*
* @param options - Character set and length configuration.
* @returns A plain-text randomly generated password string.
*
* @example
* const pwd = passwords.generate({ length: 24, symbols: true });
*/
generate(options = {}) {
const { uppercase = true, lowercase = true, numbers = true, symbols = true, excludeSimilar = false, extra = "", } = options;
// ── 1. Clamp & validate length ──────────────────────────────────────────
const length = Math.min(PasswordMDict_1.MAX_GENERATE_LENGTH, Math.max(PasswordMDict_1.MIN_GENERATE_LENGTH, Math.floor(options.length ?? 20)));
// ── 2. Build charset ────────────────────────────────────────────────────
let charset = "";
const mandatory = []; // at least one char from each active set
const addSet = (chars, enabled) => {
if (!enabled)
return;
const cleaned = excludeSimilar
? chars.replace(PasswordMDict_1.CHARSETS.similarChars, "")
: chars;
if (cleaned.length === 0)
return;
charset += cleaned;
// Reserve one guaranteed character from this set
mandatory.push(cleaned[Random_1.Random.Int(0, cleaned.length)]);
};
addSet(PasswordMDict_1.CHARSETS.lowercase, lowercase);
addSet(PasswordMDict_1.CHARSETS.uppercase, uppercase);
addSet(PasswordMDict_1.CHARSETS.numbers, numbers);
addSet(PasswordMDict_1.CHARSETS.symbols, symbols);
if (charset.length === 0) {
throw new RangeError("PasswordManager.generate: no character types enabled — charset is empty.");
}
// Guarantee at least one char per extra string (injected, not appended)
if (extra) {
for (const ch of extra) {
if (!charset.includes(ch))
charset += ch;
mandatory.push(ch);
}
}
// ── 3. Guard: mandatory set cannot exceed requested length ───────────────
if (mandatory.length > length) {
throw new RangeError(`PasswordManager.generate: requested length (${length}) is too short ` +
`to satisfy all enabled character types (need at least ${mandatory.length}).`);
}
// ── 4. Fill remaining slots from full charset ────────────────────────────
const arr = [...mandatory];
const remaining = length - arr.length;
for (let i = 0; i < remaining; i++) {
arr.push(charset[Random_1.Random.Int(0, charset.length)]);
}
// ── 5. Fisher-Yates shuffle (crypto-safe via Random.Int) ─────────────────
for (let i = arr.length - 1; i > 0; i--) {
const j = Random_1.Random.Int(0, i + 1);
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr.join("");
}
// ─── Passphrase Generation ─────────────────────────────────────────────────
/**
* Generates a **memorable, high-entropy passphrase** using a curated
* 256-word list (similar to Diceware / EFF methodology).
*
* Entropy: `log2(256^wordCount)` = `8 * wordCount` bits minimum.
* With 5 words: ~40 bits base + number suffix ≈ 53 bits total.
* With 6 words: ~48 bits base + number suffix ≈ 61 bits total.
*
* Security note: word selection uses `Random.Int` which must be backed
* by a CSPRNG (e.g. `crypto.randomInt`).
*
* @param options - Passphrase configuration.
* @returns A plain-text passphrase string.
*
* @example
* passwords.generatePassphrase({ wordCount: 5, separator: "-" });
* // → "Bold-Cave-Iron-Jump-Warm-4827"
*/
generatePassphrase(options = {}) {
const { wordCount = 5, separator = "-", capitalize = true, appendNumbers = true, variant = "large", dir, filePath, allowFallback = "silent", } = options;
if (wordCount < 3 || wordCount > 20) {
throw new RangeError("PasswordManager.generatePassphrase: wordCount must be between 3 and 20.");
}
// Resolve the bundled mods dir relative to this compiled file
// dist/src/core/PasswordManager.js → ../../mods (production)
// src/core/PasswordManager.ts → ../mods (dev/bun direct)
const modsDir = dir ??
(0, path_1.join)(__dirname, "..", "mods") +
// ts-source fallback: if __dirname ends with /core, ../mods resolves correctly
"";
// ── Cache optimization for repeated calls ──────────────────────────────
const cacheKey = `${modsDir}|${variant}|${filePath ?? ""}|${allowFallback}`;
let wordlist = this.passphraseWordlistCache.get(cacheKey);
if (!wordlist) {
wordlist = (0, PasswordMDict_1.getWordlist)({
dir: modsDir,
variant,
filePath,
allowFallback,
});
this.passphraseWordlistCache.set(cacheKey, wordlist);
}
const words = [];
for (let i = 0; i < wordCount; i++) {
let word = wordlist[Random_1.Random.Int(0, wordlist.length)];
if (capitalize) {
word = word.charAt(0).toUpperCase() + word.slice(1);
}
words.push(word);
}
if (appendNumbers) {
// 4-digit suffix adds ~13 extra bits of entropy
const suffix = String(Random_1.Random.Int(1000, 9999));
words.push(suffix);
}
return words.join(separator);
}
// ─── PIN Generation ────────────────────────────────────────────────────────
/**
* Generates a cryptographically secure numeric PIN.
*
* Each digit is drawn independently from the full [0-9] range.
* The PIN is **zero-padded** to the requested length and returned as a
* string to preserve leading zeros (e.g. "0472").
*
* @param length - Number of digits. Must be between 4 and 32. @default 6
* @returns A plain-text numeric PIN string.
*
* @example
* passwords.generatePin(6); // → "047291"
*/
generatePin(length = 6) {
if (!Number.isInteger(length) || length < 4 || length > 32) {
throw new RangeError("PasswordManager.generatePin: length must be an integer between 4 and 32.");
}
const digits = [];
for (let i = 0; i < length; i++) {
digits.push(String(Random_1.Random.Int(0, 10)));
}
return digits.join("");
}
// ─── Strength Analysis ─────────────────────────────────────────────────────
/**
* Evaluates the strength of a password and returns a detailed report.
*
* The score is computed from multiple orthogonal criteria:
* - Length (up to 30 points)
* - Character variety (up to 50 points)
* - Absence of repetitions and sequences (up to 20 points deducted)
*
* @param password - The password to evaluate.
* @returns A `PasswordStrengthResult` with score, label, and actionable suggestions.
*
* @example
* const info = passwords.strength("MyP@ssw0rd!");
* console.log(info.score, info.label); // 82 "strong"
*/
strength(password) {
const suggestions = [];
// ── Characteristic flags ────────────────────────────────────────────────
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumbers = /[0-9]/.test(password);
const hasSymbols = /[^A-Za-z0-9]/.test(password);
const hasRepeats = /(.)\1{2,}/.test(password);
const hasSequences = /(abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password);
// ── Length score (0-30) ─────────────────────────────────────────────────
const len = password.length;
let lengthScore = 0;
if (len >= 8)
lengthScore += 10;
if (len >= 12)
lengthScore += 10;
if (len >= 16)
lengthScore += 5;
if (len >= 20)
lengthScore += 5;
// ── Variety score (0-50) ────────────────────────────────────────────────
let varietyScore = 0;
if (hasLowercase)
varietyScore += 10;
if (hasUppercase)
varietyScore += 10;
if (hasNumbers)
varietyScore += 10;
if (hasSymbols)
varietyScore += 15;
if (hasLowercase && hasUppercase && hasNumbers && hasSymbols)
varietyScore += 5;
// ── Penalty score (0..20 subtracted) ───────────────────────────────────
let penaltyScore = 0;
if (hasRepeats)
penaltyScore += 10;
if (hasSequences)
penaltyScore += 10;
// ── Entropy & Crack Time Estimation ────────────────────────────────────
let charsetSize = 0;
if (hasLowercase)
charsetSize += 26;
if (hasUppercase)
charsetSize += 26;
if (hasNumbers)
charsetSize += 10;
if (hasSymbols)
charsetSize += 32;
const entropy = charsetSize > 0 ? Math.round(len * Math.log2(charsetSize)) : 0;
// Assuming 1 billion offline guesses per second for crack time
const crackTimeSeconds = Math.pow(2, entropy) / 1_000_000_000;
let crackTimeDisplay = "Instantly";
if (crackTimeSeconds > 3153600000)
crackTimeDisplay = "Centuries";
else if (crackTimeSeconds > 31536000)
crackTimeDisplay = `${Math.round(crackTimeSeconds / 31536000)} years`;
else if (crackTimeSeconds > 2592000)
crackTimeDisplay = `${Math.round(crackTimeSeconds / 2592000)} months`;
else if (crackTimeSeconds > 86400)
crackTimeDisplay = `${Math.round(crackTimeSeconds / 86400)} days`;
else if (crackTimeSeconds > 3600)
crackTimeDisplay = `${Math.round(crackTimeSeconds / 3600)} hours`;
else if (crackTimeSeconds > 60)
crackTimeDisplay = `${Math.round(crackTimeSeconds / 60)} minutes`;
else if (crackTimeSeconds > 1)
crackTimeDisplay = `${Math.round(crackTimeSeconds)} seconds`;
let score = Math.max(0, Math.min(100, lengthScore + varietyScore - penaltyScore));
// ── Suggestions (Dynamic based on configuration) ───────────────────────
// We will populate suggestions during the custom rules evaluation below.
// ── Dictionary Check (if configured) ──────────────────────────────────
let hasDictionaryWord = false;
if (this.strengthOptions?.checkDictionary) {
const lowerPwd = password.toLowerCase();
const words = this.cachedStrengthWordlist || (0, PasswordMDict_1.getWordlist)({ allowFallback: "silent" });
// Only check words longer than 3 chars to avoid false positives on small substrings
if (words.some((w) => w.length > 3 && lowerPwd.includes(w))) {
hasDictionaryWord = true;
score = Math.max(0, score - 20); // Severe penalty for dictionary words
suggestions.push("Avoid using common dictionary words.");
}
}
// ── Label ──────────────────────────────────────────────────────────────
let label;
if (score < 20)
label = "very-weak";
else if (score < 40)
label = "weak";
else if (score < 60)
label = "fair";
else if (score < 80)
label = "strong";
else
label = "very-strong";
let isValid = true;
// ── Apply custom rules & populate suggestions ──────────────────────────
if (this.strengthOptions) {
if (this.strengthOptions.minLength && len < this.strengthOptions.minLength) {
isValid = false;
suggestions.push(`Use at least ${this.strengthOptions.minLength} characters.`);
}
if (this.strengthOptions.maxLength && len > this.strengthOptions.maxLength) {
isValid = false;
suggestions.push(`Password must not exceed ${this.strengthOptions.maxLength} characters.`);
}
if (this.strengthOptions.requireUppercase && !hasUppercase) {
isValid = false;
suggestions.push("Add uppercase letters (A-Z).");
}
if (this.strengthOptions.requireLowercase && !hasLowercase) {
isValid = false;
suggestions.push("Add lowercase letters (a-z).");
}
if (this.strengthOptions.requireNumbers && !hasNumbers) {
isValid = false;
suggestions.push("Include at least one digit (0-9).");
}
if (this.strengthOptions.requireSymbols && !hasSymbols) {
isValid = false;
suggestions.push("Include at least one special character (!@#$%...).");
}
if (this.strengthOptions.preventRepeats && hasRepeats) {
isValid = false;
suggestions.push("Avoid repeating characters (e.g. 'aaa').");
}
if (this.strengthOptions.preventSequences && hasSequences) {
isValid = false;
suggestions.push("Avoid common sequences (e.g. '123', 'abc').");
}
if (this.strengthOptions.checkDictionary && hasDictionaryWord) {
isValid = false;
// Suggestion for dictionary word is already added in the Dictionary Check block above
}
}
else {
// Default validity threshold if no rules provided (e.g. score >= 40)
isValid = score >= 40;
}
return {
score,
label,
suggestions,
isValid,
analysis: {
length: len,
hasUppercase,
hasLowercase,
hasNumbers,
hasSymbols,
hasRepeats,
hasSequences,
entropy,
crackTimeSeconds,
crackTimeDisplay,
},
};
}
// ─── Breach Check ──────────────────────────────────────────────────────────
/**
* Checks whether a password has appeared in a publicly known data breach
* using the **HaveIBeenPwned Pwned Passwords API v3** with **k-anonymity**.
*
* The full password is **never transmitted**. Only the first 5 characters of
* its SHA-1 hex digest are sent over the network; the remainder of the
* matching is performed locally.
*
* @param password - The plain-text password to check.
* @returns A `BreachCheckResult` indicating breach status and occurrence count.
* @throws If the network request fails or returns an unexpected status code.
*
* @example
* const result = await passwords.isBreached("hunter2");
* if (result.breached) {
* console.warn(`Password found ${result.occurrences} times in breach databases.`);
* }
*/
async isBreached(password) {
const sha1 = (0, crypto_1.createHash)("sha1")
.update(password)
.digest("hex")
.toUpperCase();
const prefix = sha1.slice(0, 5);
const suffix = sha1.slice(5);
const response = await fetch(`https://api.pwnedpasswords.com/range/${prefix}`, {
headers: {
// Padding header reduces response-size side-channel leakage
"Add-Padding": "true",
},
});
if (!response.ok) {
throw new Error(`PasswordManager.isBreached: HIBP API returned HTTP ${response.status}.`);
}
const body = await response.text();
// Each line: "<SUFFIX>:<COUNT>" or "<SUFFIX>:0" (padding entries)
for (const line of body.split("\r\n")) {
const [hashSuffix, countStr] = line.split(":");
if (hashSuffix === suffix) {
const occurrences = parseInt(countStr, 10);
return { breached: occurrences > 0, occurrences };
}
}
return { breached: false, occurrences: 0 };
}
// ─── Rehash Detection ──────────────────────────────────────────────────────
/**
* Determines whether a stored hash was produced with weaker parameters than
* the instance's current configuration (e.g. after a security upgrade).
*
* Supports Argon2id hash strings in the PHC string format:
* `$argon2id$v=19$m=<mem>,t=<iter>,p=<par>$<salt>$<hash>`
*
* For other algorithms, returns `false` (no opinion) so the caller can
* decide on a case-by-case basis.
*
* @param hash - The stored hash string to inspect.
* @returns `true` if the hash should be re-hashed on next successful login.
*
* @example
* if (passwords.needsRehash(user.passwordHash)) {
* user.passwordHash = await passwords.hash(rawPassword);
* await user.save();
* }
*/
needsRehash(hash) {
if (!hash.startsWith("$argon2id$"))
return false;
// Parse PHC format: $argon2id$v=19$m=65536,t=3,p=4$...
const paramsMatch = hash.match(/\$m=(\d+),t=(\d+),p=(\d+)\$/);
if (!paramsMatch)
return true; // malformed → force rehash
const [, m, t, p] = paramsMatch.map(Number);
return m < this.memoryCost || t < this.iterations || p < this.parallelism;
}
// ─── Input Sanitization ────────────────────────────────────────────────────
/**
* Validates and normalizes a raw password string before hashing.
*
* Checks performed:
* - Not empty or whitespace-only.
* - Minimum length of 8 characters.
* - Maximum length of 1024 characters (DoS guard against bcrypt-style
* long-password attacks on other algorithms).
* - Unicode NFC normalization (prevents homoglyph bypass attacks).
*
* @param password - The raw password string from user input.
* @returns The NFC-normalized password, ready for hashing.
* @throws `TypeError` if the input is not a string.
* @throws `RangeError` if the password fails length validation.
*
* @example
* const normalized = passwords.sanitizeInput(req.body.password);
* const hash = await passwords.hash(normalized);
*/
sanitizeInput(password) {
if (typeof password !== "string") {
throw new TypeError("PasswordManager.sanitizeInput: password must be a string.");
}
const trimmed = password.trim();
if (trimmed.length === 0) {
throw new RangeError("PasswordManager.sanitizeInput: password must not be empty.");
}
if (trimmed.length < 8) {
throw new RangeError("PasswordManager.sanitizeInput: password must be at least 8 characters long.");
}
if (trimmed.length > 1024) {
throw new RangeError("PasswordManager.sanitizeInput: password exceeds the maximum allowed length (1024).");
}
// NFC normalization: prevents ā (U+0101) vs a + combining macron attacks
return trimmed.normalize("NFC");
}
// ─── Utilities ─────────────────────────────────────────────────────────────
/**
* Returns a summary of the instance's current configuration.
* The `pepper` is intentionally omitted from the output.
*/
getConfig() {
return {
algorithm: this.algo,
memoryCost: this.memoryCost,
iterations: this.iterations,
parallelism: this.parallelism,
};
}
}
exports.PasswordManager = PasswordManager;
//# sourceMappingURL=PasswordManager.js.map