Decoder::fromString()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
/* Based on PHPSECLIB: https://github.com/phpseclib/phpseclib/blob/master/phpseclib/File/ASN1.php */
3
4
namespace vakata\asn1;
5
6
use DateTime;
7
8
/**
9
 * A class handling ASN1 decoding.
10
 */
11
class Decoder
12
{
13
    protected $reader;
14
15
    /**
16
     * Create an instance by passing in an instantiated reader.
17
     *
18
     * @param Reader $reader
19
     */
20
    public function __construct(Reader $reader)
21
    {
22
        $this->reader = $reader;
23
    }
24
    /**
25
     * Create a new instance from an ASN1 string.
26
     *
27
     * @param string $data the ASN1 data
28
     * @return Decoder
29
     */
30
    public static function fromString($data)
31
    {
32
        return new static(Reader::fromString($data));
33
    }
34
    /**
35
     * Create a new instance from a file.
36
     *
37
     * @param string $path the path to the file to parse
38
     * @return Decoder
39
     */
40
    public static function fromFile($path)
41
    {
42
        return new static(Reader::fromFile($path));
43
    }
44
45
    public function getReader()
46
    {
47
        return $this->reader;
48
    }
49
50
    protected function header()
51
    {
52
        $start = $this->reader->pos();
53
        $identifier = ord($this->reader->byte());
54
        $constructed = ($identifier >> 5) & 1; // 6th bit
55
        $class = ($identifier >> 6) & 3; // 7th and 8th bits
56
        $tag = $identifier & 31; // first 5 bits
57
        if ($tag === 31) { // long tag (read each 7 bits until the 8th is 0)
58
            $tag = 0;
59
            while (true) {
60
                $temp = ord($this->reader->byte());
61
                $tag <<= 7;
62
                $tag |= $temp & 127;
63
                if (($temp & 128) === 0) {
64
                    break;
65
                }
66
            }
67
        }
68
        if ($tag === 0 && $class === 0) {
69
            return [
70
                'constructed' => $constructed,
71
                'class' => $class,
72
                'tag' => $tag,
73
                'start' => $start,
74
                'length' => ($this->reader->pos() - $start),
75
                'content_start' => $this->reader->pos(),
76
                'content_length' => 0
77
            ];
78
        }
79
        $temp = ord($this->reader->byte());
80
        $length = null;
81
        if ($temp === 128) {
82
            // indefinite
83
            $length = null;
84
        } elseif ($temp & 128) {
85
            // long form
86
            $octets = $temp & 127;
87
            $length = 0;
88
            for ($i = 0; $i < $octets; $i++) {
89
                $length <<= 8;
90
                $length |= ord($this->reader->byte());
91
            }
92
        } else {
93
            // short form
94
            $length = $temp;
95
        }
96
        return [
97
            'constructed' => $constructed,
98
            'class' => $class,
99
            'tag' => $tag,
100
            'start' => $start,
101
            'length' => $length !== null ? $length + ($this->reader->pos() - $start) : null,
102
            'content_start' => $this->reader->pos(),
103
            'content_length' => $length
104
        ];
105
    }
106
    protected function decode($header)
107
    {
108
        $contents = $header['content_length'] > 0 ?
109
            $this->reader->chunk($header['content_start'], $header['content_length']) :
110
            '';
111
        if ($header['class'] !== ASN1::CLASS_UNIVERSAL) {
112
            return $contents;
113
        }
114
        return $this->decodeContents($header['tag'], $header['constructed'], $contents);
115
    }
116
    protected function decodeContents($tag, $constructed, $contents)
117
    {
118
        switch ($tag) {
119
            case ASN1::TYPE_BOOLEAN:
120
                return (bool)ord($contents[0]);
121
            case ASN1::TYPE_INTEGER:
122
                return ASN1::fromBase256($contents);
123
            case ASN1::TYPE_ENUMERATED:
124
                return (int)base_convert(ASN1::fromBase256($contents), 2, 10);
125
            case ASN1::TYPE_REAL:
126
                // TODO: read the specs
127
                return false;
128
            case ASN1::TYPE_BIT_STRING:
129
                if ($constructed) {
130
                    $temp = static::fromString($contents)->values();
131
                    $real = '';
132
                    for ($i = 0; $i < count($temp) - 1; $i++) {
133
                        $real .= $temp['value'];
134
                    }
135
                    return $temp[count($temp) - 1]['value'][0] . $real . substr($temp[$i]['value'], 1);
136
                }
137
                return $contents;
138
            case ASN1::TYPE_OCTET_STRING:
139
                if ($constructed) {
140
                    return implode('', array_map(function ($v) {
141
                        if (!is_array($v)) {
142
                            return $v;
143
                        }
144
                        return $v['value'];
145
                    }, static::fromString($contents)->values()));
146
                }
147
                return $contents;
148
            case ASN1::TYPE_BMP_STRING:
149
                return extension_loaded("iconv") ? iconv('UCS-2BE', 'UTF-8', $contents) : $contents;
150
            case ASN1::TYPE_UNIVERSAL_STRING:
151
                return extension_loaded("iconv") ? iconv('UCS-4BE', 'UTF-8', $contents) : $contents;
152
            case ASN1::TYPE_NULL:
153
                return null;
154
            case ASN1::TYPE_UTC_TIME:
155
                $format = 'YmdHis';
156
                $matches = [];
157
                if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $contents, $matches)) {
158
                    $contents = $matches[1] . '00' . $matches[2];
159
                }
160
                $prefix = substr($contents, 0, 2) >= 50 ? '19' : '20';
161
                $contents = $prefix . $contents;
162
                if ($contents[strlen($contents) - 1] == 'Z') {
163
                    $contents = substr($contents, 0, -1) . '+0000';
164
                }
165
                if (strpos($contents, '-') !== false || strpos($contents, '+') !== false) {
166
                    $format .= 'O';
167
                }
168
                $result = @DateTime::createFromFormat($format, $contents);
169
                return $result ? $result->getTimestamp() : false;
170
            case ASN1::TYPE_GENERALIZED_TIME:
171
                $format = 'YmdHis';
172
                if (strpos($contents, '.') !== false) {
173
                    $format .= '.v';
174
                }
175
                if ($contents[strlen($contents) - 1] == 'Z') {
176
                    $contents = substr($contents, 0, -1) . '+0000';
177
                }
178
                if (strpos($contents, '-') !== false || strpos($contents, '+') !== false) {
179
                    $format .= 'O';
180
                }
181
                $result = @DateTime::createFromFormat($format, $contents);
182
                return $result ? $result->format(strpos($contents, '.') !== false ? 'U.v' : 'U') : false;
183
            case ASN1::TYPE_OBJECT_IDENTIFIER:
184
                $temp = ord($contents[0]);
185
                $real = sprintf('%d.%d', floor($temp / 40), $temp % 40);
186
                $obid = 0;
187
                // process septets
188
                for ($i = 1; $i < strlen($contents); $i++) {
189
                    $temp = ord($contents[$i]);
190
                    $obid <<= 7;
191
                    $obid |= $temp & 0x7F;
192
                    if (~$temp & 0x80) {
193
                        $real .= '.' . $obid;
194
                        $obid = 0;
195
                    }
196
                }
197
                return $real;
198
            default:
199
                return $contents;
200
        }
201
    }
202
    /**
203
     * Dump the parsed structure of the ASN1 data.
204
     *
205
     * @param mixed $max internal - do not use
206
     * @return mixed in most cases this is an array, as all complex structures are either a sequence or a set
207
     */
208
    public function structure($max = null)
209
    {
210
        $skeleton = [];
211
        while (!$this->reader->eof() && ($max === null || $this->reader->pos() < $max)) {
212
            $header = $this->header();
213
            if ($header['class'] === 0 && $header['tag'] === 0) {
214
                if ($max === null) {
215
                    break;
216
                } else {
217
                    continue;
218
                }
219
            }
220
            if ($header['class'] !== ASN1::CLASS_UNIVERSAL && $header['constructed']) {
221
                $header['children'] = $this->structure(
222
                    $header['length'] ? $header['start'] + $header['length'] - 1 : null
223
                );
224
                if ($header['length'] === null) {
225
                    $this->reader->byte();
226
                    $header['length'] = $this->reader->pos() - $header['start'];
227
                    $header['content_length'] = $this->reader->pos() - $header['content_start'];
228
                }
229
                $skeleton[] = $header;
230
            } else {
231
                if ($header['class'] === ASN1::CLASS_UNIVERSAL &&
232
                    in_array($header['tag'], [ASN1::TYPE_SET, ASN1::TYPE_SEQUENCE])
233
                ) {
234
                    $header['children'] = $this->structure(
235
                        $header['length'] ? $header['start'] + $header['length'] - 1 : null
236
                    );
237
                    if ($header['length'] === null) {
238
                        $this->reader->byte();
239
                        $header['length'] = $this->reader->pos() - $header['start'];
240
                        $header['content_length'] = $this->reader->pos() - $header['content_start'];
241
                    }
242
                } else {
243
                    if ($header['length'] === null) {
244
                        $this->reader->readUntil(chr(0).chr(0));
245
                        $header['length'] = $this->reader->pos() - $header['start'];
246
                        $header['content_length'] = $this->reader->pos() - $header['content_start'];
247
                    } else {
248
                        if ($header['content_length'] > 0) {
249
                            $this->reader->bytes($header['content_length']);
250
                        }
251
                    }
252
                }
253
                if (!isset($header['children'])) {
254
                    $pos = $this->reader->pos();
255
                    $header['value'] = $this->decode($header);
256
                    $this->reader->seek($pos);
257
                }
258
                $skeleton[] = $header;
259
            }
260
        }
261
        return $skeleton;
262
    }
263
    /**
264
     * Dump the parsed values only.
265
     *
266
     * @param mixed $skeleton internal - do not use
267
     * @return mixed in most cases this is an array, as all complex structures are either a sequence or a set
268
     */
269
    public function values($skeleton = null)
270
    {
271
        $skeleton = $skeleton ?? $this->structure();
272
        foreach ($skeleton as $k => $v) {
273
            if (isset($v['children'])) {
274
                $skeleton[$k] = $this->values($v['children']);
275
            } else {
276
                $skeleton[$k] = $v['value'] ?? null;
277
            }
278
        }
279
        return $skeleton;
280
    }
281
    /**
282
     * Map the parsed data to a map
283
     *
284
     * @param array $map the map to use - look in the structure classes for example map arrays
285
     * @param mixed $skeleton internal - do not use
286
     * @return mixed in most cases this is an array, as all complex structures are either a sequence or a set
287
     */
288
    public function map($map, $skeleton = null)
289
    {
290
        $null = null;
291
        if ($skeleton === null && $this->reader->pos() !== 0) {
292
            $this->reader->rewind();
293
        }
294
        $skeleton = $skeleton ?? $this->structure()[0] ?? null;
295
        if (!isset($skeleton)) {
296
            throw new ASN1Exception('No decoded data for map');
297
        }
298
        if ($skeleton['class'] !== ASN1::CLASS_UNIVERSAL) {
299
            if ($map['tag'] === ASN1::TYPE_CHOICE) {
300
                foreach ($map['children'] as $child) {
301
                    if (isset($child['name']) && (int)$skeleton['tag'] === (int)$child['name']) {
302
                        $map = $child;
303
                        if (isset($child['value']) && $child['value']) {
304
                            return $child['value'];
305
                        }
306
                        break;
307
                    }
308
                }
309
            }
310
        }
311
        if ($skeleton['class'] !== ASN1::CLASS_UNIVERSAL) {
312
            if (isset($map['implicit']) && $map['implicit']) {
313
                $skeleton['class'] = ASN1::CLASS_UNIVERSAL;
314
                $skeleton['tag'] = $map['tag'];
315
                if (isset($skeleton['value'])) {
316
                    $skeleton['value'] = $this->decodeContents($skeleton['tag'], true, $skeleton['value']);
317
                }
318
            } else {
319
                $skeleton = $skeleton['children'][0] ?? null;
320
            }
321
        }
322
        if ($map['tag'] === ASN1::TYPE_CHOICE) {
323
            foreach ($map['children'] as $child) {
324
                if ($skeleton['tag'] === $child['tag']) {
325
                    $map = $child;
326
                    if (isset($child['value']) && $child['value']) {
327
                        return $child['value'];
328
                    }
329
                    break;
330
                }
331
            }
332
        }
333
        if (in_array($map['tag'], [ASN1::TYPE_SEQUENCE, ASN1::TYPE_SET]) &&
334
            in_array($skeleton['tag'], [ASN1::TYPE_SEQUENCE, ASN1::TYPE_SET])) {
335
            $map['tag'] = $skeleton['tag'];
336
        }
337
        if ($map['tag'] === ASN1::TYPE_ANY && isset($skeleton['tag'])) {
338
            $map['tag'] = $skeleton['tag'];
339
        }
340
        if (!in_array($map['tag'], [ASN1::TYPE_ANY, ASN1::TYPE_ANY_RAW, ASN1::TYPE_ANY_SKIP, ASN1::TYPE_ANY_DER]) &&
341
            $map['tag'] !== $skeleton['tag']
342
        ) {
343
            if (!isset($map['optional']) || !$map['optional']) {
344
                throw new ASN1Exception('Decoded data does not match mapping - ' . $skeleton['tag']);
345
            }
346
            return $null;
347
        } else {
348
            switch ($map['tag']) {
349
                case ASN1::TYPE_ANY_DER:
350
                    $temp = $this->reader->chunk($skeleton['start'], $skeleton['length']);
351
                    return $temp;
352
                case ASN1::TYPE_ANY_SKIP:
353
                    return $null;
354
                case ASN1::TYPE_ANY_RAW:
355
                    return $skeleton['value'] ?? null;
356
                case ASN1::TYPE_SET:
357
                    if (isset($map['repeat'])) {
358
                        $result = [];
359
                        foreach ($skeleton['children'] as $v) {
360
                            $result[] = $this->map($map['repeat'], $v);
361
                        }
362
                        return $result;
363
                    } else {
364
                        if (!isset($map['children'])) {
365
                            return $null;
366
                        }
367
                        $temp = $skeleton['children'];
368
                        $result = [];
369
                        // named first
370
                        foreach ($map['children'] as $k => $v) {
371
                            if (isset($v['name'])) {
372
                                $result[$k] = null;
373
                                foreach ($temp as $kk => $vv) {
374
                                    if ($vv['class'] !== ASN1::CLASS_UNIVERSAL && (int)$v['name'] === $vv['tag']) {
375
                                        try {
376
                                            if (isset($v['implicit']) && $v['implicit']) {
377
                                                $vv['class'] = ASN1::CLASS_UNIVERSAL;
378
                                                $vv['tag'] = $map['tag'];
379
                                                if (isset($vv['value'])) {
380
                                                    $vv['value'] = $this->decodeContents($vv['tag'], true, $vv['value']);
381
                                                }
382
                                            } else {
383
                                                $vv = $vv['children'][0] ?? null;
384
                                            }
385
                                            $result[$k] = $this->map($v, $vv);
386
                                            unset($temp[$kk]);
387
                                            break;
388
                                        } catch (ASN1Exception $e) {
389
                                            // continue trying other children in case of failure
390
                                        }
391
                                    }
392
                                }
393
                                if ($result[$k] === null && (!isset($v['optional']) || !$v['optional'])) {
394
                                    throw new ASN1Exception('Missing tagged type - ' . $k);
395
                                }
396
                            }
397
                        }
398
                        foreach ($map['children'] as $k => $v) {
399
                            if (isset($v['name'])) {
400
                                continue;
401
                            }
402
                            $result[$k] = null;
403
                            foreach ($temp as $kk => $vv) {
404
                                if ($v['tag'] === $vv['tag'] ||
405
                                    in_array(
406
                                        $v['tag'],
407
                                        [
408
                                            ASN1::TYPE_ANY,
409
                                            ASN1::TYPE_ANY_DER,
410
                                            ASN1::TYPE_ANY_RAW,
411
                                            ASN1::TYPE_ANY_SKIP,
412
                                            ASN1::TYPE_CHOICE
413
                                        ]
414
                                    )
415
                                ) {
416
                                    try {
417
                                        $result[$k] = $this->map($v, $vv);
418
                                        unset($temp[$kk]);
419
                                        break;
420
                                    } catch (ASN1Exception $e) {
421
                                        $result[$k] = null;
422
                                    }
423
                                }
424
                            }
425
                            if ($result[$k] === null && (!isset($v['optional']) || !$v['optional'])) {
426
                                throw new ASN1Exception('Decoded data does not match mapping - ' . $k);
427
                            }
428
                        }
429
                        return $result;
430
                    }
431
                    break;
432
                case ASN1::TYPE_SEQUENCE:
433
                    if (isset($map['repeat'])) {
434
                        $result = [];
435
                        foreach ($skeleton['children'] as $v) {
436
                            $result[] = $this->map($map['repeat'], $v);
437
                        }
438
                        return $result;
439
                    } else {
440
                        if (!isset($map['children'])) {
441
                            return $null;
442
                        }
443
                        $result = [];
444
                        foreach ($skeleton['children'] as $kk => $vv) {
445
                            foreach ($map['children'] as $k => $v) {
446
                                if (isset($v['name'])) {
447
                                    $result[$k] = null;
448
                                    if ($vv['class'] !== ASN1::CLASS_UNIVERSAL && (int)$v['name'] === $vv['tag']) {
449
                                        if (isset($v['implicit']) && $v['implicit']) {
450
                                            $vv['class'] = ASN1::CLASS_UNIVERSAL;
451
                                            $vv['tag'] = $v['tag'];
452
                                            if (isset($vv['value'])) {
453
                                                $vv['value'] = $this->decodeContents($vv['tag'], true, $vv['value']);
454
                                            }
455
                                        } else {
456
                                            $vv = $vv['children'][0] ?? null;
457
                                        }
458
                                        $result[$k] = $this->map($v, $vv);
459
                                        unset($map['children'][$k]);
460
                                        break;
461
                                    } else {
462
                                        if (!isset($v['optional']) || !$v['optional']) {
463
                                            throw new ASN1Exception('Missing tagged type - ' . $k);
464
                                        }
465
                                    }
466
                                    unset($map['children'][$k]);
467
                                    continue;
468
                                }
469
                                if ($v['tag'] === $vv['tag'] ||
470
                                    in_array(
471
                                        $v['tag'],
472
                                        [
473
                                            ASN1::TYPE_ANY,
474
                                            ASN1::TYPE_ANY_DER,
475
                                            ASN1::TYPE_ANY_RAW,
476
                                            ASN1::TYPE_ANY_SKIP,
477
                                            ASN1::TYPE_CHOICE
478
                                        ]
479
                                    )
480
                                ) {
481
                                    try {
482
                                        unset($map['children'][$k]);
483
                                        $result[$k] = $this->map($v, $vv);
484
                                        break;
485
                                    } catch (ASN1Exception $e) {
486
                                        // continue trying other children in case of failure
487
                                    }
488
                                }
489
                                if (!isset($v['optional']) || !$v['optional']) {
490
                                    throw new ASN1Exception('Missing type - ' . $k);
491
                                } else {
492
                                    $result[$k] = null;
493
                                    unset($map['children'][$k]);
494
                                }
495
                            }
496
                        }
497
                        return $result;
498
                    }
499
                    break;
500
                case ASN1::TYPE_OBJECT_IDENTIFIER:
501
                    $temp = isset($map['resolve']) && $map['resolve'] ?
502
                        ASN1::OIDtoText($skeleton['value']) :
503
                        $skeleton['value'];
504
                    return $temp;
505
                case ASN1::TYPE_OCTET_STRING:
506
                    if (isset($map['der']) && $map['der']) {
507
                        $temp = static::fromString($skeleton['value']);
508
                        $temp = isset($map['map']) ? $temp->map($map['map']) : $temp->values();
509
                        return $temp;
510
                    } else {
511
                        $temp = isset($map['raw']) && $map['raw'] ?
512
                            $skeleton['value'] :
513
                            base64_encode($skeleton['value']);
514
                        return $temp;
515
                    }
516
                    break;
517
                case ASN1::TYPE_INTEGER:
518
                    $base = isset($map['base']) && (int)$map['base'] ? (int)$map['base'] : 10;
519
                    if ($base < 3) {
520
                        $result = $skeleton['value'];
521
                    } else {
522
                        if (strlen($skeleton['value']) > 53 && $base === 16) {
523
                            $hex = '';
524
                            for ($i = strlen($skeleton['value']) - 4; $i >= 0; $i-=4) {
525
                                $hex .= dechex((int)bindec(substr($skeleton['value'], $i, 4)));
526
                            }
527
                            $result = strrev($hex);
528
                        } else {
529
                            $temp = base_convert($skeleton['value'], 2, $base);
530
                            if ($base === 10) {
531
                                $temp = (int)$temp;
532
                            }
533
                            $result = $temp;
534
                        }
535
                    }
536
                    if (isset($map['map']) && isset($map['map'][$result])) {
537
                        $result = $map['map'][$result];
538
                    }
539
                    return $result;
540
                case ASN1::TYPE_UTC_TIME:
541
                case ASN1::TYPE_GENERALIZED_TIME:
542
                    return $skeleton['value'];
543
                default:
544
                    $result = $skeleton['value'];
545
                    if (isset($map['map']) && isset($map['map'][$result])) {
546
                        $result = $map['map'][$result];
547
                    }
548
                    return $result;
549
            }
550
        }
551
    }
552
}
553