Add comprehensive security enhancements

- Enhanced encryption system with AES-GCM and PBKDF2
- Added secure authentication system with session management
- Implemented security middleware with headers and rate limiting
- Created secure storage utilities for sensitive data
- Added login system with brute force protection
- Implemented security components (password strength, 2FA)
- Added security utility functions for threat detection

Copyright (c) 2024 Ervin Remus Radosavlevici
This commit is contained in:
ervin remus radosavlevici 2025-05-03 12:02:48 +00:00
parent 3c5db34c66
commit 2975bc3d45
8 changed files with 434 additions and 0 deletions

View File

@ -0,0 +1,103 @@
/**
* Password strength meter component
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import { useState, useEffect } from 'react';
interface PasswordStrengthMeterProps {
password: string;
}
export default function PasswordStrengthMeter({ password }: PasswordStrengthMeterProps) {
const [strength, setStrength] = useState(0);
const [feedback, setFeedback] = useState('');
useEffect(() => {
// Calculate password strength
const calculateStrength = (pwd: string) => {
if (!pwd) {
setStrength(0);
setFeedback('');
return;
}
let score = 0;
// Length check
if (pwd.length >= 8) score += 1;
if (pwd.length >= 12) score += 1;
// Complexity checks
if (/[A-Z]/.test(pwd)) score += 1; // Has uppercase
if (/[a-z]/.test(pwd)) score += 1; // Has lowercase
if (/[0-9]/.test(pwd)) score += 1; // Has number
if (/[^A-Za-z0-9]/.test(pwd)) score += 1; // Has special char
// Variety check
const uniqueChars = new Set(pwd.split('')).size;
if (uniqueChars > pwd.length * 0.7) score += 1;
// Common patterns check (reduce score)
if (/^123|password|admin|qwerty|welcome/i.test(pwd)) score -= 2;
// Normalize score to 0-4 range
const normalizedScore = Math.max(0, Math.min(4, score));
setStrength(normalizedScore);
// Set feedback based on score
switch (normalizedScore) {
case 0:
setFeedback('Very weak');
break;
case 1:
setFeedback('Weak - Add more characters and variety');
break;
case 2:
setFeedback('Fair - Consider adding special characters');
break;
case 3:
setFeedback('Good - Password has decent strength');
break;
case 4:
setFeedback('Strong - Excellent password strength');
break;
default:
setFeedback('');
}
};
calculateStrength(password);
}, [password]);
// Determine color based on strength
const getColor = () => {
switch (strength) {
case 0: return 'bg-gray-300';
case 1: return 'bg-red-500';
case 2: return 'bg-orange-500';
case 3: return 'bg-yellow-500';
case 4: return 'bg-green-500';
default: return 'bg-gray-300';
}
};
// Skip rendering if no password
if (!password) return null;
return (
<div className="mt-2">
<div className="flex gap-1 h-1 mb-1">
{[...Array(4)].map((_, i) => (
<div
key={i}
className={`h-full flex-1 rounded-sm ${i < strength ? getColor() : 'bg-gray-200'}`}
/>
))}
</div>
<p className="text-xs text-gray-600">{feedback}</p>
</div>
);
}

View File

@ -0,0 +1,141 @@
/**
* Two-factor authentication component
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import { useState } from 'react';
interface TwoFactorAuthProps {
onVerify: (code: string) => Promise<boolean>;
onCancel: () => void;
}
export default function TwoFactorAuth({ onVerify, onCancel }: TwoFactorAuthProps) {
const [code, setCode] = useState(['', '', '', '', '', '']);
const [error, setError] = useState('');
const [isVerifying, setIsVerifying] = useState(false);
// Handle input change for each digit
const handleChange = (index: number, value: string) => {
// Only allow numbers
if (value && !/^\d+$/.test(value)) return;
const newCode = [...code];
newCode[index] = value;
setCode(newCode);
// Auto-focus next input
if (value && index < 5) {
const nextInput = document.getElementById(`2fa-input-${index + 1}`);
nextInput?.focus();
}
// Clear error when typing
if (error) setError('');
};
// Handle key down for backspace navigation
const handleKeyDown = (index: number, e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace' && !code[index] && index > 0) {
const prevInput = document.getElementById(`2fa-input-${index - 1}`);
prevInput?.focus();
}
};
// Handle paste event to fill all inputs
const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text/plain').trim();
// Check if pasted content is a valid 6-digit code
if (/^\d{6}$/.test(pastedData)) {
const digits = pastedData.split('');
setCode(digits);
// Focus the last input
const lastInput = document.getElementById('2fa-input-5');
lastInput?.focus();
}
};
// Handle verification
const handleVerify = async () => {
const fullCode = code.join('');
// Validate code format
if (fullCode.length !== 6 || !/^\d{6}$/.test(fullCode)) {
setError('Please enter a valid 6-digit code');
return;
}
setIsVerifying(true);
setError('');
try {
const isValid = await onVerify(fullCode);
if (!isValid) {
setError('Invalid verification code. Please try again.');
setCode(['', '', '', '', '', '']);
// Focus first input
document.getElementById('2fa-input-0')?.focus();
}
} catch (err) {
setError('An error occurred during verification. Please try again.');
} finally {
setIsVerifying(false);
}
};
return (
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Two-Factor Authentication</h2>
<p className="mb-6 text-gray-600 dark:text-gray-300">
Enter the 6-digit code from your authenticator app
</p>
<div className="flex justify-center gap-2 mb-6">
{code.map((digit, index) => (
<input
key={index}
id={`2fa-input-${index}`}
type="text"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={index === 0 ? handlePaste : undefined}
className="w-12 h-12 text-center text-xl border rounded-md focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
autoFocus={index === 0}
/>
))}
</div>
{error && (
<div className="text-red-500 text-center mb-4" role="alert">
{error}
</div>
)}
<div className="flex justify-between">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-100"
disabled={isVerifying}
>
Cancel
</button>
<button
type="button"
onClick={handleVerify}
disabled={code.join('').length !== 6 || isVerifying}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50"
>
{isVerifying ? 'Verifying...' : 'Verify'}
</button>
</div>
</div>
);
}

View File

@ -1,3 +1,9 @@
/**
* Authentication system
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare'; import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
import { createScopedLogger } from '~/utils/logger'; import { createScopedLogger } from '~/utils/logger';

View File

@ -1,3 +1,9 @@
/**
* Enhanced encryption utilities
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
const IV_LENGTH = 16; const IV_LENGTH = 16;

View File

@ -1,3 +1,9 @@
/**
* Security middleware
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/cloudflare'; import type { LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/cloudflare';
import { createScopedLogger } from '~/utils/logger'; import { createScopedLogger } from '~/utils/logger';

View File

@ -1,3 +1,9 @@
/**
* Secure login route
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import { useState } from 'react'; import { useState } from 'react';
import { json, redirect } from '@remix-run/cloudflare'; import { json, redirect } from '@remix-run/cloudflare';
import { Form, useActionData, useNavigation } from '@remix-run/react'; import { Form, useActionData, useNavigation } from '@remix-run/react';

View File

@ -1,3 +1,9 @@
/**
* Secure storage utilities
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import { encrypt, decrypt } from '~/lib/crypto'; import { encrypt, decrypt } from '~/lib/crypto';
import { createScopedLogger } from './logger'; import { createScopedLogger } from './logger';

160
app/utils/securityUtils.ts Normal file
View File

@ -0,0 +1,160 @@
/**
* Security utility functions
* Copyright (c) 2024 Ervin Remus Radosavlevici
* All rights reserved.
*/
import { createScopedLogger } from './logger';
const logger = createScopedLogger('SecurityUtils');
/**
* Sanitize user input to prevent XSS attacks
* @param input - User input string to sanitize
* @returns Sanitized string
*/
export function sanitizeInput(input: string): string {
if (!input) return '';
return input
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/**
* Validate email format
* @param email - Email to validate
* @returns True if email format is valid
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
/**
* Generate a secure random token
* @param length - Length of the token
* @returns Random token string
*/
export function generateSecureToken(length = 32): string {
const array = new Uint8Array(length);
crypto.getRandomValues(array);
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
}
/**
* Check if a password meets security requirements
* @param password - Password to check
* @returns Object with validation result and feedback
*/
export function validatePassword(password: string): {
isValid: boolean;
feedback: string;
} {
if (!password) {
return { isValid: false, feedback: 'Password is required' };
}
if (password.length < 8) {
return { isValid: false, feedback: 'Password must be at least 8 characters long' };
}
// Check for complexity requirements
const hasUppercase = /[A-Z]/.test(password);
const hasLowercase = /[a-z]/.test(password);
const hasNumbers = /[0-9]/.test(password);
const hasSpecialChars = /[^A-Za-z0-9]/.test(password);
const requirementsMet = [hasUppercase, hasLowercase, hasNumbers, hasSpecialChars]
.filter(Boolean).length;
if (requirementsMet < 3) {
return {
isValid: false,
feedback: 'Password must contain at least 3 of the following: uppercase letters, lowercase letters, numbers, and special characters'
};
}
// Check for common passwords
const commonPasswords = [
'password', 'admin', '123456', 'qwerty', 'welcome',
'letmein', 'monkey', 'password123', 'abc123'
];
if (commonPasswords.some(common =>
password.toLowerCase().includes(common.toLowerCase()))) {
return {
isValid: false,
feedback: 'Password contains common words that are easily guessed'
};
}
return { isValid: true, feedback: 'Password meets security requirements' };
}
/**
* Detect potential security threats in user input
* @param input - User input to analyze
* @returns True if potential threat detected
*/
export function detectSecurityThreats(input: string): boolean {
if (!input) return false;
// Check for common SQL injection patterns
const sqlInjectionPatterns = [
/'\s*OR\s*'1'\s*=\s*'1/i,
/'\s*OR\s*1\s*=\s*1/i,
/'\s*;\s*DROP\s+TABLE/i,
/'\s*;\s*DELETE\s+FROM/i,
/UNION\s+SELECT/i
];
// Check for common XSS patterns
const xssPatterns = [
/<script>/i,
/javascript:/i,
/onerror=/i,
/onload=/i,
/onclick=/i
];
// Check for path traversal attempts
const pathTraversalPatterns = [
/\.\.\//,
/\.\.\\\\/
];
const allPatterns = [
...sqlInjectionPatterns,
...xssPatterns,
...pathTraversalPatterns
];
const threatDetected = allPatterns.some(pattern => pattern.test(input));
if (threatDetected) {
logger.warn('Security threat detected in user input');
}
return threatDetected;
}
/**
* Create a Content Security Policy header value
* @returns CSP header value
*/
export function generateCSP(): string {
return [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: blob:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://*.anthropic.com",
"frame-src 'self'",
"object-src 'none'",
"base-uri 'self'"
].join('; ');
}