capacitor-native-update
Version:
Native Update Plugin for Capacitor
203 lines • 7.81 kB
JavaScript
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