Completed
Push — master ( bc7d7f...7a9596 )
by Théo
02:46
created

Signature::get()   C

Complexity

Conditions 8
Paths 22

Size

Total Lines 63
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 63
c 0
b 0
f 0
rs 6.8825
cc 8
eloc 35
nc 22
nop 1

How to fix   Long Method   

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
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box;
16
17
use KevinGH\Box\Exception\Exception;
18
use KevinGH\Box\Exception\FileException;
19
use KevinGH\Box\Exception\OpenSslException;
20
use KevinGH\Box\Signature\Hash;
21
use KevinGH\Box\Signature\PublicKeyDelegate;
22
use KevinGH\Box\Signature\VerifyInterface;
23
use PharException;
24
25
/**
26
 * Retrieves and verifies a phar's signature without using the extension.
27
 *
28
 * While the phar extension is not used to retrieve or verify a phar's
29
 * signature, other extensions may still be needed to properly process
30
 * the signature.
31
 *
32
 * @author Kevin Herrera <[email protected]>
33
 */
34
class Signature
35
{
36
    /**
37
     * The phar file path.
38
     *
39
     * @var string
40
     */
41
    private $file;
42
43
    /**
44
     * The file handle.
45
     *
46
     * @var resource
47
     */
48
    private $handle;
49
50
    /**
51
     * The size of the file.
52
     *
53
     * @var int
54
     */
55
    private $size;
56
57
    /**
58
     * The recognized signature types.
59
     *
60
     * @var array
61
     */
62
    private static $types = [
63
        [
64
            'name' => 'MD5',
65
            'flag' => 0x01,
66
            'size' => 16,
67
            'class' => Hash::class,
68
        ],
69
        [
70
            'name' => 'SHA-1',
71
            'flag' => 0x02,
72
            'size' => 20,
73
            'class' => Hash::class,
74
        ],
75
        [
76
            'name' => 'SHA-256',
77
            'flag' => 0x03,
78
            'size' => 32,
79
            'class' => Hash::class,
80
        ],
81
        [
82
            'name' => 'SHA-512',
83
            'flag' => 0x04,
84
            'size' => 64,
85
            'class' => Hash::class,
86
        ],
87
        [
88
            'name' => 'OpenSSL',
89
            'flag' => 0x10,
90
            'size' => null,
91
            'class' => PublicKeyDelegate::class,
92
        ],
93
    ];
94
95
    /**
96
     * Sets the phar file path.
97
     *
98
     * @param string $path the phar file path
99
     *
100
     * @throws Exception
101
     * @throws FileException if the file does not exist
102
     */
103
    public function __construct($path)
104
    {
105
        if (!is_file($path)) {
106
            throw FileException::create(
107
                'The path "%s" does not exist or is not a file.',
108
                $path
109
            );
110
        }
111
112
        $this->file = realpath($path);
0 ignored issues
show
Documentation Bug introduced by
It seems like realpath($path) can also be of type false. However, the property $file is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
113
114
        if (false === ($this->size = @filesize($path))) {
115
            throw FileException::lastError();
116
        }
117
    }
118
119
    /**
120
     * Closes the open file handle.
121
     */
122
    public function __destruct()
123
    {
124
        $this->close();
125
    }
126
127
    /**
128
     * Creates a new instance of Signature.
129
     *
130
     * @param string $path the phar file path
131
     *
132
     * @return Signature the new instance
133
     */
134
    public static function create($path)
135
    {
136
        return new self($path);
137
    }
138
139
    /**
140
     * Returns the signature for the phar.
141
     *
142
     * The value returned is identical to that of `Phar->getSignature()`. If
143
     * $required is not given, it will default to the `phar.require_hash`
144
     * current value.
145
     *
146
     * @param bool $required Is the signature required?
147
     *
148
     * @throws PharException if the phar is not valid
149
     *
150
     * @return array the signature
151
     */
152
    public function get($required = null)
153
    {
154
        if (null === $required) {
155
            $required = (bool) ini_get('phar.require_hash');
156
        }
157
158
        $this->seek(-4, SEEK_END);
159
160
        if ('GBMB' !== $this->read(4)) {
161
            if ($required) {
162
                throw new PharException(
163
                    sprintf(
164
                        'The phar "%s" is not signed.',
165
                        $this->file
166
                    )
167
                );
168
            }
169
170
            return null;
171
        }
172
173
        $this->seek(-8, SEEK_END);
174
175
        $flag = unpack('V', $this->read(4));
0 ignored issues
show
Bug introduced by
The call to unpack() has too few arguments starting with offset. ( Ignorable by Annotation )

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

175
        $flag = /** @scrutinizer ignore-call */ unpack('V', $this->read(4));

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
176
        $flag = $flag[1];
177
178
        foreach (self::$types as $type) {
179
            if ($flag === $type['flag']) {
180
                break;
181
            }
182
183
            unset($type);
184
        }
185
186
        if (!isset($type)) {
187
            throw new PharException(
188
                sprintf(
189
                    'The signature type (%x) is not recognized for the phar "%s".',
190
                    $flag,
191
                    $this->file
192
                )
193
            );
194
        }
195
196
        $offset = -8;
197
198
        if (0x10 === $type['flag']) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $type seems to be defined by a foreach iteration on line 178. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
199
            $offset = -12;
200
201
            $this->seek(-12, SEEK_END);
202
203
            $type['size'] = unpack('V', $this->read(4));
204
            $type['size'] = $type['size'][1];
205
        }
206
207
        $this->seek($offset - $type['size'], SEEK_END);
208
209
        $hash = $this->read($type['size']);
210
        $hash = unpack('H*', $hash);
211
212
        return [
213
            'hash_type' => $type['name'],
214
            'hash' => strtoupper($hash[1]),
215
        ];
216
    }
217
218
    /**
219
     * Verifies the signature of the phar.
220
     *
221
     * @throws Exception
222
     * @throws FileException    if the private key could not be read
223
     * @throws OpenSslException if there is an OpenSSL error
224
     *
225
     * @return bool TRUE if verified, FALSE if not
226
     */
227
    public function verify()
228
    {
229
        $signature = $this->get();
230
231
        $size = $this->size;
232
        $type = null;
233
234
        foreach (self::$types as $type) {
235
            if ($type['name'] === $signature['hash_type']) {
236
                if (0x10 === $type['flag']) {
237
                    $this->seek(-12, SEEK_END);
238
239
                    $less = $this->read(4);
240
                    $less = unpack('V', $less);
0 ignored issues
show
Bug introduced by
The call to unpack() has too few arguments starting with offset. ( Ignorable by Annotation )

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

240
                    $less = /** @scrutinizer ignore-call */ unpack('V', $less);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
241
                    $less = $less[1];
242
243
                    $size -= 12 + $less;
244
                } else {
245
                    $size -= 8 + $type['size'];
246
                }
247
248
                break;
249
            }
250
        }
251
252
        $this->seek(0);
253
254
        /** @var $verify VerifyInterface */
255
        $verify = new $type['class']();
256
        $verify->init($type['name'], $this->file);
257
258
        $buffer = 64;
259
260
        while (0 < $size) {
261
            if ($size < $buffer) {
262
                $buffer = $size;
263
                $size = 0;
264
            }
265
266
            $verify->update($this->read($buffer));
267
268
            $size -= $buffer;
269
        }
270
271
        return $verify->verify($signature['hash']);
272
    }
273
274
    /**
275
     * Closes the open file handle.
276
     */
277
    private function close(): void
278
    {
279
        if (is_resource($this->handle)) {
280
            @fclose($this->handle);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

280
            /** @scrutinizer ignore-unhandled */ @fclose($this->handle);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
281
282
            $this->handle = null;
283
        }
284
    }
285
286
    /**
287
     * Returns the file handle.
288
     *
289
     * If the file handle is not opened, it will be automatically opened.
290
     *
291
     * @throws Exception
292
     * @throws FileException if the file could not be opened
293
     *
294
     * @return resource the file handle
295
     */
296
    private function handle()
297
    {
298
        if (!$this->handle) {
299
            if (!($this->handle = @fopen($this->file, 'rb'))) {
0 ignored issues
show
Documentation Bug introduced by
It seems like @fopen($this->file, 'rb') can also be of type false. However, the property $handle is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
300
                throw FileException::lastError();
301
            }
302
        }
303
304
        return $this->handle;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->handle could also return false which is incompatible with the documented return type resource. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
305
    }
306
307
    /**
308
     * Reads a number of bytes from the file.
309
     *
310
     * @param int $bytes the number of bytes
311
     *
312
     * @throws Exception
313
     * @throws FileException if the file could not be read
314
     *
315
     * @return string the read bytes
316
     */
317
    private function read($bytes)
318
    {
319
        if (false === ($read = @fread($this->handle(), $bytes))) {
320
            throw FileException::lastError();
321
        }
322
323
        if (($actual = strlen($read)) !== $bytes) {
324
            throw FileException::create(
325
                'Only read %d of %d bytes from "%s".',
326
                $actual,
327
                $bytes,
328
                $this->file
329
            );
330
        }
331
332
        return $read;
333
    }
334
335
    /**
336
     * Seeks to a specific point in the file.
337
     *
338
     * @param int $offset the offset to seek
339
     * @param int $whence the direction
340
     *
341
     * @throws Exception
342
     * @throws FileException if the file could not be seeked
343
     */
344
    private function seek($offset, $whence = SEEK_SET): void
345
    {
346
        if (-1 === @fseek($this->handle(), $offset, $whence)) {
347
            throw FileException::lastError();
348
        }
349
    }
350
}
351