wpdb = $wpdb; $this->settings = $settings; } /** * Handle scent authentication requests (OAuth2 endpoints) */ public function handle_scent_request(): void { $endpoint = get_query_var('oauth_endpoint'); // 🔐 SECURITY: Enforce HTTPS for all OAuth2 endpoints $this->enforce_https(); // 🔐 SECURITY: Add comprehensive security headers $this->add_security_headers(); // 🔐 SECURITY: Check for blocked IPs and emergency blocks if ($this->is_client_blocked()) { $this->send_blocked_response(); return; } // 🔐 SECURITY: Validate request integrity if (!$this->validate_request_integrity()) { $this->log_security_violation('Request integrity validation failed'); $this->send_error_response(400, 'invalid_request', 'Request validation failed'); return; } // 🔐 SECURITY: Check rate limits before processing request if (!TigerStyleScent_RateLimiter::check_rate_limit($endpoint)) { TigerStyleScent_RateLimiter::send_rate_limit_response($endpoint); return; } switch ($endpoint) { case 'authorize': $this->handle_territory_authorization(); break; case 'token': $this->handle_scent_token_exchange(); break; case 'introspect': $this->handle_scent_analysis(); break; case 'revoke': $this->handle_scent_revocation(); break; default: $this->send_error_response(404, 'unknown_territory', 'Unknown territory endpoint'); } } /** * Handle territory authorization (OAuth2 authorize endpoint) */ private function handle_territory_authorization(): void { // 🔐 SECURITY: Comprehensive OAuth2 parameter validation $validation_rules = TigerStyleScent_InputValidator::get_oauth2_validation_rules(); $auth_rules = [ 'response_type' => $validation_rules['response_type'], 'client_id' => $validation_rules['client_id'], 'redirect_uri' => $validation_rules['redirect_uri'], 'scope' => $validation_rules['scope'], 'state' => $validation_rules['state'] ]; $validation_result = TigerStyleScent_InputValidator::validate_oauth2_request($_GET, $auth_rules); if (!$validation_result['valid']) { // Log validation failure with detailed context TigerStyleScent_SecurityLogger::log_security_event( TigerStyleScent_SecurityLogger::EVENT_VALIDATION_FAILURE, TigerStyleScent_SecurityLogger::SEVERITY_MEDIUM, 'Authorization endpoint validation failed', [ 'errors' => $validation_result['errors'], 'warnings' => $validation_result['warnings'], 'raw_input' => $_GET ] ); $this->send_error_response(400, 'invalid_request', 'Invalid request parameters'); return; } // Extract validated parameters $response_type = $validation_result['sanitized']['response_type']; $client_id = $validation_result['sanitized']['client_id']; $redirect_uri = $validation_result['sanitized']['redirect_uri']; $scope = $validation_result['sanitized']['scope'] ?? 'basic'; $state = $validation_result['sanitized']['state'] ?? ''; // Validate required parameters for territory access if (empty($response_type) || empty($client_id)) { $this->send_error_response(400, 'invalid_request', 'Missing required territory parameters'); return; } // Only support authorization code (territory code) flow if ($response_type !== 'code') { $this->send_error_response(400, 'unsupported_response_type', 'Only territory code flow is supported'); return; } // Validate client scent credentials $client = $this->recognize_client_scent($client_id); if (!$client) { $this->send_error_response(400, 'invalid_client', 'Unknown client scent'); return; } // Validate redirect URI for safe territory return if (!empty($redirect_uri) && !$this->validate_territory_return_uri($client, $redirect_uri)) { $this->send_error_response(400, 'invalid_request', 'Invalid territory return URI'); return; } // Check if user is authenticated (has proper scent) if (!is_user_logged_in()) { // Redirect to WordPress login with return URL $login_url = wp_login_url(add_query_arg($_GET, admin_url('admin.php'))); wp_redirect($login_url); exit; } // Handle POST request (user authorization decision) if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (isset($_POST['authorize']) && wp_verify_nonce($_POST['_wpnonce'], 'tigerstyle_scent_authorize')) { $this->grant_territory_access($client_id, $redirect_uri, $scope, $state); } elseif (isset($_POST['deny'])) { $this->deny_territory_access($redirect_uri, $state); } return; } // Show territory authorization form $this->show_territory_authorization_form([ 'client' => $client, 'scope' => $scope, 'state' => $state, 'redirect_uri' => $redirect_uri, 'client_id' => $client_id ]); } /** * Handle scent token exchange (OAuth2 token endpoint) */ private function handle_scent_token_exchange(): void { // Only accept POST requests for scent token exchange if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $this->send_error_response(405, 'method_not_allowed', 'Scent exchange requires POST'); return; } $grant_type = sanitize_text_field($_POST['grant_type'] ?? ''); switch ($grant_type) { case 'authorization_code': $this->handle_territory_code_grant(); break; case 'refresh_token': $this->handle_scent_refresh_grant(); break; case 'client_credentials': $this->handle_client_scent_credentials_grant(); break; default: $this->send_error_response(400, 'unsupported_grant_type', 'Unsupported scent grant type'); } } /** * Handle territory code grant (authorization code flow) */ private function handle_territory_code_grant(): void { // Extract scent parameters $code = sanitize_text_field($_POST['code'] ?? ''); $client_id = sanitize_text_field($_POST['client_id'] ?? ''); $redirect_uri = esc_url_raw($_POST['redirect_uri'] ?? ''); $code_verifier = sanitize_text_field($_POST['code_verifier'] ?? ''); // Get client scent credentials from Authorization header or POST $client_credentials = $this->extract_client_scent_credentials(); if ($client_credentials) { $client_id = $client_credentials['client_id']; $client_secret = $client_credentials['client_secret']; } else { $client_secret = sanitize_text_field($_POST['client_secret'] ?? ''); } // Validate required scent parameters if (empty($code) || empty($client_id)) { $this->send_error_response(400, 'invalid_request', 'Missing required scent parameters'); return; } // Get and validate territory code $territory_code = $this->get_territory_code($code); if (!$territory_code) { $this->send_error_response(400, 'invalid_grant', 'Invalid territory code'); return; } // Check if territory code has expired if (strtotime($territory_code['expires']) < time()) { $this->delete_territory_code($code); $this->send_error_response(400, 'invalid_grant', 'Territory code expired'); return; } // Validate client scent match if ($territory_code['client_id'] !== $client_id) { $this->send_error_response(400, 'invalid_client', 'Client scent mismatch'); return; } // Get client details for scent recognition $client = $this->recognize_client_scent($client_id); if (!$client) { // 🔐 SECURITY: Log client authentication failure TigerStyleScent_SecurityLogger::log_security_event( TigerStyleScent_SecurityLogger::EVENT_AUTH_FAILURE, TigerStyleScent_SecurityLogger::SEVERITY_MEDIUM, 'Unknown client attempted authentication', ['client_id' => $client_id, 'endpoint' => 'token'], $client_id ); $this->send_error_response(400, 'invalid_client', 'Invalid client scent'); return; } // Validate client secret for confidential clients (strong scent verification) if (!$client['is_public']) { if (empty($client_secret) || !password_verify($client_secret, $client['client_secret'])) { // 🔐 SECURITY: Log client secret failure TigerStyleScent_SecurityLogger::log_security_event( TigerStyleScent_SecurityLogger::EVENT_AUTH_FAILURE, TigerStyleScent_SecurityLogger::SEVERITY_HIGH, 'Invalid client secret provided', ['client_id' => $client_id, 'endpoint' => 'token', 'is_public' => false], $client_id ); $this->send_error_response(401, 'invalid_client', 'Invalid scent credentials'); return; } } // Validate territory return URI if (!empty($redirect_uri) && $territory_code['redirect_uri'] !== $redirect_uri) { $this->send_error_response(400, 'invalid_grant', 'Territory return URI mismatch'); return; } // Generate scent tokens $scent_token = $this->generate_scent_token($client_id, $territory_code['user_id'], $territory_code['scope']); $refresh_scent = $this->generate_refresh_scent($client_id, $territory_code['user_id'], $territory_code['scope']); // 🔐 SECURITY: Log successful token issuance TigerStyleScent_SecurityLogger::log_security_event( TigerStyleScent_SecurityLogger::EVENT_TOKEN_ISSUED, TigerStyleScent_SecurityLogger::SEVERITY_LOW, 'OAuth2 tokens successfully issued', [ 'grant_type' => 'authorization_code', 'scope' => $territory_code['scope'], 'token_length' => strlen($scent_token), 'refresh_token_issued' => !empty($refresh_scent) ], $client_id, $territory_code['user_id'] ); // Delete territory code (one-time use like a scent trail) $this->delete_territory_code($code); // Send scent token response $this->send_scent_token_response($scent_token, $refresh_scent, $territory_code['scope']); } /** * Handle scent analysis (OAuth2 introspect endpoint) */ private function handle_scent_analysis(): void { // 🔐 CSRF Protection for POST requests if ($_SERVER['REQUEST_METHOD'] === 'POST') { $nonce = sanitize_text_field($_POST['_wpnonce'] ?? ''); if (!wp_verify_nonce($nonce, 'tigerstyle_scent_introspect')) { $this->send_error_response(403, 'invalid_request', 'CSRF token required'); return; } } $token = sanitize_text_field($_POST['token'] ?? ''); if (empty($token)) { $this->send_error_response(400, 'invalid_request', 'Missing scent token for analysis'); return; } $scent_data = $this->analyze_scent_token($token); header('Content-Type: application/json'); echo json_encode($scent_data); exit; } /** * Recognize client by their unique scent (get client data) */ private function recognize_client_scent(string $client_id): ?array { // Query WordPress posts to find OAuth2 client by scent signature $posts = get_posts([ 'post_type' => 'oauth2_client', 'meta_key' => 'client_id', 'meta_value' => $client_id, 'posts_per_page' => 1, 'post_status' => 'publish' ]); if (empty($posts)) { return null; } $post = $posts[0]; $client_scent_data = [ 'client_id' => get_post_meta($post->ID, 'client_id', true), 'client_secret' => get_post_meta($post->ID, 'client_secret', true), 'redirect_uris' => get_post_meta($post->ID, 'redirect_uris', true), 'grant_types' => get_post_meta($post->ID, 'grant_types', true), 'scope' => get_post_meta($post->ID, 'scope', true), 'is_public' => (bool)get_post_meta($post->ID, 'is_public', true), 'name' => $post->post_title, 'client_name' => $post->post_title // Add client_name for scent recognition display ]; return $client_scent_data; } /** * Validate territory return URI for safe scent trail */ private function validate_territory_return_uri(array $client, string $redirect_uri): bool { $redirect_uris = $client['redirect_uris'] ?? ''; if (empty($redirect_uris)) { return false; } $allowed_uris = explode(',', $redirect_uris); return in_array($redirect_uri, array_map('trim', $allowed_uris)); } /** * Generate unique scent token (access token) */ private function generate_scent_token(string $client_id, int $user_id, string $scope): string { // 🔐 SECURITY: Maximum entropy token generation (48 bytes = 384 bits) $scent_token = $this->generate_secure_token(48); $expires = date('Y-m-d H:i:s', time() + 3600); // 1 hour scent trail // Store scent token in territory database $this->wpdb->insert( $this->wpdb->prefix . 'oauth_access_tokens', [ 'access_token' => $scent_token, 'client_id' => $client_id, 'user_id' => $user_id, 'expires' => $expires, 'scope' => $scope ] ); return $scent_token; } /** * Generate refresh scent for long-term authentication */ private function generate_refresh_scent(string $client_id, int $user_id, string $scope): string { // 🔐 SECURITY: Higher entropy for refresh tokens (64 bytes = 512 bits) $refresh_scent = $this->generate_secure_token(64); $expires = date('Y-m-d H:i:s', time() + (30 * 24 * 3600)); // 30 day scent memory // Store refresh scent in territory database $this->wpdb->insert( $this->wpdb->prefix . 'oauth_refresh_tokens', [ 'refresh_token' => $refresh_scent, 'client_id' => $client_id, 'user_id' => $user_id, 'expires' => $expires, 'scope' => $scope ] ); return $refresh_scent; } /** * Analyze scent token validity and return data */ public function analyze_scent_token(string $token): ?array { $result = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}oauth_access_tokens WHERE access_token = %s", $token ), ARRAY_A ); if (!$result) { return ['active' => false]; } // Check if scent has expired if (strtotime($result['expires']) < time()) { // Clean up expired scent $this->wpdb->delete( $this->wpdb->prefix . 'oauth_access_tokens', ['access_token' => $token] ); return ['active' => false]; } return [ 'active' => true, 'client_id' => $result['client_id'], 'user_id' => (int)$result['user_id'], 'scope' => $result['scope'], 'exp' => strtotime($result['expires']) ]; } /** * Show territory authorization form with cat-themed UI */ private function show_territory_authorization_form(array $params): void { // Set content type header('Content-Type: text/html; charset=utf-8'); // TigerStyle Scent authorization form with cat theming ?> TigerStyle Territory Access
🐾

TigerStyle Territory Access

The application "" wants to recognize your scent.

🏰 Requested Territory Permissions:
generate_secure_token(40); $expires = date('Y-m-d H:i:s', time() + 600); // 10 minute territory code // Store territory code $this->wpdb->insert( $this->wpdb->prefix . 'oauth_authorization_codes', [ 'authorization_code' => $territory_code, 'client_id' => $client_id, 'user_id' => $user_id, 'redirect_uri' => $redirect_uri, 'expires' => $expires, 'scope' => $scope ] ); // Build redirect URL with territory code $redirect_params = [ 'code' => $territory_code, 'state' => $state ]; $redirect_url = add_query_arg($redirect_params, $redirect_uri); wp_redirect($redirect_url); exit; } /** * Send scent token response */ private function send_scent_token_response(string $scent_token, string $refresh_scent, string $scope): void { $response = [ 'access_token' => $scent_token, 'token_type' => 'Bearer', // OAuth2 compliant, but we detect ScentBearer too! 'expires_in' => 3600, 'refresh_token' => $refresh_scent, 'scope' => $scope, // TigerStyle extension - clients can use either Bearer or ScentBearer 'tigerstyle_token_type' => 'ScentBearer', 'scent_strength' => 'strong' // Cat-themed metadata ]; header('Content-Type: application/json'); header('Cache-Control: no-store'); echo json_encode($response); exit; } /** * Sanitize error messages to prevent information disclosure */ private function get_safe_error_message(string $error_type): string { $safe_messages = [ 'invalid_client' => 'Authentication failed', 'invalid_grant' => 'Request denied', 'invalid_request' => 'Bad request', 'invalid_scope' => 'Access denied', 'unauthorized_client' => 'Unauthorized', 'unsupported_grant_type' => 'Request type not supported', 'unsupported_response_type' => 'Response type not supported', 'unknown_territory' => 'Endpoint not found', 'method_not_allowed' => 'Method not allowed', 'rate_limit_exceeded' => 'Too many requests' ]; return $safe_messages[$error_type] ?? 'Request failed'; } /** * Send error response with sanitized messages */ private function send_error_response(int $status_code, string $error, string $description): void { // 🔐 SECURITY: Use sanitized error messages in production $safe_description = $this->get_safe_error_message($error); // In debug mode, show detailed errors for development if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) { $safe_description = $description; } http_response_code($status_code); header('Content-Type: application/json'); $response = [ 'error' => $error, 'error_description' => $safe_description ]; echo json_encode($response); exit; } /** * Extract client scent credentials from Authorization header */ private function extract_client_scent_credentials(): ?array { $auth_header = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; if (strpos($auth_header, 'Basic ') === 0) { $credentials = base64_decode(substr($auth_header, 6)); $parts = explode(':', $credentials, 2); if (count($parts) === 2) { return [ 'client_id' => $parts[0], 'client_secret' => $parts[1], ]; } } return null; } /** * Get territory code data */ private function get_territory_code(string $code): ?array { return $this->wpdb->get_row( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}oauth_authorization_codes WHERE authorization_code = %s", $code ), ARRAY_A ); } /** * Delete used territory code */ private function delete_territory_code(string $code): void { $this->wpdb->delete( $this->wpdb->prefix . 'oauth_authorization_codes', ['authorization_code' => $code] ); } /** * 🔐 SECURITY: Enforce HTTPS for all OAuth2 endpoints */ private function enforce_https(): void { // Check if HTTPS is required in settings $require_https = $this->settings['require_https'] ?? true; if ($require_https && !is_ssl()) { // Log security violation if (defined('TIGERSTYLE_SCENT_DEBUG') && TIGERSTYLE_SCENT_DEBUG) { error_log('[TigerStyle Scent Security] HTTP request blocked - HTTPS required'); } // Return secure error http_response_code(400); header('Content-Type: application/json'); echo json_encode([ 'error' => 'invalid_request', 'error_description' => 'HTTPS required for OAuth2 endpoints' ]); exit; } } /** * 🔐 SECURITY: Add comprehensive security headers */ private function add_security_headers(): void { // Prevent clickjacking header('X-Frame-Options: DENY'); // XSS protection header('X-XSS-Protection: 1; mode=block'); // MIME type sniffing protection header('X-Content-Type-Options: nosniff'); // Referrer policy for privacy header('Referrer-Policy: strict-origin-when-cross-origin'); // Content Security Policy for OAuth2 endpoints header("Content-Security-Policy: default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none';"); // HSTS header for HTTPS enforcement (1 year) if (is_ssl()) { header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'); } // Cache control for sensitive OAuth2 responses header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Pragma: no-cache'); header('Expires: 0'); // Feature policy to disable potentially dangerous features header('Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=(), usb=()'); } /** * 🔐 SECURITY: Generate cryptographically secure tokens with maximum entropy * * @param int $bytes Number of random bytes (determines entropy) * @return string Base64URL encoded secure token */ private function generate_secure_token(int $bytes): string { // Generate cryptographically secure random bytes $random_bytes = random_bytes($bytes); // Add additional entropy sources for maximum security $entropy_sources = [ microtime(true), wp_salt('auth'), wp_salt('secure_auth'), $_SERVER['HTTP_USER_AGENT'] ?? '', $_SERVER['REMOTE_ADDR'] ?? '', wp_generate_uuid4(), ]; // Combine entropy sources $additional_entropy = hash('sha256', json_encode($entropy_sources), true); // Mix random bytes with additional entropy using HMAC $mixed_entropy = hash_hmac('sha256', $random_bytes, $additional_entropy, true); // Use base64url encoding for safe URL transmission return rtrim(strtr(base64_encode($mixed_entropy . $random_bytes), '+/', '-_'), '='); } /** * 🔐 SECURITY: Check if client IP is blocked */ private function is_client_blocked(): bool { $client_ip = $this->get_client_ip(); // Check emergency blocks $emergency_block = get_transient("tigerstyle_scent_emergency_block_{$client_ip}"); if ($emergency_block) { return true; } // Check rate limit blocks $rate_limit_block = get_transient("tigerstyle_scent_rate_block_{$client_ip}"); if ($rate_limit_block) { return true; } return false; } /** * 🔐 SECURITY: Send blocked response */ private function send_blocked_response(): void { // Log the blocked attempt TigerStyleScent_SecurityLogger::log_security_event( TigerStyleScent_SecurityLogger::EVENT_SECURITY_VIOLATION, TigerStyleScent_SecurityLogger::SEVERITY_HIGH, 'Blocked IP attempted access', ['blocked_ip' => $this->get_client_ip()] ); http_response_code(403); header('Content-Type: application/json'); echo json_encode([ 'error' => 'access_denied', 'error_description' => 'Access temporarily restricted' ]); exit; } /** * 🔐 SECURITY: Validate request integrity */ private function validate_request_integrity(): bool { // Check request size limits if (!$this->validate_request_size()) { return false; } // Validate content type for POST requests if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$this->validate_content_type()) { return false; } // Check for suspicious request patterns if ($this->detect_suspicious_patterns()) { return false; } return true; } /** * 🔐 SECURITY: Validate request size limits */ private function validate_request_size(): bool { $max_content_length = 1024 * 1024; // 1MB limit $content_length = $_SERVER['CONTENT_LENGTH'] ?? 0; if ($content_length > $max_content_length) { $this->log_security_violation('Request size exceeded limit', [ 'content_length' => $content_length, 'max_allowed' => $max_content_length ]); return false; } return true; } /** * 🔐 SECURITY: Validate content type for POST requests */ private function validate_content_type(): bool { $content_type = $_SERVER['CONTENT_TYPE'] ?? ''; $allowed_types = [ 'application/x-www-form-urlencoded', 'application/json', 'multipart/form-data' ]; foreach ($allowed_types as $type) { if (strpos($content_type, $type) === 0) { return true; } } $this->log_security_violation('Invalid content type', [ 'content_type' => $content_type, 'allowed_types' => $allowed_types ]); return false; } /** * 🔐 SECURITY: Detect suspicious request patterns */ private function detect_suspicious_patterns(): bool { $request_uri = $_SERVER['REQUEST_URI'] ?? ''; $user_agent = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Patterns that indicate potential attacks $suspicious_patterns = [ // Directory traversal '/\.\.[\/\\\\]/', // SQL injection patterns '/union\s+select/i', '/drop\s+table/i', // Script injection '/)<[^<]*)*<\/script>/i', // Command injection '/[;&|`$]/', // Null bytes '/\x00/', ]; foreach ($suspicious_patterns as $pattern) { if (preg_match($pattern, $request_uri) || preg_match($pattern, $user_agent)) { $this->log_security_violation('Suspicious request pattern detected', [ 'pattern' => $pattern, 'request_uri' => $request_uri, 'user_agent' => substr($user_agent, 0, 255) ]); return true; } } return false; } /** * 🔐 SECURITY: Enhanced security headers */ private function add_security_headers(): void { // Prevent clickjacking header('X-Frame-Options: DENY'); // Prevent MIME sniffing header('X-Content-Type-Options: nosniff'); // XSS protection header('X-XSS-Protection: 1; mode=block'); // Referrer policy header('Referrer-Policy: strict-origin-when-cross-origin'); // Content Security Policy for OAuth2 endpoints header("Content-Security-Policy: default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'"); // Force HTTPS (HSTS) if (is_ssl()) { header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'); } // Custom security header for identification header('X-Security-Exemplar: TigerStyle-Scent-OAuth2'); // Prevent caching of sensitive OAuth2 responses header('Cache-Control: no-store, no-cache, must-revalidate, private'); header('Pragma: no-cache'); header('Expires: 0'); } /** * 🔐 SECURITY: Log security violations */ private function log_security_violation(string $message, array $context = []): void { TigerStyleScent_SecurityLogger::log_security_event( TigerStyleScent_SecurityLogger::EVENT_SECURITY_VIOLATION, TigerStyleScent_SecurityLogger::SEVERITY_HIGH, $message, $context ); } /** * 🔐 SECURITY: Get client IP with proxy support */ private function get_client_ip(): string { // Check for IP from headers (in order of preference) $ip_headers = [ 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_X_FORWARDED_FOR', // General proxy 'HTTP_X_REAL_IP', // Nginx proxy 'HTTP_X_FORWARDED', // Squid proxy 'HTTP_FORWARDED_FOR', // Legacy 'HTTP_FORWARDED', // RFC 7239 'REMOTE_ADDR' // Direct connection ]; foreach ($ip_headers as $header) { $ip = $_SERVER[$header] ?? ''; if (!empty($ip)) { // Handle comma-separated IPs (take first one) $ip = trim(explode(',', $ip)[0]); // Validate IP address if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } // Fallback to REMOTE_ADDR (may be private/local IP) return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'; } // Additional methods for refresh tokens, client credentials, etc. would follow the same cat-themed pattern... }