@shirokuma-library/mcp-knowledge-base
Version:
Shirokuma MCP Server for comprehensive knowledge management including issues, plans, documents, and work sessions. All stored data is structured for AI processing, not human readability.
202 lines (201 loc) • 6.58 kB
JavaScript
import * as crypto from 'crypto';
import { createLogger } from '../utils/logger.js';
const logger = createLogger('SecurityUtils');
export function generateSecureToken(bytes = 32) {
return crypto.randomBytes(bytes).toString('base64url');
}
export function hashPassword(password, salt) {
const actualSalt = salt || crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password, actualSalt, 10000, 64, 'sha512');
return `${actualSalt}:${hash.toString('hex')}`;
}
export function verifyPassword(password, hashedPassword) {
const [salt, hash] = hashedPassword.split(':');
const verifyHash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512');
return hash === verifyHash.toString('hex');
}
export function generateHmac(data, secret) {
return crypto
.createHmac('sha256', secret)
.update(data)
.digest('hex');
}
export function verifyHmac(data, signature, secret) {
const expected = generateHmac(data, secret);
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
export function encrypt(text, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key, 'hex'), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
tag: tag.toString('hex')
};
}
export function decrypt(encryptedData, key) {
try {
const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(key, 'hex'), Buffer.from(encryptedData.iv, 'hex'));
decipher.setAuthTag(Buffer.from(encryptedData.tag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
catch (error) {
logger.error('Decryption failed', { error });
return null;
}
}
export function maskSensitiveData(data, visibleChars = 4) {
if (data.length <= visibleChars * 2) {
return '*'.repeat(data.length);
}
const start = data.substring(0, visibleChars);
const end = data.substring(data.length - visibleChars);
const masked = '*'.repeat(Math.max(4, data.length - visibleChars * 2));
return `${start}${masked}${end}`;
}
export function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return false;
}
if (email.length > 254) {
return false;
}
const [localPart] = email.split('@');
if (localPart.length > 64) {
return false;
}
return true;
}
export function isValidUrl(url, allowedProtocols = ['http:', 'https:']) {
try {
const parsed = new URL(url);
if (!allowedProtocols.includes(parsed.protocol)) {
return false;
}
if (parsed.username || parsed.password) {
return false;
}
if (process.env.NODE_ENV === 'production') {
const hostname = parsed.hostname;
if (hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')) {
return false;
}
}
return true;
}
catch {
return false;
}
}
export function generateSessionId() {
return generateSecureToken(32);
}
export function sanitizeFilename(filename) {
let sanitized = filename.replace(/[/\\]/g, '');
sanitized = sanitized.replace(/[^a-zA-Z0-9._-]/g, '_');
sanitized = sanitized.replace(/^\.+/, '');
if (sanitized.length > 255) {
const ext = sanitized.substring(sanitized.lastIndexOf('.'));
sanitized = sanitized.substring(0, 255 - ext.length) + ext;
}
if (!sanitized) {
sanitized = 'unnamed';
}
return sanitized;
}
export function checkPasswordStrength(password) {
const feedback = [];
let score = 0;
if (password.length >= 8) {
score += 20;
}
if (password.length >= 12) {
score += 20;
}
if (password.length >= 16) {
score += 20;
}
if (password.length < 8) {
feedback.push('Password should be at least 8 characters');
}
if (/[a-z]/.test(password)) {
score += 10;
}
if (/[A-Z]/.test(password)) {
score += 10;
}
if (/[0-9]/.test(password)) {
score += 10;
}
if (/[^a-zA-Z0-9]/.test(password)) {
score += 10;
}
if (/^[0-9]+$/.test(password)) {
score -= 20;
feedback.push('Avoid using only numbers');
}
if (/^[a-zA-Z]+$/.test(password)) {
score -= 10;
feedback.push('Consider adding numbers or symbols');
}
if (/012|123|234|345|456|567|678|789|890/.test(password)) {
score -= 10;
feedback.push('Avoid sequential numbers');
}
if (/abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz/i.test(password)) {
score -= 10;
feedback.push('Avoid sequential letters');
}
if (/(.)\1{2,}/.test(password)) {
score -= 10;
feedback.push('Avoid repeated characters');
}
score = Math.max(0, Math.min(100, score));
if (score < 40) {
feedback.unshift('Weak password');
}
else if (score < 70) {
feedback.unshift('Moderate password');
}
else {
feedback.unshift('Strong password');
}
return { score, feedback };
}
export function secureCompare(a, b) {
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
export function generateCsrfToken() {
return generateSecureToken(24);
}
export function redactSensitiveFields(obj, sensitiveFields = ['password', 'token', 'secret', 'key', 'auth', 'credential']) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
if (!Array.isArray(redacted)) {
for (const key in redacted) {
const lowerKey = key.toLowerCase();
if (sensitiveFields.some(field => lowerKey.includes(field))) {
redacted[key] = '[REDACTED]';
}
else if (typeof redacted[key] === 'object') {
redacted[key] = redactSensitiveFields(redacted[key], sensitiveFields);
}
}
}
return redacted;
}