<?php declare(strict_types=1); namespace PhpMyAdmin\WebAuthn; use Webmozart\Assert\Assert; use function ord; use function unpack; use const INF; use const NAN; /** * Concise Binary Object Representation (CBOR) decoder. * * This is not a general purpose CBOR decoder and only implements the CTAP2 canonical CBOR encoding form. * * @see https://www.rfc-editor.org/rfc/rfc7049 * @see https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#message-encoding */ final class CBORDecoder { /** * @return mixed * * @throws WebAuthnException */ public function decode(DataStream $stream) { return $this->wellFormed($stream); } /** * @see https://www.rfc-editor.org/rfc/rfc7049#appendix-C * * @return mixed * * @throws WebAuthnException */ private function wellFormed(DataStream $stream) { // process initial bytes $initialByte = ord($stream->take(1)); $majorType = $initialByte >> 5; $value = $additionalInformation = $initialByte & 0x1f; switch ($additionalInformation) { case 24: if ($majorType !== 7) { $value = ord($stream->take(1)); } break; case 25: if ($majorType !== 7) { $unpackedValue = unpack('n', $stream->take(2)); Assert::isArray($unpackedValue); Assert::keyExists($unpackedValue, 1); Assert::integer($unpackedValue[1]); $value = $unpackedValue[1]; } break; case 26: if ($majorType !== 7) { $unpackedValue = unpack('N', $stream->take(4)); Assert::isArray($unpackedValue); Assert::keyExists($unpackedValue, 1); Assert::integer($unpackedValue[1]); $value = $unpackedValue[1]; } break; case 27: if ($majorType !== 7) { $unpackedValue = unpack('J', $stream->take(8)); Assert::isArray($unpackedValue); Assert::keyExists($unpackedValue, 1); Assert::integer($unpackedValue[1]); $value = $unpackedValue[1]; } break; case 28: case 29: case 30: case 31: throw new WebAuthnException(); } // process content switch ($majorType) { case 0: return $this->getUnsignedInteger($value); case 1: return $this->getNegativeInteger($value); case 2: return $this->getByteString($stream, $value); case 3: return $this->getTextString($stream, $value); case 4: return $this->getList($stream, $value); case 5: return $this->getMap($stream, $value); case 6: return $this->getTag($stream); case 7: return $this->getFloatNumberOrSimpleValue($stream, $value, $additionalInformation); default: throw new WebAuthnException(); } } private function getUnsignedInteger(int $value): int { return $value; } private function getNegativeInteger(int $value): int { return -1 - $value; } /** * @throws WebAuthnException */ private function getByteString(DataStream $stream, int $value): string { return $stream->take($value); } /** * @throws WebAuthnException */ private function getTextString(DataStream $stream, int $value): string { return $stream->take($value); } /** * @psalm-return list<mixed> * * @throws WebAuthnException */ private function getList(DataStream $stream, int $value): array { $list = []; for ($i = 0; $i < $value; $i++) { /** @psalm-suppress MixedAssignment */ $list[] = $this->wellFormed($stream); } return $list; } /** * @psalm-return array<array-key, mixed> * * @throws WebAuthnException */ private function getMap(DataStream $stream, int $value): array { $map = []; for ($i = 0; $i < $value; $i++) { /** @psalm-suppress MixedAssignment, MixedArrayOffset */ $map[$this->wellFormed($stream)] = $this->wellFormed($stream); } return $map; } /** * @return mixed * * @throws WebAuthnException */ private function getTag(DataStream $stream) { // 1 embedded data item return $this->wellFormed($stream); } /** * @return mixed * * @throws WebAuthnException */ private function getFloatNumberOrSimpleValue(DataStream $stream, int $value, int $additionalInformation) { switch ($additionalInformation) { case 20: return true; case 21: return false; case 22: return null; case 24: // simple value return ord($stream->take(1)); case 25: return $this->getHalfFloat($stream); case 26: return $this->getSingleFloat($stream); case 27: return $this->getDoubleFloat($stream); case 31: // "break" stop code for indefinite-length items throw new WebAuthnException(); default: return $value; } } /** * IEEE 754 Half-Precision Float (16 bits follow) * * @see https://www.rfc-editor.org/rfc/rfc7049#appendix-D * * @throws WebAuthnException */ private function getHalfFloat(DataStream $stream): float { $value = unpack('n', $stream->take(2)); Assert::isArray($value); Assert::keyExists($value, 1); Assert::integer($value[1]); $half = $value[1]; $exp = ($half >> 10) & 0x1f; $mant = $half & 0x3ff; if ($exp === 0) { $val = $mant * (2 ** -24); } elseif ($exp !== 31) { $val = ($mant + 1024) * (2 ** ($exp - 25)); } else { $val = $mant === 0 ? INF : NAN; } return $half & 0x8000 ? -$val : $val; } /** * IEEE 754 Single-Precision Float (32 bits follow) * * @throws WebAuthnException */ private function getSingleFloat(DataStream $stream): float { $value = unpack('G', $stream->take(4)); Assert::isArray($value); Assert::keyExists($value, 1); Assert::float($value[1]); return $value[1]; } /** * IEEE 754 Double-Precision Float (64 bits follow) * * @throws WebAuthnException */ private function getDoubleFloat(DataStream $stream): float { $value = unpack('E', $stream->take(8)); Assert::isArray($value); Assert::keyExists($value, 1); Assert::float($value[1]); return $value[1]; } }