Passed
Push — master ( 22acbe...a6e730 )
by Théo
02:08
created

Signature   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 245
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 22
dl 0
loc 245
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
C get() 0 63 8
A handle() 0 7 2
A seek() 0 3 1
A close() 0 6 2
A __destruct() 0 3 1
A __construct() 0 7 1
A read() 0 7 1
B verify() 0 44 6
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 Assert\Assertion;
18
use KevinGH\Box\Verifier\Hash;
19
use KevinGH\Box\Verifier\PublicKeyDelegate;
20
use PharException;
21
22
/**
23
 * Retrieves and verifies a PHAR's signature without using the extension.
24
 *
25
 * While the PHAR extension is not used to retrieve or verify a PHAR's signature, other extensions may still be needed
26
 * to properly process the signature.
27
 */
28
final class Signature
29
{
30
    /**
31
     * The recognized PHAR signatures types.
32
     *
33
     * @var string
34
     */
35
    private const TYPES = [
36
        [
37
            'name' => 'MD5',
38
            'flag' => 0x01,
39
            'size' => 16,
40
            'class' => Hash::class,
41
        ],
42
        [
43
            'name' => 'SHA-1',
44
            'flag' => 0x02,
45
            'size' => 20,
46
            'class' => Hash::class,
47
        ],
48
        [
49
            'name' => 'SHA-256',
50
            'flag' => 0x03,
51
            'size' => 32,
52
            'class' => Hash::class,
53
        ],
54
        [
55
            'name' => 'SHA-512',
56
            'flag' => 0x04,
57
            'size' => 64,
58
            'class' => Hash::class,
59
        ],
60
        [
61
            'name' => 'OpenSSL',
62
            'flag' => 0x10,
63
            'size' => null,
64
            'class' => PublicKeyDelegate::class,
65
        ],
66
    ];
67
68
    /**
69
     * @var string The PHAR file path
70
     */
71
    private $file;
72
73
    /**
74
     * @var resource The file handle
75
     */
76
    private $handle;
77
78
    /**
79
     * @var int The size of the file
80
     */
81
    private $size;
82
83
    public function __construct(string $path)
84
    {
85
        Assertion::file($path);
86
87
        $this->file = realpath($path);
88
89
        $this->size = filesize($path);
90
    }
91
92
    public function __destruct()
93
    {
94
        $this->close();
95
    }
96
97
    /**
98
     * Returns the signature for the PHAR.
99
     *
100
     * The value returned is identical to that of `Phar->getSignature()`. If
101
     * $required is not given, it will default to the `phar.require_hash`
102
     * current value.
103
     *
104
     * @param bool $required Is the signature required?
105
     *
106
     * @throws PharException If the phar is not valid
107
     *
108
     * @return null|array The signature
109
     */
110
    public function get(bool $required = null): ?array
111
    {
112
        if (null === $required) {
113
            $required = (bool) ini_get('phar.require_hash');
114
        }
115
116
        $this->seek(-4, SEEK_END);
117
118
        if ('GBMB' !== $this->read(4)) {
119
            if ($required) {
120
                throw new PharException(
121
                    sprintf(
122
                        'The phar "%s" is not signed.',
123
                        $this->file
124
                    )
125
                );
126
            }
127
128
            return null;
129
        }
130
131
        $this->seek(-8, SEEK_END);
132
133
        $flag = unpack('V', $this->read(4));
134
        $flag = $flag[1];
135
136
        foreach (self::TYPES as $type) {
137
            if ($flag === $type['flag']) {
138
                break;
139
            }
140
141
            unset($type);
142
        }
143
144
        if (!isset($type)) {
145
            throw new PharException(
146
                sprintf(
147
                    'The signature type (%x) is not recognized for the phar "%s".',
148
                    $flag,
149
                    $this->file
150
                )
151
            );
152
        }
153
154
        $offset = -8;
155
156
        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 136. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
157
            $offset = -12;
158
159
            $this->seek(-12, SEEK_END);
160
161
            $type['size'] = unpack('V', $this->read(4));
162
            $type['size'] = $type['size'][1];
163
        }
164
165
        $this->seek($offset - $type['size'], SEEK_END);
166
167
        $hash = $this->read($type['size']);
168
        $hash = unpack('H*', $hash);
169
170
        return [
171
            'hash_type' => $type['name'],
172
            'hash' => strtoupper($hash[1]),
173
        ];
174
    }
175
176
    public function verify(): bool
177
    {
178
        $signature = $this->get();
179
180
        $size = $this->size;
181
        $type = null;
182
183
        foreach (self::TYPES as $type) {
184
            if ($type['name'] === $signature['hash_type']) {
185
                if (0x10 === $type['flag']) {
186
                    $this->seek(-12, SEEK_END);
187
188
                    $less = $this->read(4);
189
                    $less = unpack('V', $less);
190
                    $less = $less[1];
191
192
                    $size -= 12 + $less;
193
                } else {
194
                    $size -= 8 + $type['size'];
195
                }
196
197
                break;
198
            }
199
        }
200
201
        $this->seek(0);
202
203
        /** @var $verify Verifier */
204
        $verify = new $type['class']($type['name'], $this->file);
205
206
        $buffer = 64;
207
208
        while (0 < $size) {
209
            if ($size < $buffer) {
210
                $buffer = $size;
211
                $size = 0;
212
            }
213
214
            $verify->update($this->read($buffer));
215
216
            $size -= $buffer;
217
        }
218
219
        return $verify->verify($signature['hash']);
220
    }
221
222
    /**
223
     * Closes the open file handle.
224
     */
225
    private function close(): void
226
    {
227
        if (is_resource($this->handle)) {
228
            fclose($this->handle);
229
230
            $this->handle = null;
231
        }
232
    }
233
234
    /**
235
     * Returns the file handle. If the file handle is not opened, it will be automatically opened.
236
     *
237
     * @return resource the file handle
238
     */
239
    private function handle()
240
    {
241
        if (!$this->handle) {
242
            $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...
243
        }
244
245
        return $this->handle;
246
    }
247
248
    /**
249
     * Reads a number of bytes from the file.
250
     *
251
     * @param int $bytes the number of bytes
252
     *
253
     * @return string the read bytes
254
     */
255
    private function read(int $bytes): string
256
    {
257
        $read = fread($this->handle(), $bytes);
258
259
        Assertion::same(strlen($read), $bytes);
260
261
        return $read;
262
    }
263
264
    /**
265
     * Seeks to a specific point in the file.
266
     *
267
     * @param int $offset the offset to seek
268
     * @param int $whence the direction
269
     */
270
    private function seek(int $offset, int $whence = SEEK_SET): void
271
    {
272
        fseek($this->handle(), $offset, $whence);
273
    }
274
}
275