Decoder::decodeType()   C
last analyzed

Complexity

Conditions 13
Paths 21

Size

Total Lines 42
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 49.5039

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 32
c 1
b 0
f 0
dl 0
loc 42
ccs 12
cts 30
cp 0.4
rs 6.6166
cc 13
nc 21
nop 2
crap 49.5039

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Ocsp\Asn1\Der;
4
5
use DateTimeImmutable;
6
use DateTimeZone;
7
use Ocsp\Asn1\Decoder as DecoderInterface;
8
use Ocsp\Asn1\Element;
9
use Ocsp\Asn1\Element\BitString;
10
use Ocsp\Asn1\Element\GeneralizedTime;
11
use Ocsp\Asn1\Element\Integer;
12
use Ocsp\Asn1\Element\ObjectIdentifier;
13
use Ocsp\Asn1\Element\OctetString;
14
use Ocsp\Asn1\Element\PrintableString;
15
use Ocsp\Asn1\Element\RawConstructed;
16
use Ocsp\Asn1\Element\RawPrimitive;
17
use Ocsp\Asn1\Element\Sequence;
18
use Ocsp\Asn1\Element\Set;
19
use Ocsp\Asn1\Tag;
20
use Ocsp\Asn1\TaggableElement;
21
use Ocsp\Asn1\UniversalTagID;
22
use Ocsp\Exception\Asn1DecodingException;
23
use Ocsp\Service\Math;
24
25
/**
26
 * Decoder from DER to ASN.1.
27
 */
28
class Decoder implements DecoderInterface
29
{
30
    /**
31
     * {@inheritdoc}
32
     *
33
     * @see \Ocsp\Asn1\Encoder::getEncodingHandle()
34
     */
35 3
    public function getEncodingHandle()
36
    {
37 3
        return 'der';
38
    }
39
40
    /**
41
     * {@inheritdoc}
42
     *
43
     * @see \Ocsp\Asn1\Decoder::decodeElement()
44
     */
45 3
    public function decodeElement($bytes)
46
    {
47 3
        $offset = 0;
48
49 3
        return $this->decodeElementAt($bytes, $offset);
50
    }
51
52
    /**
53
     * Decode an element at a specific position in a range of bytes.
54
     *
55
     * @param string $bytes
56
     * @param int $offset
57
     *
58
     * @throws \Ocsp\Exception\Asn1DecodingException
59
     *
60
     * @return \Ocsp\Asn1\Element
61
     */
62 3
    protected function decodeElementAt($bytes, &$offset)
63
    {
64 3
        list($typeID, $class, $isConstructed) = $this->decodeType($bytes, $offset);
65 3
        $encodedValue = $this->extractEncodedValue($bytes, $offset);
66
67 3
        return $isConstructed ? $this->decodeConstructed($typeID, $class, $encodedValue) : $this->decodePrimitive(/** @scrutinizer ignore-type */ $typeID, $class, $encodedValue);
68
    }
69
70
    /**
71
     * Decode a CONSTRUCTED ASN.1 element.
72
     *
73
     * @param int|\phpseclib\Math\BigInteger|\phpseclib3\Math\BigInteger $typeID
0 ignored issues
show
Bug introduced by
The type phpseclib\Math\BigInteger was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
74
     * @param string $class
75
     * @param string $encodedValue
76
     *
77
     * @throws \Ocsp\Exception\Asn1DecodingException
78
     *
79
     * @return \Ocsp\Asn1\Element
80
     */
81 3
    protected function decodeConstructed($typeID, $class, $encodedValue)
82
    {
83 3
        $offset = 0;
84 3
        $encodedValueLength = strlen($encodedValue);
85 3
        $elements = [];
86 3
        while ($offset < $encodedValueLength) {
87 3
            if ($encodedValue[$offset] === "\x00" && isset($encodedValue[$offset + 1]) && $encodedValue[$offset + 1] === "\x00") {
88
                // end of elements in case the length is in indefinite form
89
                break;
90
            }
91 3
            $elements[] = $this->decodeElementAt($encodedValue, $offset);
92
        }
93 3
        if (count($elements) === 1 && $class !== Element::CLASS_UNIVERSAL && $elements[0] instanceof TaggableElement) {
94 3
            return $elements[0]->setTag(Tag::explicit($typeID, $class));
95
        }
96 3
        if (is_int($typeID) && $class === Element::CLASS_UNIVERSAL) {
97
            switch ($typeID) {
98 3
                case UniversalTagID::SEQUENCE:
99 3
                    return Sequence::create($elements);
100 3
                case UniversalTagID::SET:
101 3
                    return Set::create($elements);
102
            }
103
        }
104
105
        return RawConstructed::create($this->getEncodingHandle(), $typeID, $class, $elements);
106
    }
107
108
    /**
109
     * Decode a PRIMITIVE ASN.1 element.
110
     *
111
     * @param int $typeID
112
     * @param string $class
113
     * @param string $encodedValue
114
     *
115
     * @throws \Ocsp\Exception\Asn1DecodingException
116
     *
117
     * @return \Ocsp\Asn1\Element
118
     */
119 3
    protected function decodePrimitive($typeID, $class, $encodedValue)
120
    {
121 3
        if ($class === Element::CLASS_UNIVERSAL) {
122
            switch ($typeID) {
123 3
                case UniversalTagID::INTEGER:
124 3
                    return Integer::create($this->decodeInteger($encodedValue));
125 3
                case UniversalTagID::BIT_STRING:
126 3
                    list($bytes, $numTrailingBits) = $this->decodeBitString($encodedValue);
127
128 3
                    return BitString::create($bytes, $numTrailingBits);
129 3
                case UniversalTagID::OCTET_STRING:
130 3
                    return OctetString::create($this->decodeOctetString($encodedValue));
131 3
                case UniversalTagID::OBJECT_IDENTIFIER:
132 3
                    return ObjectIdentifier::create($this->decodeObjectIdentifier($encodedValue));
133 3
                case UniversalTagID::PRINTABLESTRING:
134 3
                    return PrintableString::create($this->decodePrintableString($encodedValue));
135 3
                case UniversalTagID::GENERALIZEDTIME:
136 2
                    return GeneralizedTime::create($this->decodeGeneralizedTime($encodedValue));
137
            }
138
        }
139
140 3
        return RawPrimitive::create($this->getEncodingHandle(), $typeID, $class, $encodedValue);
141
    }
142
143
    /**
144
     * Extract the details about at a specific position in a range of bytes.
145
     *
146
     * @param string $bytes
147
     * @param int $offset
148
     *
149
     * @throws \Ocsp\Exception\Asn1DecodingException
150
     *
151
     * @return array The first element is the type ID (int|\phpseclib\Math\BigInteger|\phpseclib3\Math\BigInteger), the second is the class (string), the third is true (if the type is constructed) or false (not constructed)
152
     */
153 3
    protected function decodeType($bytes, &$offset)
154
    {
155 3
        if (!isset($bytes[$offset])) {
156
            throw Asn1DecodingException::create();
157
        }
158 3
        $byte = ord($bytes[$offset++]);
159 3
        $isConstructed = ($byte & 0b100000) !== 0;
160 3
        if (($byte & 0b11000000) === 0b11000000) {
161
            $class = Element::CLASS_PRIVATE;
162 3
        } elseif ($byte & 0b10000000) {
163 3
            $class = Element::CLASS_CONTEXTSPECIFIC;
164 3
        } elseif ($byte & 0b01000000) {
165
            $class = Element::CLASS_APPLICATION;
166
        } else {
167 3
            $class = Element::CLASS_UNIVERSAL;
168
        }
169 3
        $typeID = $byte & 0b00011111;
170 3
        if ($typeID === 0b00011111) {
171
            $typeParts = [];
172
            do {
173
                if (!isset($bytes[$offset])) {
174
                    throw Asn1DecodingException::create();
175
                }
176
                $byte = ord($bytes[$offset++]);
177
                $typeParts[] = $byte & 0b01111111;
178
            } while (($byte & 0b10000000) === 0);
179
            $numTypeParts = count($typeParts);
180
            if ($numTypeParts > PHP_INT_SIZE || ($numTypeParts === PHP_INT_SIZE && $typeParts[$numTypeParts - 1] & 0b10000000)) {
181
                $typeIDBits = '';
182
                for ($i = 0; $i < $numTypeParts; $i++) {
183
                    $typeIDBits .= str_pad(decbin($typeParts[$i]), 7, '0', STR_PAD_LEFT);
184
                }
185
                $typeID = Math::createBigInteger($typeIDBits, 2);
186
            } else {
187
                $typeID = 0;
188
                for ($i = $numTypeParts - 1; $i >= 0; $i--) {
189
                    $typeID = ($typeID << 7) + $typeParts[$i];
190
                }
191
            }
192
        }
193
194 3
        return [$typeID, $class, $isConstructed];
195
    }
196
197
    /**
198
     * Extract the bytes representing the value of an element.
199
     *
200
     * @param string $bytes
201
     * @param int $offset
202
     *
203
     * @throws \Ocsp\Exception\Asn1DecodingException
204
     *
205
     * @return string
206
     */
207 3
    protected function extractEncodedValue($bytes, &$offset)
208
    {
209 3
        $length = $this->decodeLength($bytes, $offset);
210 3
        if ($length === 0) {
211 3
            return '';
212
        }
213 3
        if ($offset + $length > strlen($bytes)) {
214
            throw Asn1DecodingException::create();
215
        }
216 3
        $encodedValue = substr($bytes, $offset, $length);
217 3
        $offset += $length;
218
219 3
        return $encodedValue;
220
    }
221
222
    /**
223
     * Extract the length (in bytes) of the encoded value an element.
224
     *
225
     * @param string $bytes
226
     * @param int $offset
227
     *
228
     * @throws \Ocsp\Exception\Asn1DecodingException
229
     *
230
     * @return int
231
     */
232 3
    protected function decodeLength($bytes, &$offset)
233
    {
234 3
        if (!isset($bytes[$offset])) {
235
            throw Asn1DecodingException::create();
236
        }
237 3
        $byte = ord($bytes[$offset++]);
238 3
        if (($byte & 0b10000000) === 0) {
239
            // short form
240 3
            return $byte;
241
        }
242 3
        if ($byte === 0b10000000) {
243
            // indefinite form
244
            return strlen($bytes) - $offset;
245
        }
246
        // technically, the long form of the length can be represented by up to 126 octets (bytes), but we'll only
247
        // support it up to four.
248 3
        $numLenghtBytes = $byte & 0b01111111;
249 3
        if ($numLenghtBytes === 0) {
250
            throw Asn1DecodingException::create();
251
        }
252 3
        $length = 0;
253 3
        for ($i = 0; $i < $numLenghtBytes; $i++) {
254 3
            if (!isset($bytes[$offset])) {
255
                throw Asn1DecodingException::create();
256
            }
257 3
            $byte = ord($bytes[$offset++]);
258 3
            if ($i === PHP_INT_SIZE || ($i === PHP_INT_SIZE - 1 && $byte & 0b10000000)) {
259
                throw Asn1DecodingException::create('Element length too long for this implementation');
260
            }
261 3
            $length = ($length << 8) + $byte;
262
        }
263
264 3
        return $length;
265
    }
266
267
    /**
268
     * Decode the value of an INTEGER element.
269
     *
270
     * @param string $bytes
271
     *
272
     * @return int|\phpseclib\Math\BigInteger|\phpseclib3\Math\BigInteger
273
     */
274 3
    protected function decodeInteger($bytes)
275
    {
276 3
        $numBytes = strlen($bytes);
277 3
        $firstByte = ord($bytes[0]);
278 3
        $isNegative = ($firstByte & 0b10000000) !== 0;
279 3
        if ($isNegative === false) {
280
            switch ($numBytes) {
281 3
                case 1:
282 3
                    return $firstByte;
283 3
                case 2:
284
                    return current(unpack('n', $bytes));
285 3
                case 3:
286
                    return current(unpack('N', "\x00" . $bytes));
287 3
                case 4:
288
                    return current(unpack('N', $bytes));
289
            }
290 3
            if ($numBytes <= 8 && PHP_INT_SIZE >= 8 && PHP_VERSION_ID >= 50603) {
291
                return current(unpack('J', str_pad($bytes, 8, "\x00", STR_PAD_LEFT)));
292
            }
293
        }
294
295 3
        return Math::createBigInteger($bytes, -256);
296
    }
297
298
    /**
299
     * Decode the value of a BIT STRING element.
300
     *
301
     * @param string $bytes
302
     *
303
     * @return array The first element contains the bytes (a string), the second element the number of trailing bits (an integer)
304
     */
305 3
    protected function decodeBitString($bytes)
306
    {
307 3
        $numTrailingBits = ord($bytes[0]) & 0b01111111;
308 3
        $bytes = substr($bytes, 1);
309 3
        if ($bytes === false) {
310
            $bytes = '';
311
        }
312
313 3
        return [$bytes, $numTrailingBits];
314
    }
315
316
    /**
317
     * Decode the value of a OCTET STRING element.
318
     *
319
     * @param string $bytes
320
     *
321
     * @return string
322
     */
323 3
    protected function decodeOctetString($bytes)
324
    {
325 3
        return $bytes;
326
    }
327
328
    /**
329
     * Decode the value of a OBJECT IDENTIFIER element.
330
     *
331
     * @param string $bytes
332
     *
333
     * @throws \Ocsp\Exception\Asn1DecodingException
334
     *
335
     * @return string
336
     */
337 3
    protected function decodeObjectIdentifier($bytes)
338
    {
339 3
        $byte = ord($bytes[0]);
340 3
        $result = sprintf('%d.%d', floor($byte / 40), $byte % 40);
341 3
        $len = strlen($bytes);
342 3
        $chunkBits = '';
343 3
        $maxIntBits = PHP_INT_SIZE * 8 - 1;
344 3
        for ($offset = 1; $offset < $len; $offset++) {
345 3
            $byte = ord($bytes[$offset]);
346 3
            $chunkBits .= str_pad(decbin($byte & 0b01111111), 7, '0', STR_PAD_LEFT);
347 3
            if (($byte & 0b10000000) === 0) {
348 3
                $result .= '.';
349 3
                if (strlen($chunkBits) <= $maxIntBits) {
350 3
                    $result .= (string) bindec($chunkBits);
351
                } else {
352
                    $result .= Math::createBigInteger($chunkBits, 2)->toString();
353
                }
354 3
                $chunkBits = '';
355
            }
356
        }
357 3
        if ($chunkBits !== '') {
358
            throw Asn1DecodingException::create();
359
        }
360
361 3
        return $result;
362
    }
363
364
    /**
365
     * Decode the value of a PrintableString element.
366
     *
367
     * @param string $bytes
368
     *
369
     * @return string
370
     */
371 3
    protected function decodePrintableString($bytes)
372
    {
373 3
        return $bytes;
374
    }
375
376
    /**
377
     * Decode the value of a GeneralizedTime element.
378
     *
379
     * @param string $bytes
380
     *
381
     * @throws \Ocsp\Exception\Asn1DecodingException
382
     *
383
     * @return \DateTimeImmutable
384
     */
385 2
    protected function decodeGeneralizedTime($bytes)
386
    {
387 2
        $matches = null;
388 2
        if (!preg_match('/(\d{4}\d{2}\d{2}\d{2}\d{2}\d{2})(?:\.(\d*))?Z$/', $bytes, $matches)) {
389
            throw Asn1DecodingException::create();
390
        }
391 2
        $dateTime = DateTimeImmutable::createFromFormat('!YmdHis.uT', $matches[1] . '.' . (isset($matches[2]) ? $matches[2] : '0') . 'UTC', new DateTimeZone('UTC'));
392 2
        $result = $dateTime->setTimezone(new DateTimeZone(date_default_timezone_get()));
393
394 2
        return $result;
395
    }
396
}
397