*/ class ParTCP_Incoming_Message { public $rawMessage; public $parseError; public $data; public $validationResult; public $encryptedElements = []; public $decryptionError; public $signatureStatus; public $signatureStatusMessage; public function __construct( $rawMessage, $preserveCrypto = FALSE ){ $this->rawMessage = str_replace( ["\r\n", "\r"], "\n", trim( $rawMessage ) ); $this->data = yaml_parse( $this->rawMessage ); if ( ! $this->data ){ $this->parseError = TRUE; return; } if ( ! is_array( $this->data ) ){ $this->data = []; return; } $this->encryptedElements = array_filter( $this->data, function( $k ){ return substr( $k, -1 ) == '~'; }, ARRAY_FILTER_USE_KEY ); if ( $this->encryptedElements ){ if ( ! $preserveCrypto ){ // get private key if ( empty( $this->data['To'] ) ){ $this->decryptionError = _('No receiver specified'); return; } $localId = new ParTCP_Private_Identity( $this->data['To'] ); if ( empty( $localId->privKey ) ){ $this->decryptionError = _('Private key not found'); return; } ParTCP_Crypto::set_local_privkey( $localId->privKey ); // get public key if ( ! empty( $this->data['Public-Key'] ) ){ ParTCP_Crypto::set_remote_pubkey( $this->data['Public-Key'] ); } else { if ( empty( $this->data['From'] ) ){ $this->decryptionError = _('No sender specified'); return; } $remoteId = new ParTCP_Public_Identity( $this->data['From'], TRUE ); if ( empty( $remoteId->pubKey ) ){ $this->decryptionError = _('Public key not found'); return; } ParTCP_Crypto::set_remote_pubkey( $remoteId->pubKey ); } } // decrypt elements foreach ( $this->encryptedElements as $key => $value ){ $newKey = rtrim( $key, '~' ); $decrypted = ParTCP_Crypto::decrypt( $value ); if ( $decrypted === FALSE ){ $this->data[ $newKey ] = NULL; $this->decryptionError = implode( "\n", ParTCP_Crypto::$errors ); break; } if ( ! preg_match( '//u', $decrypted ) ){ $this->data[ $newKey ] = NULL; $this->decryptionError = 'Decryption resulted in invalid UTF-8 sequence'; break; } $decoded = json_decode( $decrypted, TRUE ); $this->data[ $newKey ] = $decoded ?: $decrypted; unset( $this->data[ $key ] ); } } } public function get( $name ){ return $this->data[ $name ] ?? NULL; } public function is_encrypted( $key ){ return in_array( "{$key}~", array_keys( $this->encryptedElements ) ); } public function get_signature_status( $useEmbeddedKey = FALSE ){ $signature = $this->get('Signature'); if ( ! $signature ){ $this->signatureStatus = -1; $this->signatureStatusMessage = _('Missing signature'); return FALSE; } if ( $useEmbeddedKey ){ if ( empty( $this->data['Public-Key'] ) ){ $this->signatureStatus = -3; $this->signatureStatusMessage = _('Missing public key'); return FALSE; } ParTCP_Crypto::set_remote_pubkey( $this->data['Public-Key'] ); } else { if ( empty( $this->data['From'] ) ){ $this->signatureStatus = -5; $this->signatureStatusMessage = _('Missing sender'); return FALSE; } if ( empty( ParTCP_Crypto::$pubKeySign ) ){ $remoteId = new ParTCP_Public_Identity( $this->data['From'], TRUE ); if ( empty( $remoteId->pubKey ) ){ $this->signatureStatus = -6; $this->signatureStatusMessage = _('Sender\'s public key not retrievable'); return FALSE; } ParTCP_Crypto::set_remote_pubkey( $remoteId->pubKey ); } } $unsignedMsg = substr( $this->rawMessage, strpos( $this->rawMessage, "\n" ) + 1 ); $success = ParTCP_Crypto::verify_signature( $signature, $unsignedMsg ); if ( ! $success ){ $this->signatureStatus = -9; $this->signatureStatusMessage = _('Verification failed'); return FALSE; } $this->signatureStatus = 1; $this->signatureStatusMessage = _('Signature verified successfully'); return TRUE; } public function validate_structure( $mtd ){ if ( empty( $mtd['Elements'] ) ){ return FALSE; } $mtd['Elements'] = array_filter( $mtd['Elements'], function( $v ){ return ! is_null( $v ); } ); $existent = array_keys( $this->data ); $allowed = array_keys( $mtd['Elements'] ); $extra = array_diff( $existent, $allowed ); $this->validationResult = []; if ( ! empty( $extra ) ){ $this->validationResult[] = _('Message contains illegal element(s)') . ' [' . implode( ', ', $extra ) . ']'; } foreach ( $mtd['Elements'] as $name => $rules ){ if ( ! empty( $rules['required'] ) && ! isset( $this->data[ $name ] ) && ( ! is_array( $rules['required'] ) || ! array_intersect( $rules['required'], $existent ) ) ){ $this->validationResult[] = sprintf( _('Required element \'%s\' is missing'), $name ); } if ( ! isset( $this->data[ $name ] ) ){ continue; // element does not exist, so nothing to do } if ( ! empty( $rules['encryption_required'] ) && ! $this->is_encrypted( $name ) ){ $this->validationResult[] = sprintf( _('Element \'%s\' has to be encrypted'), $name ); } $result = $this->validate_value( $this->data[ $name ], $rules ); if ( $result !== TRUE ){ $this->validationResult[] = "Element '{$name}': {$result}"; } } return empty( $this->validationResult ); } private function validate_value( &$value, $rules ){ $type = $rules['type'] ?? 'string'; switch ( $type ){ case 'string': if ( is_array( $value ) ){ return _('value has wrong type (string expected)'); } if ( ! empty( $rules['min_length'] ) && $rules['min_length'] > strlen( $value ) ){ return sprintf( _('value is too short (minimum: %s bytes)'), $rules['min_length'] ); } if ( ! empty( $rules['max_length'] ) && $rules['max_length'] < strlen( $value ) ){ return sprintf( _('value is too long (maximum: %s bytes)'), $rules['max_length'] ); } break; case 'signature': case 'public_key': $length = $type == 'signature' ? ParTCP_Crypto::get_signature_length() : ParTCP_Crypto::get_pubkey_length(); if ( strlen( $value ) != $length ){ return sprintf( _('value has wrong length (%s bytes expected)'), $length ); } break; case 'integer': case 'float': if ( $type == 'integer' ? ! is_int( $value ): ! is_float( $value ) ){ return sprintf( _('value has wrong type (%s expected)'), $type ); } if ( ! empty( $rules['min_value'] ) && $rules['min_value'] > $value ){ return sprintf( _('value is too small (minimum: %s)'), $rules['min_value'] ); } if ( ! empty( $rules['max_value'] ) && $rules['max_value'] < $value ){ return sprintf( _('value is too large (maximum: %s)'), $rules['max_value'] ); } break; case 'bool': if ( ! is_bool( $value ) && ! in_array( $value, [ 0, 1 ] ) ){ return sprintf( _('value has wrong type (%s expected)'), $type ); } break; case 'timestamp': case 'date': if ( is_string( $value ) ){ $value = strtotime( $value ); } if ( ! in_array( gettype( $value ), [ 'integer', 'double' ] ) ){ return sprintf( _('value has wrong type (%s expected)'), $type ); } $oldTimezone = date_default_timezone_get(); date_default_timezone_set('UTC'); if ( $type == 'date' && date( 'His', $value ) != '000000' ){ return sprintf( _('value has wrong type (%s expected)'), $type ); } if ( ! empty( $rules['min_value'] ) && strtotime( $rules['min_value'] ) > $value ){ return sprintf( _('value is too early (minimum: %s)'), $rules['min_value'] ); } if ( ! empty( $rules['max_value'] ) && strtotime( $rules['max_value'] ) < $value ){ return sprintf( _('value is too late (maximum: %s)'), $rules['max_value'] ); } date_default_timezone_set( $oldTimezone ); break; case 'identity': case 'identifier': if ( ! is_string( $value ) ){ return sprintf( _('value has wrong type (%s expected)'), $type ); } if ( strlen( $value ) < 3 ){ return sprintf( _('value is too short (minimum: %s bytes)'), 3 ); } elseif ( strlen( $value ) > 128 ){ return sprintf( _('value is too long (maximum: %s bytes)'), 128 ); } break; case 'object': if ( ! is_array( $value ) ){ try { $value = json_decode( $value, TRUE ); } catch( Exception $e ) { return sprintf( _('value has wrong type (%s expected)'), $type ); } } if ( ! empty( $rules['class'] ) ){ // get otd for the class and validate object structure if ( FALSE /* object structure is invalid */ ){ return sprintf( _('object does not correspond with the \'%s\' class definition'), $rules['class'] ); } } break; case 'list': if ( ! is_array( $value ) ){ try { $value = json_decode( $value, TRUE ); } catch( Exception $e ) { return sprintf( _('value has wrong type (%s expected)'), $type ); } } $max = $rules['max_count'] ?? 100; if ( count( $value ) > $max ){ return sprintf( _('list has too many elements (maximum: %s)'), $max ); } $itemRules = []; foreach ( $rules as $ruleName => $ruleValue ){ if ( substr( $ruleName, 0, 5 ) == 'item_' ){ $itemRules[ substr( $ruleName, 5 ) ] = $ruleValue; } } $count = 1; foreach ( $value as $item ){ $itemResult = $this->validate_value( $item, $itemRules ); if ( $itemResult !== TRUE ){ return sprintf( _('list item #%s is invalid'), $count ) . " - {$itemResult}"; break; } $count++; } break; } return TRUE; } } // end of file incoming_message.class.php