UNPKG

capacitor-native-update

Version:
203 lines 7.81 kB
import { ConfigManager } from './config'; import { ValidationError, ErrorCode } from './errors'; import { Logger } from './logger'; export class SecurityValidator { constructor() { this.configManager = ConfigManager.getInstance(); this.logger = Logger.getInstance(); } static getInstance() { if (!SecurityValidator.instance) { SecurityValidator.instance = new SecurityValidator(); } return SecurityValidator.instance; } /** * Calculate SHA-256 checksum of data */ async calculateChecksum(data) { const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } /** * Verify checksum matches expected value */ async verifyChecksum(data, expectedChecksum) { if (!expectedChecksum) { this.logger.warn('No checksum provided for verification'); return true; // Allow if no checksum provided } const actualChecksum = await this.calculateChecksum(data); const isValid = actualChecksum === expectedChecksum.toLowerCase(); if (!isValid) { this.logger.error('Checksum verification failed', { expected: expectedChecksum, actual: actualChecksum }); } return isValid; } /** * Alias for verifyChecksum for backward compatibility */ async validateChecksum(data, expectedChecksum) { return this.verifyChecksum(data, expectedChecksum); } /** * Verify digital signature (stub for now - implement with proper crypto library) */ async verifySignature(_data, _signature) { if (!this.configManager.get('enableSignatureValidation')) { return true; } const publicKey = this.configManager.get('publicKey'); if (!publicKey) { throw new ValidationError(ErrorCode.SIGNATURE_INVALID, 'Public key not configured for signature validation'); } // TODO: Implement actual signature verification using SubtleCrypto or a library // For now, this is a placeholder this.logger.debug('Signature verification not yet implemented'); return true; } /** * Sanitize file path to prevent directory traversal */ sanitizePath(path) { // Remove any parent directory references const sanitized = path .split('/') .filter(part => part !== '..' && part !== '.') .join('/'); // Ensure path doesn't start with / return sanitized.replace(/^\/+/, ''); } /** * Validate bundle ID format */ validateBundleId(bundleId) { if (!bundleId || typeof bundleId !== 'string') { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Bundle ID must be a non-empty string'); } // Allow alphanumeric, hyphens, underscores, and dots const validPattern = /^[a-zA-Z0-9\-_.]+$/; if (!validPattern.test(bundleId)) { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Bundle ID contains invalid characters'); } if (bundleId.length > 100) { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Bundle ID is too long (max 100 characters)'); } } /** * Validate semantic version format */ validateVersion(version) { if (!version || typeof version !== 'string') { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Version must be a non-empty string'); } // Basic semantic versioning pattern const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; if (!semverPattern.test(version)) { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Version must follow semantic versioning format (e.g., 1.2.3)'); } } /** * Check if version is a downgrade */ isVersionDowngrade(currentVersion, newVersion) { const current = this.parseVersion(currentVersion); const next = this.parseVersion(newVersion); if (next.major < current.major) return true; if (next.major > current.major) return false; if (next.minor < current.minor) return true; if (next.minor > current.minor) return false; return next.patch < current.patch; } /** * Parse semantic version */ parseVersion(version) { const parts = version.split('-')[0].split('.'); // Ignore pre-release return { major: parseInt(parts[0], 10) || 0, minor: parseInt(parts[1], 10) || 0, patch: parseInt(parts[2], 10) || 0, }; } /** * Validate URL format and security */ validateUrl(url) { if (!url || typeof url !== 'string') { throw new ValidationError(ErrorCode.INVALID_URL, 'URL must be a non-empty string'); } let parsedUrl; try { parsedUrl = new URL(url); } catch (_a) { throw new ValidationError(ErrorCode.INVALID_URL, 'Invalid URL format'); } // Enforce HTTPS if (parsedUrl.protocol !== 'https:') { throw new ValidationError(ErrorCode.INVALID_URL, 'Only HTTPS URLs are allowed'); } // Check against allowed hosts const allowedHosts = this.configManager.get('allowedHosts'); if (allowedHosts.length > 0 && !allowedHosts.includes(parsedUrl.hostname)) { throw new ValidationError(ErrorCode.UNAUTHORIZED_HOST, `Host ${parsedUrl.hostname} is not in the allowed hosts list`); } // Prevent localhost/private IPs in production const privatePatterns = [ /^localhost$/i, /^127\./, /^10\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./, /^::1$/, /^fc00:/i, /^fe80:/i, ]; if (privatePatterns.some(pattern => pattern.test(parsedUrl.hostname))) { throw new ValidationError(ErrorCode.UNAUTHORIZED_HOST, 'Private/local addresses are not allowed'); } } /** * Validate file size */ validateFileSize(size) { if (typeof size !== 'number' || size < 0) { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'File size must be a non-negative number'); } const maxSize = this.configManager.get('maxBundleSize'); if (size > maxSize) { throw new ValidationError(ErrorCode.BUNDLE_TOO_LARGE, `File size ${size} exceeds maximum allowed size of ${maxSize} bytes`); } } /** * Generate a secure random ID */ generateSecureId() { const array = new Uint8Array(16); crypto.getRandomValues(array); return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); } /** * Validate metadata object */ validateMetadata(metadata) { if (metadata && typeof metadata !== 'object') { throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Metadata must be an object'); } // Limit metadata size to prevent abuse const metadataStr = JSON.stringify(metadata || {}); if (metadataStr.length > 10240) { // 10KB limit throw new ValidationError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Metadata is too large (max 10KB)'); } } } //# sourceMappingURL=security.js.map