/** * WiFi Credentials Validation Utility * * Provides comprehensive validation for WiFi SSID and password * following IEEE 802.11 standards and common router implementations. */ export interface WiFiValidationResult { valid: boolean; errors: string[]; warnings: string[]; } export interface WiFiCredentials { ssid: string; password: string; } /** * Validate WiFi SSID * * Rules: * - Length: 1-32 characters (IEEE 802.11 standard) * - Can contain: letters, numbers, spaces, and special characters * - Cannot be empty * - Should not contain control characters */ export function validateSSID(ssid: string): WiFiValidationResult { const errors: string[] = []; const warnings: string[] = []; // Check for empty SSID if (!ssid || ssid.trim().length === 0) { errors.push('Network name (SSID) cannot be empty'); return { valid: false, errors, warnings }; } // Check length (IEEE 802.11 standard: max 32 bytes) // Note: UTF-8 characters can be multiple bytes const byteLength = new Blob([ssid]).size; if (byteLength > 32) { errors.push('Network name is too long (max 32 bytes)'); } // Check for control characters (ASCII 0-31) if (/[\x00-\x1F]/.test(ssid)) { errors.push('Network name contains invalid control characters'); } // Warnings for potentially problematic SSIDs if (ssid.startsWith(' ') || ssid.endsWith(' ')) { warnings.push('Network name has leading or trailing spaces - this may cause connection issues'); } if (/\s{2,}/.test(ssid)) { warnings.push('Network name has multiple consecutive spaces'); } return { valid: errors.length === 0, errors, warnings, }; } /** * Validate WiFi Password * * Rules based on WPA/WPA2/WPA3 standards: * - WPA/WPA2-PSK: 8-63 characters (ASCII) * - Open networks: no password required * - WEP: 5 or 13 characters (ASCII) or 10/26 hex digits (deprecated) * * Additional checks: * - Should not be empty for secured networks * - Should not contain control characters * - Common weak passwords warning */ export function validatePassword( password: string, authType?: string ): WiFiValidationResult { const errors: string[] = []; const warnings: string[] = []; // Open networks don't need password if (authType === 'Open') { return { valid: true, errors, warnings }; } // Check for empty password on secured networks if (!password || password.length === 0) { errors.push('Password is required for secured networks'); return { valid: false, errors, warnings }; } // WPA/WPA2/WPA3 validation (most common) if (!authType || authType.includes('WPA')) { // Length check: 8-63 characters if (password.length < 8) { errors.push('Password must be at least 8 characters for WPA/WPA2/WPA3 networks'); } if (password.length > 63) { errors.push('Password cannot exceed 63 characters for WPA/WPA2/WPA3 networks'); } // Check for ASCII only (non-ASCII can cause issues) if (!/^[\x20-\x7E]*$/.test(password)) { warnings.push('Password contains non-ASCII characters - this may cause issues on some devices'); } } // WEP validation (deprecated but still exists) if (authType === 'WEP') { const isValidWEP = password.length === 5 || // 40-bit WEP password.length === 13 || // 104-bit WEP (password.length === 10 && /^[0-9A-Fa-f]+$/.test(password)) || // 40-bit hex (password.length === 26 && /^[0-9A-Fa-f]+$/.test(password)); // 104-bit hex if (!isValidWEP) { errors.push('WEP password must be 5 or 13 ASCII characters, or 10/26 hexadecimal digits'); } warnings.push('WEP is deprecated and insecure - consider upgrading your router to WPA2/WPA3'); } // Check for control characters if (/[\x00-\x1F]/.test(password)) { errors.push('Password contains invalid control characters'); } // Weak password warnings if (password.length < 12 && !errors.length) { warnings.push('Password is weak - consider using at least 12 characters'); } // Common weak passwords const weakPasswords = [ 'password', '12345678', 'qwerty123', 'abc123456', 'password1', 'password123', '11111111', '00000000' ]; if (weakPasswords.includes(password.toLowerCase())) { warnings.push('This is a commonly used weak password'); } // Check for only numbers if (/^\d+$/.test(password) && password.length === 8) { warnings.push('Password contains only numbers - consider adding letters and symbols'); } return { valid: errors.length === 0, errors, warnings, }; } /** * Validate complete WiFi credentials (SSID + password) */ export function validateWiFiCredentials( ssid: string, password: string, authType?: string ): WiFiValidationResult { const ssidResult = validateSSID(ssid); const passwordResult = validatePassword(password, authType); return { valid: ssidResult.valid && passwordResult.valid, errors: [...ssidResult.errors, ...passwordResult.errors], warnings: [...ssidResult.warnings, ...passwordResult.warnings], }; } /** * Check if password is strong enough for production use * More strict than basic validation */ export function isStrongPassword(password: string): boolean { if (password.length < 12) return false; const hasUppercase = /[A-Z]/.test(password); const hasLowercase = /[a-z]/.test(password); const hasNumber = /\d/.test(password); const hasSpecial = /[^A-Za-z0-9]/.test(password); // Require at least 3 of 4 character types const typesCount = [hasUppercase, hasLowercase, hasNumber, hasSpecial].filter(Boolean).length; return typesCount >= 3; } /** * Sanitize WiFi credentials before sending to device * Trims whitespace and performs basic cleanup */ export function sanitizeWiFiCredentials(credentials: WiFiCredentials): WiFiCredentials { return { ssid: credentials.ssid.trim(), password: credentials.password, // Don't trim password - spaces might be intentional }; } /** * Get user-friendly error message for validation result */ export function getValidationErrorMessage(result: WiFiValidationResult): string { if (result.valid) return ''; if (result.errors.length > 0) { return result.errors[0]; // Return first error } return 'Invalid WiFi credentials'; } /** * Get combined error and warning message */ export function getValidationMessage(result: WiFiValidationResult): string { const messages: string[] = []; if (result.errors.length > 0) { messages.push(...result.errors); } if (result.warnings.length > 0) { messages.push(...result.warnings); } return messages.join('\n'); } /** * Real-time validation result for UI feedback */ export interface RealTimeValidationState { ssidValid: boolean; ssidError: string | null; passwordValid: boolean; passwordError: string | null; passwordStrength: 'weak' | 'medium' | 'strong' | null; canSubmit: boolean; warnings: string[]; } /** * Perform real-time validation as user types * Returns a simplified state for UI updates */ export function validateRealTime( ssid: string, password: string, authType?: string ): RealTimeValidationState { const result: RealTimeValidationState = { ssidValid: true, ssidError: null, passwordValid: true, passwordError: null, passwordStrength: null, canSubmit: false, warnings: [], }; // SSID validation (only show error if user has started typing) if (ssid.length > 0) { const ssidResult = validateSSID(ssid); result.ssidValid = ssidResult.valid; result.ssidError = ssidResult.errors.length > 0 ? ssidResult.errors[0] : null; result.warnings.push(...ssidResult.warnings); } // Password validation if (authType === 'Open') { // Open networks don't need password result.passwordValid = true; result.passwordStrength = null; } else if (password.length > 0) { const passwordResult = validatePassword(password, authType); result.passwordValid = passwordResult.valid; result.passwordError = passwordResult.errors.length > 0 ? passwordResult.errors[0] : null; result.warnings.push(...passwordResult.warnings); // Calculate password strength for visual feedback if (password.length >= 8) { if (isStrongPassword(password)) { result.passwordStrength = 'strong'; } else if (password.length >= 10) { result.passwordStrength = 'medium'; } else { result.passwordStrength = 'weak'; } } } // Can submit if both are valid (or password not required for open networks) const ssidOk = ssid.length > 0 && result.ssidValid; const passwordOk = authType === 'Open' || (password.length >= 8 && result.passwordValid); result.canSubmit = ssidOk && passwordOk; return result; } /** * Parse BLE WiFi error response and return user-friendly message */ export function parseWiFiErrorResponse(errorMessage: string): { type: 'password_wrong' | 'network_not_found' | 'timeout' | 'connection_failed' | 'unknown'; message: string; suggestion: string; } { const lowerError = errorMessage.toLowerCase(); // Password rejection if ( lowerError.includes('credentials rejected') || lowerError.includes('wrong password') || lowerError.includes('check password') || lowerError.includes('w|fail') ) { return { type: 'password_wrong', message: 'WiFi password is incorrect', suggestion: 'Please double-check the password and try again. Make sure caps lock is off.', }; } // Network not found if ( lowerError.includes('not found') || lowerError.includes('no network') || lowerError.includes('ssid not available') ) { return { type: 'network_not_found', message: 'WiFi network not found', suggestion: 'Make sure the sensor is within range of your WiFi router.', }; } // Timeout if ( lowerError.includes('timeout') || lowerError.includes('timed out') || lowerError.includes('did not respond') ) { return { type: 'timeout', message: 'Sensor did not respond in time', suggestion: 'Move closer to the sensor and try again. Make sure it is powered on.', }; } // Connection failed if ( lowerError.includes('connect') || lowerError.includes('ble') || lowerError.includes('bluetooth') ) { return { type: 'connection_failed', message: 'Lost connection to sensor', suggestion: 'Please try again. If the problem persists, restart the sensor.', }; } // Unknown error return { type: 'unknown', message: 'WiFi configuration failed', suggestion: 'Please try again. If the problem persists, contact support.', }; } /** * Validate WiFi credentials and prepare for BLE transmission * Returns sanitized credentials or throws with detailed error */ export function prepareCredentialsForDevice( ssid: string, password: string, authType?: string ): { ssid: string; password: string } { // Full validation const validation = validateWiFiCredentials(ssid, password, authType); if (!validation.valid) { const errorMsg = getValidationErrorMessage(validation); throw new Error(errorMsg); } // Sanitize const sanitized = sanitizeWiFiCredentials({ ssid, password }); // Additional safety checks for BLE transmission // Check for characters that might cause parsing issues in BLE protocol // Protocol uses | and , as delimiters: W|SSID,PASSWORD if (sanitized.ssid.includes('|') || sanitized.ssid.includes(',')) { throw new Error('Network name contains invalid characters (| or ,)'); } if (sanitized.password.includes('|')) { // Password can contain comma but not pipe (delimiter) throw new Error('Password contains invalid character (|)'); } return sanitized; }