Test Failed
Pull Request — master (#667)
by
unknown
02:48
created

Info::getVersion()   A

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
nc 1
nop 0
dl 0
loc 3
rs 10
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * This file is based on code of tecnickcom/TCPDF PDF library.
5
 *
6
 * Original author Nicola Asuni ([email protected]) and
7
 * contributors (https://github.com/tecnickcom/TCPDF/graphs/contributors).
8
 *
9
 * @see https://github.com/tecnickcom/TCPDF
10
 *
11
 * Original code was licensed on the terms of the LGPL v3.
12
 *
13
 * ------------------------------------------------------------------------------
14
 *
15
 * @file This file is part of the PdfParser library.
16
 *
17
 * @author  Alastair Irvine <[email protected]>
18
 *
19
 * @date    2024-01-12
20
 *
21
 * @license LGPLv3
22
 *
23
 * @url     <https://github.com/smalot/pdfparser>
24
 *
25
 *  PdfParser is a pdf library written in PHP, extraction oriented.
26
 *  Copyright (C) 2017 - Sébastien MALOT <[email protected]>
27
 *
28
 *  This program is free software: you can redistribute it and/or modify
29
 *  it under the terms of the GNU Lesser General Public License as published by
30
 *  the Free Software Foundation, either version 3 of the License, or
31
 *  (at your option) any later version.
32
 *
33
 *  This program is distributed in the hope that it will be useful,
34
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
35
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
36
 *  GNU Lesser General Public License for more details.
37
 *
38
 *  You should have received a copy of the GNU Lesser General Public License
39
 *  along with this program.
40
 *  If not, see <http://www.pdfparser.org/sites/default/LICENSE.txt>.
41
 */
42
43
namespace Smalot\PdfParser\Encryption;
44
45
class Info
46
{
47
    protected $docId;
48
    protected $metaData;
49
    protected $ok = false;
50
51
    protected $fileKeyLength = 0;
52
    protected $encAlgorithm = null;
53
    protected $streamFilter = "";
54
    protected $stringFilter = "";
55
    protected $cfLength = 0;
56
57
58
    function __construct(array $rawMetadata, array $idArr)
59
    {
60
        /** @var
61
         * Associative array indexed by $this->metaData key, mapping to arrays:
62
         *   [ <index:string>, <numeric:bool> ]
63
         */
64
        $metadataTranslation = [
65
            'version' =>  [ 'V', true ],
66
            'revision' => [ 'R', true ],
67
            'length' =>   [ 'Length', true ],
68
            'ownerKey' => [ 'O', false ],
69
            'userKey' =>  [ 'U', false ],
70
            'ownerEnc' => [ 'OE', false ],
71
            'userEnc' =>  [ 'UE', false ],
72
            'perms' =>    [ 'P', true ]
73
        ];
74
75
        $this->metadata = ['encryptMetadata' => true];
0 ignored issues
show
Bug Best Practice introduced by
The property metadata does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
76
        // $rawMetadata is an array of one value being an array representing a PDF list
77
        $headerArr = $rawMetadata[0];
78
        if (\count($headerArr) == 3 && $headerArr[0] == '<<') {
79
            $headerDic = $headerArr[1];
80
        } else {
81
            throw new SyntaxError("Missing encryption header");
82
        }
83
        foreach ($metadataTranslation as $key => $info) {
84
            if ($info[1]) {
85
                $this->metadata[$key] = (int)\Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, $info[0], 'numeric');
86
            } else {
87
                // First look for a raw string
88
                $val = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, $info[0], '(', false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type array|null|string expected by parameter $default of Smalot\PdfParser\RawData...arser::getHeaderValue(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
                $val = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, $info[0], '(', /** @scrutinizer ignore-type */ false);
Loading history...
89
                if (false !== $val) {
90
                    $this->metadata[$key] = $val;
91
                } else {
92
                    // Then look for a hex string
93
                    $val = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, $info[0], '<');
94
                    $this->metadata[$key] = \hex2bin($val);
0 ignored issues
show
Bug introduced by
It seems like $val can also be of type array and null; however, parameter $string of hex2bin() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

94
                    $this->metadata[$key] = \hex2bin(/** @scrutinizer ignore-type */ $val);
Loading history...
95
                }
96
            }
97
        }
98
99
        // This should be an array
100
        try {
101
            $this->docID = $this->decodeDocID($idArr);
0 ignored issues
show
Bug Best Practice introduced by
The property docID does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
102
        }
103
        catch (\TypeError $e) {
104
            // or a scalar value, which is a spec breach
105
            $this->docID = "";
106
        }
107
108
        if ($this->metadata['version'] != 0 && $this->metadata['revision'] != 0
109
            && $this->metadata['perms'] != 0
110
            && is_string($this->metadata['ownerKey']) && is_string($this->metadata['userKey'])) {
0 ignored issues
show
introduced by
The condition is_string($this->metadata['ownerKey']) is always false.
Loading history...
111
            if (($this->metadata['revision'] <= 4 && strlen($this->metadata['ownerKey']) == 32 && strlen($this->metadata['userKey']) == 32)
112
                || (($this->metadata['revision'] == 5 || $this->metadata['revision'] == 6)
113
                    // the spec says 48 bytes, but Acrobat pads them out longer
114
                    && strlen($this->metadata['ownerKey']) >= 48 && strlen($this->metadata['userKey']) >= 48
115
                    && is_string($this->metadata['ownerEnc']) && strlen($this->metadata['ownerEnc']) == 32 && is_string($this->metadata['userEnc'])
116
                    && strlen($this->metadata['userEnc']) == 32)) {
117
                $this->encAlgorithm = 'RC4';
118
                // revision 2 forces a 40-bit key - some buggy PDF generators
119
                // set the Length value incorrectly
120
                if ($this->metadata['revision'] == 2 || $this->metadata['length'] == 0) {
121
                    $this->fileKeyLength = 5;
122
                } else {
123
                    $this->fileKeyLength = $this->metadata['length'] / 8;
124
                }
125
                $this->metadata['encryptMetadata'] = true;
126
                //~ this currently only handles a subset of crypt filter functionality
127
                //~ (in particular, it ignores the EFF entry in $headerDic, and
128
                //~ doesn't handle the case where StmF, StrF, and EFF are not all the
129
                //~ same)
130
                if (($this->metadata['version'] == 4 || $this->metadata['version'] == 5) && ($this->metadata['revision'] == 4 || $this->metadata['revision'] == 5 || $this->metadata['revision'] == 6)) {
131
                    $cryptFiltersDic = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, 'CF', '<<');
132
                    $this->metadata['streamFilter'] = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, 'StmF', '/');
133
                    $this->metadata['stringFilter'] = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, 'StrF', '/');
134
                    if (!empty($cryptFiltersDic) && is_string($this->metadata['streamFilter']) && is_string($this->metadata['stringFilter']) && $this->metadata['streamFilter'] == $this->metadata['stringFilter']) {
135
                        if ($this->metadata['streamFilter'] == "Identity") {
136
                            // no encryption on streams or strings
137
                            $this->metadata['version'] = $this->metadata['revision'] = -1;
138
                        } else {
139
                            // Find required crypt filter and its crypt filter method
140
                            // and update metadata accordingly
141
                            $cryptFilterInfoDic = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($cryptFiltersDic, $this->metadata['streamFilter'], '<<');
142
                            $method = \Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($cryptFilterInfoDic, 'CFM', '/');
143
                            switch ($method) {
144
                                case 'V2':
145
                                    $this->metadata['version'] = 2;
146
                                    $this->metadata['revision'] = 3;
147
                                    $this->metadata['cfLength'] = (int)\Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($cryptFilterInfoDic, 'Length', 'numeric', -1);
148
                                    if ($this->metadata['cfLength'] != 0) {
149
                                        //~ according to the spec, this should be cfLength / 8
150
                                        $this->fileKeyLength = $this->metadata['cfLength'];
151
                                    }
152
                                    break;
153
154
                                case 'AESV2':
155
                                    $this->metadata['version'] = 2;
156
                                    $this->metadata['revision'] = 3;
157
                                    $this->encAlgorithm = 'AES';
158
                                    $this->metadata['cfLength'] = (int)\Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($cryptFilterInfoDic, 'Length', 'numeric', -1);
159
                                    if ($this->metadata['cfLength'] != 0) {
160
                                        //~ according to the spec, this should be cfLength / 8
161
                                        $this->fileKeyLength = $this->metadata['cfLength'];
162
                                    }
163
                                    break;
164
165
                                case 'AESV3':
166
                                    $this->metadata['version'] = 5;
167
                                    // let $this->metadata['revision'] be 5 or 6
168
                                    $this->encAlgorithm = 'AES256';
169
                                    $this->metadata['cfLength'] = (int)\Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($cryptFilterInfoDic, 'Length', 'numeric', -1);
170
                                    if ($this->metadata['cfLength'] != 0) {
171
                                        //~ according to the spec, this should be cfLengthArr / 8
172
                                        $this->fileKeyLength = $this->metadata['cfLength'];
173
                                    }
174
                                    break;
175
176
                                default:
177
                                    throw new SyntaxError("Unknown CFM '$method'");
178
                            }
179
                        }
180
                    }
181
                    $this->metadata['encryptMetadata'] = (\Smalot\PdfParser\RawData\RawDataParser::getHeaderValue($headerDic, 'EncryptMetadata', 'boolean') === "true");
182
                }
183
                if ($this->metadata['version'] >= 1 && $this->metadata['version'] <= 2 && $this->metadata['revision'] >= 2 && $this->metadata['revision'] <= 3) {
184
                    if ($this->fileKeyLength > 16 || $this->fileKeyLength < 0) {
185
                        $this->fileKeyLength = 16;
186
                    }
187
                    $this->ok = true;
188
                } elseif ($this->metadata['version'] == 5 && ($this->metadata['revision'] == 5 || $this->metadata['revision'] == 6)) {
189
                    if (is_string($this->metadata['ownerEnc']) && is_string($this->metadata['userEnc'])) {
190
                        if ($this->fileKeyLength > 32 || $this->fileKeyLength < 0) {
191
                            $this->fileKeyLength = 32;
192
                        }
193
                        $this->ok = true;
194
                    } else {
195
                        throw new SyntaxError("Weird encryption owner/user info");
196
                    }
197
                } elseif (!($this->version == -1 && $this->revision == -1)) {
198
                    throw new Unimplemented("Unsupported version/revision (%d/%d) of Standard security handler", $this->version, $this->revision);
199
                }
200
            } else {
201
                throw new SyntaxError("Invalid encryption key length");
202
            }
203
        } else {
204
            throw new SyntaxError("Weird encryption info");
205
        }
206
    }
207
208
209
    /**
210
     * Get an element from the array of IDs and convert it from hex.
211
     *
212
     * @return a binary string
213
     */
214
    protected function decodeDocID(array $idArr)
215
    {
216
        // If multiple elements, assume that the first one is correct
217
        $result = \hex2bin($idArr[0]);
218
        if ($result === false)
219
        {
220
            throw new SyntaxError("Can't decode DocID");
221
        }
222
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type string which is incompatible with the documented return type Smalot\PdfParser\Encryption\a.
Loading history...
223
    }
224
225
226
    public function getVersion()
227
    {
228
        return $this->metadata['version'];
229
    }
230
231
232
    public function getRevision()
233
    {
234
        return $this->metadata['revision'];
235
    }
236
237
238
    public function getLength()
239
    {
240
        return $this->metadata['length'];
241
    }
242
243
244
    public function getOwnerKey()
245
    {
246
        return $this->metadata['ownerKey'];
247
    }
248
249
250
    public function getUserKey()
251
    {
252
        return $this->metadata['userKey'];
253
    }
254
255
256
    public function getOwnerEnc()
257
    {
258
        return $this->metadata['ownerEnc'];
259
    }
260
261
262
    public function getUserEnc()
263
    {
264
        return $this->metadata['userEnc'];
265
    }
266
267
268
    public function getPerms()
269
    {
270
        return $this->metadata['perms'];
271
    }
272
273
274
    public function getEncryptMetadata()
275
    {
276
        return $this->metadata['encryptMetadata'];
277
    }
278
279
280
    public function getDocID()
281
    {
282
        return $this->docID;
283
    }
284
285
286
    public function getEncAlgorithm()
287
    {
288
        return $this->encAlgorithm;
289
    }
290
291
292
    public function getFileKeyLength()
293
    {
294
        return $this->fileKeyLength;
295
    }
296
}
297