1<?php 2/** 3 * This file is part of the FreeDSx ASN1 package. 4 * 5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace FreeDSx\Asn1\Encoder; 12 13use FreeDSx\Asn1\Exception\EncoderException; 14use FreeDSx\Asn1\Exception\InvalidArgumentException; 15use FreeDSx\Asn1\Exception\PartialPduException; 16use FreeDSx\Asn1\Type as EncodedType; 17use FreeDSx\Asn1\Type\AbstractStringType; 18use FreeDSx\Asn1\Type\AbstractTimeType; 19use FreeDSx\Asn1\Type\AbstractType; 20use FreeDSx\Asn1\Type\BitStringType; 21use FreeDSx\Asn1\Type\BooleanType; 22use FreeDSx\Asn1\Type\EnumeratedType; 23use FreeDSx\Asn1\Type\GeneralizedTimeType; 24use FreeDSx\Asn1\Type\IncompleteType; 25use FreeDSx\Asn1\Type\IntegerType; 26use FreeDSx\Asn1\Type\NullType; 27use FreeDSx\Asn1\Type\OidType; 28use FreeDSx\Asn1\Type\RealType; 29use FreeDSx\Asn1\Type\RelativeOidType; 30use FreeDSx\Asn1\Type\SetOfType; 31use FreeDSx\Asn1\Type\SetType; 32use FreeDSx\Asn1\Type\UtcTimeType; 33 34/** 35 * Basic Encoding Rules (BER) encoder. 36 * 37 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 38 */ 39class BerEncoder implements EncoderInterface 40{ 41 /** 42 * Used to represent a bool false binary string. 43 */ 44 protected const BOOL_FALSE = "\x00"; 45 46 /** 47 * Used to represent a bool true binary string. 48 */ 49 protected const BOOL_TRUE = "\xff"; 50 51 /** 52 * Anything greater than this we assume we may need to deal with a bigint in an OIDs second component. 53 */ 54 protected const MAX_SECOND_COMPONENT = PHP_INT_MAX - 80; 55 56 /** 57 * @var array 58 */ 59 protected $tagMap = [ 60 AbstractType::TAG_CLASS_APPLICATION => [], 61 AbstractType::TAG_CLASS_CONTEXT_SPECIFIC => [], 62 AbstractType::TAG_CLASS_PRIVATE => [], 63 ]; 64 65 protected $tmpTagMap = []; 66 67 /** 68 * @var array 69 */ 70 protected $options = [ 71 'bitstring_padding' => '0', 72 ]; 73 74 /** 75 * @var bool 76 */ 77 protected $isGmpAvailable; 78 79 /** 80 * @var int 81 */ 82 protected $pos; 83 84 /** 85 * @var int|null 86 */ 87 protected $lastPos; 88 89 /** 90 * @var int 91 */ 92 protected $maxLen; 93 94 /** 95 * @var string|null 96 */ 97 protected $binary; 98 99 /** 100 * @param array $options 101 */ 102 public function __construct(array $options = []) 103 { 104 $this->isGmpAvailable = \extension_loaded('gmp'); 105 $this->setOptions($options); 106 } 107 108 /** 109 * {@inheritdoc} 110 */ 111 public function decode($binary, array $tagMap = []) : AbstractType 112 { 113 $this->startEncoding($binary, $tagMap); 114 if ($this->maxLen === 0) { 115 throw new InvalidArgumentException('The data to decode cannot be empty.'); 116 } elseif ($this->maxLen === 1) { 117 throw new PartialPduException('Received only 1 byte of data.'); 118 } 119 $type = $this->decodeBytes(true); 120 $this->stopEncoding(); 121 122 return $type; 123 } 124 125 /** 126 * {@inheritdoc} 127 */ 128 public function complete(IncompleteType $type, int $tagType, array $tagMap = []) : AbstractType 129 { 130 $lastPos = $this->lastPos; 131 $this->startEncoding($type->getValue(), $tagMap); 132 $newType = $this->decodeBytes(false, $tagType, $this->maxLen, $type->getIsConstructed(), AbstractType::TAG_CLASS_UNIVERSAL); 133 $this->stopEncoding(); 134 $newType->setTagNumber($type->getTagNumber()) 135 ->setTagClass($type->getTagClass()); 136 $this->lastPos = $lastPos; 137 138 return $newType; 139 } 140 141 /** 142 * {@inheritdoc} 143 */ 144 public function encode(AbstractType $type) : string 145 { 146 switch ($type) { 147 case $type instanceof BooleanType: 148 $bytes = $type->getValue() ? self::BOOL_TRUE : self::BOOL_FALSE; 149 break; 150 case $type instanceof IntegerType: 151 case $type instanceof EnumeratedType: 152 $bytes = $this->encodeInteger($type); 153 break; 154 case $type instanceof RealType: 155 $bytes = $this->encodeReal($type); 156 break; 157 case $type instanceof AbstractStringType: 158 $bytes = $type->getValue(); 159 break; 160 case $type instanceof SetOfType: 161 $bytes = $this->encodeSetOf($type); 162 break; 163 case $type instanceof SetType: 164 $bytes = $this->encodeSet($type); 165 break; 166 case $type->getIsConstructed(): 167 $bytes = $this->encodeConstructedType(...$type->getChildren()); 168 break; 169 case $type instanceof BitStringType: 170 $bytes = $this->encodeBitString($type); 171 break; 172 case $type instanceof OidType: 173 $bytes = $this->encodeOid($type); 174 break; 175 case $type instanceof RelativeOidType: 176 $bytes = $this->encodeRelativeOid($type); 177 break; 178 case $type instanceof GeneralizedTimeType: 179 $bytes = $this->encodeGeneralizedTime($type); 180 break; 181 case $type instanceof UtcTimeType: 182 $bytes = $this->encodeUtcTime($type); 183 break; 184 case $type instanceof NullType: 185 $bytes = ''; 186 break; 187 default: 188 throw new EncoderException(sprintf( 189 'The type "%s" is not currently supported.', 190 get_class($type) 191 )); 192 } 193 $length = \strlen($bytes); 194 $bytes = ($length < 128) ? \chr($length).$bytes : $this->encodeLongDefiniteLength($length).$bytes; 195 196 # The first byte of a tag always contains the class (bits 8 and 7) and whether it is constructed (bit 6). 197 $tag = $type->getTagClass() | ($type->getIsConstructed() ? AbstractType::CONSTRUCTED_TYPE : 0); 198 199 $this->validateNumericInt($type->getTagNumber()); 200 # For a high tag (>=31) we flip the first 5 bits on (0x1f) to make the first byte, then the subsequent bytes is 201 # the VLV encoding of the tag number. 202 if ($type->getTagNumber() >= 31) { 203 $bytes = \chr($tag | 0x1f).$this->intToVlqBytes($type->getTagNumber()).$bytes; 204 # For a tag less than 31, everything fits comfortably into a single byte. 205 } else { 206 $bytes = \chr($tag | $type->getTagNumber()).$bytes; 207 } 208 209 return $bytes; 210 } 211 212 /** 213 * Map universal types to specific tag class values when decoding. 214 * 215 * @param int $class 216 * @param array $map 217 * @return $this 218 */ 219 public function setTagMap(int $class, array $map) 220 { 221 if (isset($this->tagMap[$class])) { 222 $this->tagMap[$class] = $map; 223 } 224 225 return $this; 226 } 227 228 /** 229 * Get the options for the encoder. 230 * 231 * @return array 232 */ 233 public function getOptions() : array 234 { 235 return $this->options; 236 } 237 238 /** 239 * Set the options for the encoder. 240 * 241 * @param array $options 242 * @return $this 243 */ 244 public function setOptions(array $options) 245 { 246 if (isset($options['bitstring_padding']) && \is_string($options['bitstring_padding'])) { 247 $this->options['bitstring_padding'] = $options['bitstring_padding']; 248 } 249 250 return $this; 251 } 252 253 /** 254 * @return int|null 255 */ 256 public function getLastPosition() : ?int 257 { 258 return $this->lastPos; 259 } 260 261 protected function startEncoding(string $binary, array $tagMap) : void 262 { 263 $this->tmpTagMap = $tagMap + $this->tagMap; 264 $this->binary = $binary; 265 $this->lastPos = null; 266 $this->pos = 0; 267 $this->maxLen = \strlen($this->binary); 268 } 269 270 protected function stopEncoding() : void 271 { 272 $this->tmpTagMap = []; 273 $this->binary = null; 274 $this->maxLen = 0; 275 $this->lastPos = $this->pos; 276 $this->pos = 0; 277 } 278 279 /** 280 * @param bool $isRoot 281 * @param null|int $tagType 282 * @param null|int $length 283 * @param null|bool $isConstructed 284 * @param null|int $class 285 * @return AbstractType 286 * @throws EncoderException 287 * @throws PartialPduException 288 */ 289 protected function decodeBytes(bool $isRoot = false, $tagType = null, $length = null, $isConstructed = null, $class = null) : AbstractType 290 { 291 $tagNumber = $tagType; 292 if ($tagType === null) { 293 $tag = \ord($this->binary[$this->pos++]); 294 $class = $tag & 0xc0; 295 $isConstructed = (bool)($tag & AbstractType::CONSTRUCTED_TYPE); 296 $tagNumber = $tag & ~0xe0; 297 298 # Less than or equal to 30 is a low tag number represented in a single byte. 299 # A high tag number is determined using VLQ (like the OID identifier encoding) of the subsequent bytes. 300 if ($tagNumber > 30) { 301 try { 302 $tagNumber = $this->getVlqBytesToInt(); 303 # It's possible we only got part of the VLQ for the high tag, as there is no way to know its ending length. 304 } catch (EncoderException $e) { 305 if ($isRoot) { 306 throw new PartialPduException( 307 'Not enough data to decode the high tag number. No ending byte encountered for the VLQ bytes.' 308 ); 309 } 310 throw $e; 311 } 312 } 313 314 $length = \ord($this->binary[$this->pos++]); 315 if ($length === 128) { 316 throw new EncoderException('Indefinite length encoding is not currently supported.'); 317 } 318 if ($length > 128) { 319 $length = $this->decodeLongDefiniteLength($length); 320 } 321 $tagType = ($class === AbstractType::TAG_CLASS_UNIVERSAL) ? $tagNumber : ($this->tmpTagMap[$class][$tagNumber] ?? null); 322 323 if (($this->maxLen - $this->pos) < $length) { 324 $message = sprintf( 325 'The expected byte length was %s, but received %s.', 326 $length, 327 ($this->maxLen - $this->pos) 328 ); 329 if ($isRoot) { 330 throw new PartialPduException($message); 331 } else { 332 throw new EncoderException($message); 333 } 334 } 335 336 if ($tagType === null) { 337 $type = new IncompleteType(\substr($this->binary, $this->pos, $length), $tagNumber, $class, $isConstructed); 338 $this->pos += $length; 339 340 return $type; 341 } 342 } 343 344 # Yes...this huge switch statement should be a separate method. However, it is faster inline when decoding 345 # lots of data (such as thousands of ASN.1 structures at a time). 346 switch ($tagType) { 347 case AbstractType::TAG_TYPE_BOOLEAN: 348 if ($length !== 1 || $isConstructed) { 349 throw new EncoderException('The encoded boolean type is malformed.'); 350 } 351 $type = EncodedType\BooleanType::withTag($tagNumber, $class, $this->decodeBoolean()); 352 break; 353 case AbstractType::TAG_TYPE_NULL: 354 if ($length !== 0 || $isConstructed) { 355 throw new EncoderException('The encoded null type is malformed.'); 356 } 357 $type = EncodedType\NullType::withTag($tagNumber, $class); 358 break; 359 case AbstractType::TAG_TYPE_INTEGER: 360 if ($isConstructed) { 361 throw new EncoderException('The encoded integer type is malformed.'); 362 } 363 $type = EncodedType\IntegerType::withTag($tagNumber, $class, $this->decodeInteger($length)); 364 break; 365 case AbstractType::TAG_TYPE_ENUMERATED: 366 if ($isConstructed) { 367 throw new EncoderException('The encoded enumerated type is malformed.'); 368 } 369 $type = EncodedType\EnumeratedType::withTag($tagNumber, $class, $this->decodeInteger($length)); 370 break; 371 case AbstractType::TAG_TYPE_REAL: 372 if ($isConstructed) { 373 throw new EncoderException('The encoded real type is malformed.'); 374 } 375 $type = RealType::withTag($tagNumber, $class, $this->decodeReal($length)); 376 break; 377 case AbstractType::TAG_TYPE_BIT_STRING: 378 $type = EncodedType\BitStringType::withTag($tagNumber, $class, $isConstructed, $this->decodeBitString($length)); 379 break; 380 case AbstractType::TAG_TYPE_OID: 381 if ($isConstructed) { 382 throw new EncoderException('The encoded OID type is malformed.'); 383 } 384 $type = OidType::withTag($tagNumber, $class, $this->decodeOid($length)); 385 break; 386 case AbstractType::TAG_TYPE_RELATIVE_OID: 387 if ($isConstructed) { 388 throw new EncoderException('The encoded relative OID type is malformed.'); 389 } 390 $type = RelativeOidType::withTag($tagNumber, $class, $this->decodeRelativeOid($length)); 391 break; 392 case AbstractType::TAG_TYPE_GENERALIZED_TIME: 393 $type = EncodedType\GeneralizedTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeGeneralizedTime($length)); 394 break; 395 case AbstractType::TAG_TYPE_UTC_TIME: 396 $type = EncodedType\UtcTimeType::withTag($tagNumber, $class, $isConstructed, ...$this->decodeUtcTime($length)); 397 break; 398 case AbstractType::TAG_TYPE_OCTET_STRING: 399 $type = EncodedType\OctetStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 400 $this->pos += $length; 401 break; 402 case AbstractType::TAG_TYPE_GENERAL_STRING: 403 $type = EncodedType\GeneralStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 404 $this->pos += $length; 405 break; 406 case AbstractType::TAG_TYPE_VISIBLE_STRING: 407 $type = EncodedType\VisibleStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 408 $this->pos += $length; 409 break; 410 case AbstractType::TAG_TYPE_BMP_STRING: 411 $type = EncodedType\BmpStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 412 $this->pos += $length; 413 break; 414 case AbstractType::TAG_TYPE_CHARACTER_STRING: 415 $type = EncodedType\CharacterStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 416 $this->pos += $length; 417 break; 418 case AbstractType::TAG_TYPE_UNIVERSAL_STRING: 419 $type = EncodedType\UniversalStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 420 $this->pos += $length; 421 break; 422 case AbstractType::TAG_TYPE_GRAPHIC_STRING: 423 $type = EncodedType\GraphicStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 424 $this->pos += $length; 425 break; 426 case AbstractType::TAG_TYPE_VIDEOTEX_STRING: 427 $type = EncodedType\VideotexStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 428 $this->pos += $length; 429 break; 430 case AbstractType::TAG_TYPE_TELETEX_STRING: 431 $type = EncodedType\TeletexStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 432 $this->pos += $length; 433 break; 434 case AbstractType::TAG_TYPE_PRINTABLE_STRING: 435 $type = EncodedType\PrintableStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 436 $this->pos += $length; 437 break; 438 case AbstractType::TAG_TYPE_NUMERIC_STRING: 439 $type = EncodedType\NumericStringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 440 $this->pos += $length; 441 break; 442 case AbstractType::TAG_TYPE_IA5_STRING: 443 $type = EncodedType\IA5StringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 444 $this->pos += $length; 445 break; 446 case AbstractType::TAG_TYPE_UTF8_STRING: 447 $type = EncodedType\Utf8StringType::withTag($tagNumber, $class, $isConstructed, \substr($this->binary, $this->pos, $length)); 448 $this->pos += $length; 449 break; 450 case AbstractType::TAG_TYPE_SEQUENCE: 451 if (!$isConstructed) { 452 throw new EncoderException('The encoded sequence type is malformed.'); 453 } 454 $type = EncodedType\SequenceType::withTag($tagNumber, $class, $this->decodeConstructedType($length)); 455 break; 456 case AbstractType::TAG_TYPE_SET: 457 if (!$isConstructed) { 458 throw new EncoderException('The encoded set type is malformed.'); 459 } 460 $type = EncodedType\SetType::withTag($tagNumber, $class, $this->decodeConstructedType($length)); 461 break; 462 default: 463 throw new EncoderException(sprintf('Unable to decode value to a type for tag %s.', $tagType)); 464 } 465 466 return $type; 467 } 468 469 /** 470 * @param int $length 471 * @return int 472 * @throws EncoderException 473 * @throws PartialPduException 474 */ 475 protected function decodeLongDefiniteLength(int $length) : int 476 { 477 # The length of the length bytes is in the first 7 bits. So remove the MSB to get the value. 478 $lengthOfLength = $length & ~0x80; 479 480 # The value of 127 is marked as reserved in the spec 481 if ($lengthOfLength === 127) { 482 throw new EncoderException('The decoded length cannot be equal to 127 bytes.'); 483 } 484 if ($lengthOfLength > ($this->maxLen - $this->pos)) { 485 throw new PartialPduException('Not enough data to decode the length.'); 486 } 487 $endAt = $this->pos + $lengthOfLength; 488 489 # Base 256 encoded 490 $length = 0; 491 for ($this->pos; $this->pos < $endAt; $this->pos++) { 492 $length = $length * 256 + \ord($this->binary[$this->pos]); 493 } 494 495 return $length; 496 } 497 498 /** 499 * Given what should be VLQ bytes represent an int, get the int and the length of bytes. 500 * 501 * @return string|int 502 * @throws EncoderException 503 */ 504 protected function getVlqBytesToInt() 505 { 506 $value = 0; 507 $lshift = 0; 508 $isBigInt = false; 509 510 for ($this->pos; $this->pos < $this->maxLen; $this->pos++) { 511 if (!$isBigInt) { 512 $lshift = $value << 7; 513 # An overflow bitshift will result in a negative number or zero. 514 # This will check if GMP is available and flip it to a bigint safe method in one shot. 515 if ($value > 0 && $lshift <= 0) { 516 $isBigInt = true; 517 $this->throwIfBigIntGmpNeeded(true); 518 $value = \gmp_init($value); 519 } 520 } 521 if ($isBigInt) { 522 $lshift = \gmp_mul($value, \gmp_pow('2', 7)); 523 } 524 $orVal = (\ord($this->binary[$this->pos]) & 0x7f); 525 if ($isBigInt) { 526 $value = \gmp_or($lshift, \gmp_init($orVal)); 527 } else { 528 $value = $lshift | $orVal; 529 } 530 # We have reached the last byte if the MSB is not set. 531 if ((\ord($this->binary[$this->pos]) & 0x80) === 0) { 532 $this->pos++; 533 534 return $isBigInt ? \gmp_strval($value) : $value; 535 } 536 } 537 538 throw new EncoderException('Expected an ending byte to decode a VLQ, but none was found.'); 539 } 540 541 /** 542 * Get the bytes that represent variable length quantity. 543 * 544 * @param string|int $int 545 * @return string 546 * @throws EncoderException 547 */ 548 protected function intToVlqBytes($int) 549 { 550 $bigint = \is_float($int + 0); 551 $this->throwIfBigIntGmpNeeded($bigint); 552 553 if ($bigint) { 554 $int = \gmp_init($int); 555 $bytes = \chr(\gmp_intval(\gmp_and(\gmp_init(0x7f), $int))); 556 $int = \gmp_div($int, \gmp_pow(2, 7)); 557 $intVal = \gmp_intval($int); 558 } else { 559 $bytes = \chr(0x7f & $int); 560 $int >>= 7; 561 $intVal = $int; 562 } 563 564 while ($intVal > 0) { 565 if ($bigint) { 566 $bytes = \chr(\gmp_intval(\gmp_or(\gmp_and(\gmp_init(0x7f), $int), \gmp_init(0x80)))).$bytes; 567 $int = \gmp_div($int, \gmp_pow('2', 7)); 568 $intVal = \gmp_intval($int); 569 } else { 570 $bytes = \chr((0x7f & $int) | 0x80).$bytes; 571 $int >>= 7; 572 $intVal = $int; 573 } 574 } 575 576 return $bytes; 577 } 578 579 /** 580 * @param string|integer $integer 581 * @throws EncoderException 582 */ 583 protected function validateNumericInt($integer) : void 584 { 585 if (\is_int($integer)) { 586 return; 587 } 588 if (\is_string($integer) && \is_numeric($integer) && \strpos($integer, '.') === false) { 589 return; 590 } 591 592 throw new EncoderException(sprintf( 593 'The value to encode for "%s" must be numeric.', 594 $integer 595 )); 596 } 597 598 /** 599 * @param int $num 600 * @return string 601 * @throws EncoderException 602 */ 603 protected function encodeLongDefiniteLength(int $num) 604 { 605 $bytes = ''; 606 while ($num) { 607 $bytes = (\chr((int) ($num % 256))).$bytes; 608 $num = (int) ($num / 256); 609 } 610 611 $length = \strlen($bytes); 612 if ($length >= 127) { 613 throw new EncoderException('The encoded length cannot be greater than or equal to 127 bytes'); 614 } 615 616 return \chr(0x80 | $length).$bytes; 617 } 618 619 /** 620 * @param BitStringType $type 621 * @return string 622 */ 623 protected function encodeBitString(BitStringType $type) 624 { 625 $data = $type->getValue(); 626 $length = \strlen($data); 627 $unused = 0; 628 if ($length % 8) { 629 $unused = 8 - ($length % 8); 630 $data = \str_pad($data, $length + $unused, $this->options['bitstring_padding']); 631 $length = \strlen($data); 632 } 633 634 $bytes = \chr($unused); 635 for ($i = 0; $i < $length / 8; $i++) { 636 $bytes .= \chr(\bindec(\substr($data, $i * 8, 8))); 637 } 638 639 return $bytes; 640 } 641 642 /** 643 * @param RelativeOidType $type 644 * @return string 645 * @throws EncoderException 646 */ 647 protected function encodeRelativeOid(RelativeOidType $type) 648 { 649 $oids = \explode('.', $type->getValue()); 650 651 $bytes = ''; 652 foreach ($oids as $oid) { 653 $bytes .= $this->intToVlqBytes($oid); 654 } 655 656 return $bytes; 657 } 658 659 /** 660 * @param OidType $type 661 * @return string 662 * @throws EncoderException 663 */ 664 protected function encodeOid(OidType $type) 665 { 666 /** @var int[] $oids */ 667 $oids = \explode('.', $type->getValue()); 668 $length = \count($oids); 669 if ($length < 2) { 670 throw new EncoderException(sprintf( 671 'To encode the OID it must have at least 2 components: %s', 672 $type->getValue() 673 )); 674 } 675 if ($oids[0] > 2) { 676 throw new EncoderException(sprintf( 677 'The value of the first OID component cannot be greater than 2. Received: %s', 678 $oids[0] 679 )); 680 } 681 682 # The first and second components of the OID are represented using the formula: (X * 40) + Y 683 if ($oids[1] > self::MAX_SECOND_COMPONENT) { 684 $this->throwIfBigIntGmpNeeded(true); 685 $firstAndSecond = \gmp_strval(\gmp_add((string)($oids[0] * 40), $oids[1])); 686 } else { 687 $firstAndSecond = ($oids[0] * 40) + $oids[1]; 688 } 689 $bytes = $this->intToVlqBytes($firstAndSecond); 690 691 for ($i = 2; $i < $length; $i++) { 692 $bytes .= $this->intToVlqBytes($oids[$i]); 693 } 694 695 return $bytes; 696 } 697 698 /** 699 * @param GeneralizedTimeType $type 700 * @return string 701 * @throws EncoderException 702 */ 703 protected function encodeGeneralizedTime(GeneralizedTimeType $type) 704 { 705 return $this->encodeTime($type, 'YmdH'); 706 } 707 708 /** 709 * @param UtcTimeType $type 710 * @return string 711 * @throws EncoderException 712 */ 713 protected function encodeUtcTime(UtcTimeType $type) 714 { 715 return $this->encodeTime($type, 'ymdH'); 716 } 717 718 /** 719 * @param AbstractTimeType $type 720 * @param string $format 721 * @return string 722 * @throws EncoderException 723 */ 724 protected function encodeTime(AbstractTimeType $type, string $format) 725 { 726 if ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_SECONDS || $type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_FRACTIONS) { 727 $format .= 'is'; 728 } elseif ($type->getDateTimeFormat() === GeneralizedTimeType::FORMAT_MINUTES) { 729 $format .= 'i'; 730 } 731 732 # Is it possible to construct a datetime object in this way? Seems better to be safe with this check. 733 if ($type->getValue()->format('H') === '24') { 734 throw new EncoderException('Midnight must only be specified by 00, not 24.'); 735 } 736 737 return $this->formatDateTime( 738 clone $type->getValue(), 739 $type->getDateTimeFormat(), 740 $type->getTimeZoneFormat(), 741 $format 742 ); 743 } 744 745 /** 746 * @param \DateTime $dateTime 747 * @param string $dateTimeFormat 748 * @param string $tzFormat 749 * @param string $format 750 * @return string 751 */ 752 protected function formatDateTime(\DateTime $dateTime, string $dateTimeFormat, string $tzFormat, string $format) 753 { 754 if ($tzFormat === GeneralizedTimeType::TZ_LOCAL) { 755 $dateTime->setTimezone(new \DateTimeZone(date_default_timezone_get())); 756 } elseif ($tzFormat === GeneralizedTimeType::TZ_UTC) { 757 $dateTime->setTimezone(new \DateTimeZone('UTC')); 758 } 759 $value = $dateTime->format($format); 760 761 # Fractions need special formatting, so we cannot directly include them in the format above. 762 $ms = ''; 763 if ($dateTimeFormat === GeneralizedTimeType::FORMAT_FRACTIONS) { 764 $ms = (string) \rtrim($dateTime->format('u'), '0'); 765 } 766 767 $tz = ''; 768 if ($tzFormat === GeneralizedTimeType::TZ_UTC) { 769 $tz = 'Z'; 770 } elseif ($tzFormat === GeneralizedTimeType::TZ_DIFF) { 771 $tz = $dateTime->format('O'); 772 } 773 774 return $value.($ms !== '' ? '.'.$ms : '').$tz; 775 } 776 777 /** 778 * @param IntegerType|EnumeratedType $type 779 * @return string 780 * @throws EncoderException 781 */ 782 protected function encodeInteger(AbstractType $type) : string 783 { 784 $int = $type->getValue(); 785 $this->validateNumericInt($int); 786 $isBigInt = $type->isBigInt(); 787 $isNegative = ($int < 0); 788 $this->throwIfBigIntGmpNeeded($isBigInt); 789 if ($isNegative) { 790 $int = $isBigInt ? \gmp_abs($type->getValue()) : ($int * -1); 791 } 792 793 # Subtract one for Two's Complement... 794 if ($isNegative) { 795 $int = $isBigInt ? \gmp_sub($int, '1') : $int - 1; 796 } 797 798 if ($isBigInt) { 799 $bytes = \gmp_export($int); 800 } else { 801 # dechex can produce uneven hex while hex2bin requires it to be even 802 $hex = \dechex($int); 803 $bytes = \hex2bin((\strlen($hex) % 2) === 0 ? $hex : '0'.$hex); 804 } 805 806 # Two's Complement, invert the bits... 807 if ($isNegative) { 808 $len = \strlen($bytes); 809 for ($i = 0; $i < $len; $i++) { 810 $bytes[$i] = ~$bytes[$i]; 811 } 812 } 813 814 # MSB == Most Significant Bit. The one used for the sign. 815 $msbSet = (bool) (\ord($bytes[0]) & 0x80); 816 if (!$isNegative && $msbSet) { 817 $bytes = self::BOOL_FALSE.$bytes; 818 } elseif ($isNegative && !$msbSet) { 819 $bytes = self::BOOL_TRUE.$bytes; 820 } 821 822 return $bytes; 823 } 824 825 /** 826 * @param RealType $type 827 * @return string 828 * @throws EncoderException 829 */ 830 protected function encodeReal(RealType $type) 831 { 832 $real = $type->getValue(); 833 834 # If the value is zero, the contents are omitted 835 if ($real === ((float) 0)) { 836 return ''; 837 } 838 # If this is infinity, then a single octet of 0x40 is used. 839 if ($real === INF) { 840 return "\x40"; 841 } 842 # If this is negative infinity, then a single octet of 0x41 is used. 843 if ($real === -INF) { 844 return "\x41"; 845 } 846 847 // @todo Real type encoding/decoding is rather complex. Need to implement this yet. 848 throw new EncoderException('Real type encoding of this value not yet implemented.'); 849 } 850 851 /** 852 * @param int $length 853 * @return array 854 * @throws EncoderException 855 */ 856 protected function decodeGeneralizedTime($length) : array 857 { 858 return $this->decodeTime('YmdH', GeneralizedTimeType::TIME_REGEX, GeneralizedTimeType::REGEX_MAP, $length); 859 } 860 861 /** 862 * @param int $length 863 * @return array 864 * @throws EncoderException 865 */ 866 protected function decodeUtcTime($length) : array 867 { 868 return $this->decodeTime('ymdH', UtcTimeType::TIME_REGEX, UtcTimeType::REGEX_MAP, $length); 869 } 870 871 /** 872 * @param string $format 873 * @param string $regex 874 * @param array $matchMap 875 * @param int $length 876 * @return array 877 * @throws EncoderException 878 */ 879 protected function decodeTime(string $format, string $regex, array $matchMap, $length) : array 880 { 881 $bytes = \substr($this->binary, $this->pos, $length); 882 $this->pos += $length; 883 if (!\preg_match($regex, $bytes, $matches)) { 884 throw new EncoderException('The datetime format is invalid and cannot be decoded.'); 885 } 886 if ($matches[$matchMap['hours']] === '24') { 887 throw new EncoderException('Midnight must only be specified by 00, but got 24.'); 888 } 889 $tzFormat = AbstractTimeType::TZ_LOCAL; 890 $dtFormat = AbstractTimeType::FORMAT_HOURS; 891 892 # Minutes 893 if (isset($matches[$matchMap['minutes']]) && $matches[$matchMap['minutes']] !== '') { 894 $dtFormat = AbstractTimeType::FORMAT_MINUTES; 895 $format .= 'i'; 896 } 897 # Seconds 898 if (isset($matches[$matchMap['seconds']]) && $matches[$matchMap['seconds']] !== '') { 899 $dtFormat = AbstractTimeType::FORMAT_SECONDS; 900 $format .= 's'; 901 } 902 # Fractions of a second 903 if (isset($matchMap['fractions']) && isset($matches[$matchMap['fractions']]) && $matches[$matchMap['fractions']] !== '') { 904 $dtFormat = AbstractTimeType::FORMAT_FRACTIONS; 905 $format .= '.u'; 906 } 907 # Timezone 908 if (isset($matches[$matchMap['timezone']]) && $matches[$matchMap['timezone']] !== '') { 909 $tzFormat = $matches[$matchMap['timezone']] === 'Z' ? AbstractTimeType::TZ_UTC : AbstractTimeType::TZ_DIFF; 910 $format .= 'T'; 911 } 912 $this->validateDateFormat($matches, $matchMap); 913 914 $dateTime = \DateTime::createFromFormat($format, $bytes); 915 if ($dateTime === false) { 916 throw new EncoderException('Unable to decode time to a DateTime object.'); 917 } 918 $bytes = null; 919 920 return [$dateTime, $dtFormat, $tzFormat]; 921 } 922 923 /** 924 * Some encodings have specific restrictions. Allow them to override and validate this. 925 * 926 * @param array $matches 927 * @param array $matchMap 928 */ 929 protected function validateDateFormat(array $matches, array $matchMap) 930 { 931 } 932 933 /** 934 * @param int $length 935 * @return string 936 * @throws EncoderException 937 */ 938 protected function decodeOid($length) : string 939 { 940 if ($length === 0) { 941 throw new EncoderException('Zero length not permitted for an OID type.'); 942 } 943 # We need to get the first part here, as it's used to determine the first 2 components. 944 $startedAt = $this->pos; 945 $firstPart = $this->getVlqBytesToInt(); 946 947 if ($firstPart < 80) { 948 $oid = \floor($firstPart / 40).'.'.($firstPart % 40); 949 } else { 950 $isBigInt = ($firstPart > PHP_INT_MAX); 951 $this->throwIfBigIntGmpNeeded($isBigInt); 952 # In this case, the first identifier is always 2. 953 # But there is no limit on the value of the second identifier. 954 $oid = '2.'.($isBigInt ? \gmp_strval(\gmp_sub($firstPart, '80')) : (int)$firstPart - 80); 955 } 956 957 # We could potentially have nothing left to decode at this point. 958 $oidLength = $length - ($this->pos - $startedAt); 959 $subIdentifiers = ($oidLength === 0) ? '' : '.'.$this->decodeRelativeOid($oidLength); 960 961 return $oid.$subIdentifiers; 962 } 963 964 /** 965 * @param int $length 966 * @return string 967 * @throws EncoderException 968 */ 969 protected function decodeRelativeOid($length) : string 970 { 971 if ($length === 0) { 972 throw new EncoderException('Zero length not permitted for an OID type.'); 973 } 974 $oid = ''; 975 $endAt = $this->pos + $length; 976 977 while ($this->pos < $endAt) { 978 $oid .= ($oid === '' ? '' : '.').$this->getVlqBytesToInt(); 979 } 980 981 return $oid; 982 } 983 984 /** 985 * @return bool 986 */ 987 protected function decodeBoolean() : bool 988 { 989 return ($this->binary[$this->pos++] !== self::BOOL_FALSE); 990 } 991 992 /** 993 * @param int $length 994 * @return string 995 * @throws EncoderException 996 */ 997 protected function decodeBitString($length) : string 998 { 999 # The first byte represents the number of unused bits at the end. 1000 $unused = \ord($this->binary[$this->pos++]); 1001 1002 if ($unused > 7) { 1003 throw new EncoderException(sprintf( 1004 'The unused bits in a bit string must be between 0 and 7, got: %s', 1005 $unused 1006 )); 1007 } 1008 if ($unused > 0 && $length < 1) { 1009 throw new EncoderException(sprintf( 1010 'If the bit string is empty the unused bits must be set to 0. However, it is set to %s with %s octets.', 1011 $unused, 1012 $length 1013 )); 1014 } 1015 $length--; 1016 1017 return $this->binaryToBitString($length, $unused); 1018 } 1019 1020 /** 1021 * @param int $length 1022 * @param int $unused 1023 * @return string 1024 */ 1025 protected function binaryToBitString(int $length, int $unused) : string 1026 { 1027 $bitstring = ''; 1028 $endAt = $this->pos + $length; 1029 1030 for ($this->pos; $this->pos < $endAt; $this->pos++) { 1031 $octet = \sprintf( "%08d", \decbin(\ord($this->binary[$this->pos]))); 1032 if ($this->pos === ($endAt - 1) && $unused) { 1033 $bitstring .= \substr($octet, 0, ($unused * -1)); 1034 } else { 1035 $bitstring .= $octet; 1036 } 1037 } 1038 1039 return $bitstring; 1040 } 1041 1042 /** 1043 * @param int $length 1044 * @return string|int number 1045 * @throws EncoderException 1046 */ 1047 protected function decodeInteger($length) 1048 { 1049 if ($length === 0) { 1050 throw new EncoderException('Zero length not permitted for an integer type.'); 1051 } 1052 $bytes = \substr($this->binary, $this->pos, $length); 1053 $this->pos += $length; 1054 1055 $isNegative = (\ord($bytes[0]) & 0x80); 1056 # Need to reverse Two's Complement. Invert the bits... 1057 if ($isNegative) { 1058 for ($i = 0; $i < $length; $i++) { 1059 $bytes[$i] = ~$bytes[$i]; 1060 } 1061 } 1062 $int = \hexdec(\bin2hex($bytes)); 1063 1064 $isBigInt = \is_float($int); 1065 $this->throwIfBigIntGmpNeeded($isBigInt); 1066 if ($isBigInt) { 1067 $int = \gmp_import($bytes); 1068 } 1069 $bytes = null; 1070 1071 # Complete Two's Complement by adding 1 and turning it negative... 1072 if ($isNegative) { 1073 $int = $isBigInt ? \gmp_neg(\gmp_add($int, '1')) : ($int + 1) * -1; 1074 } 1075 1076 return $isBigInt ? \gmp_strval($int) : $int; 1077 } 1078 1079 /** 1080 * @param bool $isBigInt 1081 * @throws EncoderException 1082 */ 1083 protected function throwIfBigIntGmpNeeded(bool $isBigInt) : void 1084 { 1085 if ($isBigInt && !$this->isGmpAvailable) { 1086 throw new EncoderException(sprintf( 1087 'An integer higher than PHP_INT_MAX int (%s) was encountered and the GMP extension is not loaded.', 1088 PHP_INT_MAX 1089 )); 1090 } 1091 } 1092 1093 /** 1094 * @param int $length 1095 * @return float 1096 * @throws EncoderException 1097 */ 1098 protected function decodeReal($length) : float 1099 { 1100 if ($length === 0) { 1101 return 0; 1102 } 1103 1104 $ident = \ord($this->binary[$this->pos++]); 1105 if ($ident === 0x40) { 1106 return INF; 1107 } 1108 if ($ident === 0x41) { 1109 return -INF; 1110 } 1111 1112 // @todo Real type encoding/decoding is rather complex. Need to implement this yet. 1113 throw new EncoderException('The real type encoding encountered is not yet implemented.'); 1114 } 1115 1116 /** 1117 * Encoding subsets may require specific ordering on set types. Allow this to be overridden. 1118 * 1119 * @param SetType $set 1120 * @return string 1121 * @throws EncoderException 1122 */ 1123 protected function encodeSet(SetType $set) 1124 { 1125 return $this->encodeConstructedType(...$set->getChildren()); 1126 } 1127 1128 /** 1129 * Encoding subsets may require specific ordering on set of types. Allow this to be overridden. 1130 * 1131 * @param SetOfType $set 1132 * @return string 1133 * @throws EncoderException 1134 */ 1135 protected function encodeSetOf(SetOfType $set) 1136 { 1137 return $this->encodeConstructedType(...$set->getChildren()); 1138 } 1139 1140 /** 1141 * @param AbstractType ...$types 1142 * @return string 1143 * @throws EncoderException 1144 */ 1145 protected function encodeConstructedType(AbstractType ...$types) 1146 { 1147 $bytes = ''; 1148 1149 foreach ($types as $type) { 1150 $bytes .= $this->encode($type); 1151 } 1152 1153 return $bytes; 1154 } 1155 1156 /** 1157 * @param int $length 1158 * @return array 1159 * @throws EncoderException 1160 * @throws PartialPduException 1161 */ 1162 protected function decodeConstructedType($length) 1163 { 1164 $children = []; 1165 $endAt = $this->pos + $length; 1166 1167 while ($this->pos < $endAt) { 1168 $children[] = $this->decodeBytes(); 1169 } 1170 1171 return $children; 1172 } 1173} 1174