fortify2-js
Version:
MOST POWERFUL JavaScript Security Library! Military-grade cryptography + 19 enhanced object methods + quantum-resistant algorithms + perfect TypeScript support. More powerful than Lodash with built-in security.
258 lines (255 loc) • 10.4 kB
JavaScript
;
/**
* Side-Channel Attack Protection Module
*
* This module provides protection against various side-channel attacks:
* - Timing attacks: Ensures operations take constant time regardless of input
* - Cache attacks: Implements cache-resistant operations
* - Power analysis: Reduces power analysis vectors through balanced operations
* - Fault injection: Detects and mitigates fault injection attempts
*/
/**
* Performs a constant-time comparison of two strings or arrays
* Prevents timing attacks by ensuring the comparison takes the same amount of time
* regardless of how many characters match
*
* @param a - First string or array to compare
* @param b - Second string or array to compare
* @returns True if the inputs are equal, false otherwise
*/
function constantTimeEqual(a, b) {
// Convert strings to Uint8Array if needed
const bufA = typeof a === "string" ? new TextEncoder().encode(a) : a;
const bufB = typeof b === "string" ? new TextEncoder().encode(b) : b;
// If lengths differ, return false but still do the full comparison
// to maintain constant time
const result = bufA.length === bufB.length ? 1 : 0;
// XOR each byte and OR the result
// This ensures we always process all bytes regardless of mismatches
let diff = 0;
const len = Math.max(bufA.length, bufB.length);
for (let i = 0; i < len; i++) {
// Use 0 for indices beyond array length
const byteA = i < bufA.length ? bufA[i] : 0;
const byteB = i < bufB.length ? bufB[i] : 0;
// XOR the bytes and OR with the running diff
diff |= byteA ^ byteB;
}
// Return true only if lengths match AND all bytes match
return diff === 0 && result === 1;
}
/**
* Performs a masked memory access to prevent cache-timing attacks
* This technique helps mitigate cache-based side-channel attacks by
* accessing all elements of an array regardless of which one is needed
*
* @param array - Array to access
* @param index - Index to retrieve
* @returns The value at the specified index
*/
function maskedAccess(array, index) {
if (index < 0 || index >= array.length) {
throw new Error("Index out of bounds");
}
// Create a result variable
let result = array[0];
// Access every element in the array
// Only keep the one at the specified index
for (let i = 0; i < array.length; i++) {
// This is a constant-time conditional assignment
// It will set result to array[i] only when i === index
// The comparison i === index is converted to either 0 or 1
// When multiplied by -1 and cast to a 32-bit integer, it becomes either 0 or 0xFFFFFFFF
// This creates a bit mask that's either all 0s or all 1s
const mask = -(Number(i === index) | 0) >>> 0;
// Use the mask to conditionally update the result
// If mask is all 1s (i === index), result becomes array[i]
// If mask is all 0s (i !== index), result remains unchanged
// @ts-ignore: The bitwise operations are intentional for constant-time selection
result = (result & ~mask) | (array[i] & mask);
}
return result;
}
/**
* Implements a secure modular exponentiation algorithm resistant to timing attacks
* Used for cryptographic operations like RSA and Diffie-Hellman
*
* @param base - Base value
* @param exponent - Exponent value
* @param modulus - Modulus value
* @returns (base^exponent) mod modulus
*/
function secureModPow(base, exponent, modulus) {
if (modulus <= 0n) {
throw new Error("Modulus must be positive");
}
if (exponent < 0n) {
throw new Error("Negative exponents are not supported");
}
// Handle special cases
if (modulus === 1n)
return 0n;
if (exponent === 0n)
return 1n;
// Ensure base is within modulus range
base = base % modulus;
if (base === 0n)
return 0n;
// Real Montgomery ladder implementation for constant-time modular exponentiation
let r0 = 1n;
let r1 = base;
// Process each bit of the exponent from most significant to least significant
// This approach ensures the same operations are performed regardless of the bit value
const exponentBits = exponent.toString(2);
const bitLength = exponentBits.length;
for (let i = 0; i < bitLength; i++) {
// Get the current bit (0 or 1)
const bit = exponentBits[i] === "1" ? 1n : 0n;
// Constant-time conditional swap based on the bit
// We compute both possibilities and select the right one using a constant-time select
// Compute both possible next values for r0 and r1
const r0r0 = (r0 * r0) % modulus;
const r0r1 = (r0 * r1) % modulus;
const r1r1 = (r1 * r1) % modulus;
// Constant-time selection using bitwise operations on BigInts
// For bit = 0: r0 = r0*r0, r1 = r0*r1
// For bit = 1: r0 = r0*r1, r1 = r1*r1
// Create masks for selection (all 0s or all 1s)
const mask = -bit; // 0n -> 0n, 1n -> -1n (all bits set)
// Select r0 = bit ? r0r1 : r0r0
const newR0 = (r0r0 & ~mask) | (r0r1 & mask);
// Select r1 = bit ? r1r1 : r0r1
const newR1 = (r0r1 & ~mask) | (r1r1 & mask);
r0 = newR0;
r1 = newR1;
}
return r0;
}
/**
* Implements a secure memory comparison that's resistant to fault injection attacks
* This performs multiple comparisons and verifies the results match to detect glitches
*
* @param a - First buffer to compare
* @param b - Second buffer to compare
* @returns True if the buffers are equal, false otherwise
*/
function faultResistantEqual(a, b) {
// First check: standard constant-time comparison
const result1 = constantTimeEqual(a, b);
// Second check: reverse order comparison
let result2 = true;
if (a.length !== b.length) {
result2 = false;
}
else {
let diff = 0;
for (let i = a.length - 1; i >= 0; i--) {
diff |= a[i] ^ b[i];
}
result2 = diff === 0;
}
// Third check: chunked comparison
let result3 = true;
if (a.length !== b.length) {
result3 = false;
}
else {
const chunkSize = Math.max(1, Math.floor(a.length / 4));
for (let chunk = 0; chunk < 4; chunk++) {
let diff = 0;
const start = chunk * chunkSize;
const end = chunk === 3 ? a.length : (chunk + 1) * chunkSize;
for (let i = start; i < end; i++) {
diff |= a[i] ^ b[i];
}
if (diff !== 0) {
result3 = false;
}
}
}
// Verify all results match to detect fault injection
return result1 && result2 && result3;
}
/**
* Creates a secure random delay to mitigate timing attacks
* This adds unpredictable timing variations to make it harder
* to extract information through precise timing measurements
*
* @param minMs - Minimum delay in milliseconds
* @param maxMs - Maximum delay in milliseconds
* @returns Promise that resolves after the random delay
*/
function randomDelay(minMs = 1, maxMs = 10) {
const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs;
return new Promise((resolve) => setTimeout(resolve, delay));
}
/**
* Applies cache-hardening to a function to protect against cache-timing attacks
* This technique ensures the function accesses the same memory regions
* regardless of input, making cache-timing attacks more difficult
*
* @param fn - Function to protect
* @returns Cache-hardened version of the function
*/
function cacheHarden(fn) {
// Create a shared cache buffer that persists between function calls
// This is more effective than creating a new buffer each time
const cacheSize = 8192; // 8KB buffer
const cacheBuffer = new Uint8Array(cacheSize);
// Initialize the buffer with some data
for (let i = 0; i < cacheSize; i++) {
cacheBuffer[i] = (i * 17) & 0xff;
}
// Return the hardened function
return ((...args) => {
// Pre-execution cache normalization
// Access the entire cache buffer in a pattern that will load it into cache
let preSum = 0;
for (let i = 0; i < cacheSize; i += 64) {
// Typical cache line size is 64 bytes
preSum ^= cacheBuffer[i];
// Also access some random locations to create noise
const randomOffset = (i * 31 + 17) % cacheSize;
preSum ^= cacheBuffer[randomOffset];
}
// Execute the function
const startTime = performance.now();
const result = fn(...args);
const endTime = performance.now();
// Post-execution cache normalization
// This helps prevent leaking information about what the function accessed
let postSum = 0;
// Use a different access pattern after execution
for (let i = cacheSize - 1; i >= 0; i -= 64) {
postSum ^= cacheBuffer[i];
// Modify the buffer slightly to prevent optimization
cacheBuffer[i] = (cacheBuffer[i] + 1) & 0xff;
}
// Add timing jitter based on the execution time
// This helps mask the actual execution time
const executionTime = endTime - startTime;
const jitterTime = Math.min(10, executionTime * 0.1); // Up to 10% jitter
if (jitterTime > 0) {
const delay = Math.random() * jitterTime;
const endJitter = performance.now() + delay;
// Busy-wait to add jitter
// This is more reliable than setTimeout for small delays
while (performance.now() < endJitter) {
// Perform some work to prevent optimization
postSum ^= (postSum << 1) | (postSum >>> 31);
}
}
// Use the sums to prevent optimization
if (preSum === postSum && preSum === Number.MAX_SAFE_INTEGER) {
console.log("This condition will never be true");
}
return result;
});
}
exports.cacheHarden = cacheHarden;
exports.constantTimeEqual = constantTimeEqual;
exports.faultResistantEqual = faultResistantEqual;
exports.maskedAccess = maskedAccess;
exports.randomDelay = randomDelay;
exports.secureModPow = secureModPow;
//# sourceMappingURL=side-channel.js.map