Passed
Push — master ( 3b7697...25d5dd )
by Alexey
03:48 queued 29s
created

WinZipAesDecryptionStreamFilter::onClose()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 2
eloc 3
c 1
b 0
f 1
nc 2
nop 0
dl 0
loc 6
rs 10
1
<?php
2
3
namespace PhpZip\IO\Filter\Cipher\WinZipAes;
4
5
use PhpZip\Exception\RuntimeException;
6
use PhpZip\Exception\ZipAuthenticationException;
7
use PhpZip\Model\Extra\Fields\WinZipAesExtraField;
8
use PhpZip\Model\ZipEntry;
9
10
/**
11
 * Decrypt WinZip AES stream.
12
 */
13
class WinZipAesDecryptionStreamFilter extends \php_user_filter
14
{
15
    const FILTER_NAME = 'phpzip.decryption.winzipaes';
16
17
    /** @var string */
18
    private $buffer;
19
20
    /** @var string */
21
    private $authenticationCode;
22
23
    /** @var int */
24
    private $encBlockPosition = 0;
25
26
    /** @var int */
27
    private $encBlockLength = 0;
28
29
    /** @var int */
30
    private $readLength = 0;
31
32
    /** @var ZipEntry */
33
    private $entry;
34
35
    /** @var WinZipAesContext|null */
36
    private $context;
37
38
    /**
39
     * @return bool
40
     */
41
    public static function register()
42
    {
43
        return stream_filter_register(self::FILTER_NAME, __CLASS__);
44
    }
45
46
    /**
47
     * @return bool
48
     *
49
     * @noinspection DuplicatedCode
50
     */
51
    public function onCreate()
52
    {
53
        if (!isset($this->params['entry'])) {
54
            return false;
55
        }
56
57
        if (!($this->params['entry'] instanceof ZipEntry)) {
58
            throw new \RuntimeException('ZipEntry expected');
59
        }
60
        $this->entry = $this->params['entry'];
61
62
        if (
63
            $this->entry->getPassword() === null ||
64
            !$this->entry->isEncrypted() ||
65
            !$this->entry->hasExtraField(WinZipAesExtraField::HEADER_ID)
66
        ) {
67
            return false;
68
        }
69
70
        $this->buffer = '';
71
72
        return true;
73
    }
74
75
    /**
76
     * @param resource $in
77
     * @param resource $out
78
     * @param int      $consumed
79
     * @param bool     $closing
80
     *
81
     * @throws ZipAuthenticationException
82
     *
83
     * @return int
84
     */
85
    public function filter($in, $out, &$consumed, $closing)
86
    {
87
        while ($bucket = stream_bucket_make_writeable($in)) {
88
            $this->buffer .= $bucket->data;
89
            $this->readLength += $bucket->datalen;
90
91
            if ($this->readLength > $this->entry->getCompressedSize()) {
92
                $this->buffer = substr($this->buffer, 0, $this->entry->getCompressedSize() - $this->readLength);
93
            }
94
95
            // read header
96
            if ($this->context === null) {
97
                /**
98
                 * @var WinZipAesExtraField|null $winZipExtra
99
                 */
100
                $winZipExtra = $this->entry->getExtraField(WinZipAesExtraField::HEADER_ID);
101
102
                if ($winZipExtra === null) {
103
                    throw new RuntimeException('$winZipExtra is null');
104
                }
105
                $saltSize = $winZipExtra->getSaltSize();
106
                $headerSize = $saltSize + WinZipAesContext::PASSWORD_VERIFIER_SIZE;
107
108
                if (\strlen($this->buffer) < $headerSize) {
109
                    return \PSFS_FEED_ME;
110
                }
111
112
                $salt = substr($this->buffer, 0, $saltSize);
113
                $passwordVerifier = substr($this->buffer, $saltSize, WinZipAesContext::PASSWORD_VERIFIER_SIZE);
114
                $password = $this->entry->getPassword();
115
116
                if ($password === null) {
117
                    throw new RuntimeException('$password is null');
118
                }
119
                $this->context = new WinZipAesContext($winZipExtra->getEncryptionStrength(), $password, $salt);
120
                unset($password);
121
122
                // Verify password.
123
                if ($passwordVerifier !== $this->context->getPasswordVerifier()) {
124
                    throw new ZipAuthenticationException('Invalid password');
125
                }
126
127
                $this->encBlockPosition = 0;
128
                $this->encBlockLength = $this->entry->getCompressedSize() - $headerSize - WinZipAesContext::FOOTER_SIZE;
129
130
                $this->buffer = substr($this->buffer, $headerSize);
131
            }
132
133
            // encrypt data
134
            $plainText = '';
135
            $offset = 0;
136
            $len = \strlen($this->buffer);
137
            $remaining = $this->encBlockLength - $this->encBlockPosition;
138
139
            if ($remaining >= WinZipAesContext::BLOCK_SIZE && $len < WinZipAesContext::BLOCK_SIZE) {
140
                return \PSFS_FEED_ME;
141
            }
142
            $limit = min($len, $remaining);
143
144
            if ($remaining > $limit && ($limit % WinZipAesContext::BLOCK_SIZE) !== 0) {
145
                $limit -= ($limit % WinZipAesContext::BLOCK_SIZE);
146
            }
147
148
            while ($offset < $limit) {
149
                $this->context->updateIv();
150
                $length = min(WinZipAesContext::BLOCK_SIZE, $limit - $offset);
151
                $data = substr($this->buffer, 0, $length);
152
                $plainText .= $this->context->decryption($data);
153
                $offset += $length;
154
                $this->buffer = substr($this->buffer, $length);
155
            }
156
            $this->encBlockPosition += $offset;
157
158
            if (
159
                $this->encBlockPosition === $this->encBlockLength &&
160
                \strlen($this->buffer) === WinZipAesContext::FOOTER_SIZE
161
            ) {
162
                $this->authenticationCode = $this->buffer;
163
                $this->buffer = '';
164
            }
165
166
            $bucket->data = $plainText;
167
            $consumed += $bucket->datalen;
168
            stream_bucket_append($out, $bucket);
169
        }
170
171
        return \PSFS_PASS_ON;
172
    }
173
174
    /**
175
     * @see http://php.net/manual/en/php-user-filter.onclose.php
176
     *
177
     * @throws ZipAuthenticationException
178
     */
179
    public function onClose()
180
    {
181
        $this->buffer = '';
182
183
        if ($this->context !== null) {
184
            $this->context->checkAuthCode($this->authenticationCode);
185
        }
186
    }
187
}
188