1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the PHP EcryptFS library. |
||
5 | * (c) 2017 by Dennis Birkholz |
||
6 | * All rights reserved. |
||
7 | * For the license to use this library, see the provided LICENSE file. |
||
8 | */ |
||
9 | |||
10 | namespace Iqb\Ecryptfs; |
||
11 | |||
12 | class StreamWrapper |
||
13 | { |
||
14 | /** |
||
15 | * Name of the registered stream and name of the array key in the context options |
||
16 | */ |
||
17 | const STREAM_NAME = 'ecryptfs'; |
||
18 | |||
19 | /** |
||
20 | * Name of the passphrase context option |
||
21 | */ |
||
22 | const CONTEXT_PASSPHRASE = 'passphrase'; |
||
23 | |||
24 | /** |
||
25 | * Name of the engine context option |
||
26 | */ |
||
27 | const CONTEXT_ENGINE = 'engine'; |
||
28 | |||
29 | /** |
||
30 | * Name of the stream context option |
||
31 | */ |
||
32 | const CONTEXT_STREAM = 'stream'; |
||
33 | |||
34 | |||
35 | /** |
||
36 | * @var resource |
||
37 | */ |
||
38 | public $context; |
||
39 | |||
40 | /** |
||
41 | * The stream to the encrypted data |
||
42 | * |
||
43 | * @var resource |
||
44 | */ |
||
45 | private $encrypted; |
||
46 | |||
47 | /** |
||
48 | * @var CryptoEngineInterface |
||
49 | */ |
||
50 | private $cryptoEngine; |
||
51 | |||
52 | /** |
||
53 | * @var FileHeader |
||
54 | */ |
||
55 | private $header; |
||
56 | |||
57 | /** |
||
58 | * Total number of blocks according to header |
||
59 | * |
||
60 | * @var int |
||
61 | */ |
||
62 | private $blocks; |
||
63 | |||
64 | /** |
||
65 | * Current block |
||
66 | * |
||
67 | * @var int |
||
68 | */ |
||
69 | private $block; |
||
70 | |||
71 | /** |
||
72 | * Maximum stream position reachable |
||
73 | * |
||
74 | * @var int |
||
75 | */ |
||
76 | private $maxPosition; |
||
77 | |||
78 | /** |
||
79 | * Absolute position in the stream |
||
80 | * |
||
81 | * @var int |
||
82 | */ |
||
83 | private $position; |
||
84 | |||
85 | /** |
||
86 | * File encryption key encryption key (FEKEK) as binary string |
||
87 | * |
||
88 | * @var string |
||
89 | */ |
||
90 | private $fekek; |
||
91 | |||
92 | |||
93 | 36 | public function stream_open(string $path, string $mode, int $options) : bool |
|
94 | { |
||
95 | 36 | $context = \stream_context_get_options($this->context); |
|
96 | 36 | $myContext = (isset($context[self::STREAM_NAME]) && \is_array($context[self::STREAM_NAME]) ? $context[self::STREAM_NAME] : []); |
|
97 | |||
98 | // Read passphrase from context and derive file encryption key encryption key (FEKEK) |
||
99 | 36 | if (\array_key_exists(self::CONTEXT_PASSPHRASE, $myContext)) { |
|
100 | 36 | $this->fekek = Util::deriveFEKEK($myContext[self::CONTEXT_PASSPHRASE]); |
|
101 | } else { |
||
102 | if ($options & \STREAM_REPORT_ERRORS) { |
||
103 | throw new \InvalidArgumentException("Passphrase required!"); |
||
104 | } |
||
105 | return false; |
||
106 | } |
||
107 | |||
108 | // Get crypto engine from context or use OpenSSL by default |
||
109 | 36 | if (\array_key_exists(self::CONTEXT_ENGINE, $myContext)) { |
|
110 | $this->cryptoEngine = $myContext[self::CONTEXT_ENGINE]; |
||
111 | if (!$this->cryptoEngine instanceof CryptoEngineInterface) { |
||
112 | if ($options & \STREAM_REPORT_ERRORS) { |
||
113 | new \InvalidArgumentException("Supplied crypto engine must implement " . CryptoEngineInterface::class); |
||
114 | } |
||
115 | return false; |
||
116 | } |
||
117 | } else { |
||
118 | 36 | $this->cryptoEngine = new OpenSslCryptoEngine(); |
|
119 | } |
||
120 | |||
121 | // Use stream from context or open file |
||
122 | 36 | if (\array_key_exists(self::CONTEXT_STREAM, $myContext)) { |
|
123 | $this->encrypted = $myContext[self::CONTEXT_STREAM]; |
||
124 | } |
||
125 | |||
126 | else { |
||
127 | 36 | $prefix = self::STREAM_NAME . '://'; |
|
128 | |||
129 | 36 | if (\substr($path, 0, \strlen($prefix)) !== $prefix) { |
|
130 | if ($options & \STREAM_REPORT_ERRORS) { |
||
131 | \trigger_error("Invalid path!", \E_USER_WARNING); |
||
132 | } |
||
133 | return false; |
||
134 | } |
||
135 | |||
136 | 36 | $realPath = \substr($path, \strlen($prefix)); |
|
137 | 36 | if ($options & \STREAM_REPORT_ERRORS) { |
|
138 | $this->encrypted = \fopen($realPath, $mode, ($options & \STREAM_USE_PATH !== 0), $this->context); |
||
0 ignored issues
–
show
|
|||
139 | } else { |
||
140 | 36 | $this->encrypted = @\fopen($realPath, $mode, ($options & \STREAM_USE_PATH !== 0), $this->context); |
|
141 | } |
||
142 | } |
||
143 | |||
144 | 36 | if (!\is_resource($this->encrypted)) { |
|
145 | if ($options & \STREAM_REPORT_ERRORS) { |
||
146 | \trigger_error("Failed to open encrypted file!", \E_USER_WARNING); |
||
147 | } |
||
148 | return false; |
||
149 | } |
||
150 | |||
151 | 36 | $this->header = FileHeader::parse($this->encrypted); |
|
152 | 36 | $this->header->decryptFileKey($this->cryptoEngine, $this->fekek); |
|
153 | 36 | $this->position = $this->header->metadataSize; |
|
154 | 36 | $this->maxPosition = $this->header->metadataSize + $this->header->size; |
|
155 | |||
156 | 36 | return true; |
|
157 | } |
||
158 | |||
159 | |||
160 | /** |
||
161 | * @param int $length |
||
162 | * @return string |
||
163 | * @link http://php.net/manual/en/streamwrapper.stream-read.php |
||
164 | */ |
||
165 | 36 | public function stream_read(int $length) : string |
|
166 | { |
||
167 | 36 | if (($length % $this->header->extentSize) !== 0) { |
|
168 | throw new \InvalidArgumentException("Can only read multiples of " . $this->header->extentSize . " blocks"); |
||
169 | } |
||
170 | |||
171 | 36 | $readBlocks = $length / $this->header->extentSize; |
|
172 | 36 | $startBlock = \floor(($this->position - $this->header->metadataSize) / $this->header->extentSize); |
|
173 | |||
174 | 36 | $return = ''; |
|
175 | 36 | for ($i=0; $i<$readBlocks && !$this->stream_eof(); $i++) { |
|
176 | 36 | $block = $startBlock + $i; |
|
177 | 36 | $iv = \hash("md5", $this->header->rootIv . \str_pad("$block", 16, "\0", \STR_PAD_RIGHT), true); |
|
178 | |||
179 | 36 | $encrypted = \stream_get_contents($this->encrypted, $this->header->extentSize); |
|
180 | 36 | if (\strlen($encrypted) !== $this->header->extentSize) { |
|
181 | throw new \RuntimeException("Could not read enough data from stream, got only " . \strlen($encrypted) . " bytes instead of " . $this->header->extentSize); |
||
182 | } |
||
183 | 36 | $this->position = \ftell($this->encrypted); |
|
184 | 36 | $decrypted = $this->cryptoEngine->decrypt($encrypted, $this->header->cipherCode, $this->header->fileKey, $iv); |
|
185 | |||
186 | // Remove garbage from end |
||
187 | 36 | if ($this->position > $this->maxPosition) { |
|
188 | 36 | $return .= \substr($decrypted, 0, $this->header->size % $this->header->extentSize); |
|
189 | } else { |
||
190 | 36 | $return .= $decrypted; |
|
191 | } |
||
192 | } |
||
193 | |||
194 | 36 | return $return; |
|
195 | } |
||
196 | |||
197 | |||
198 | 36 | public function stream_eof() : bool |
|
199 | { |
||
200 | 36 | return ($this->position >= $this->maxPosition); |
|
201 | } |
||
202 | |||
203 | |||
204 | 36 | final public function stream_stat() : array |
|
205 | { |
||
206 | return [ |
||
207 | 36 | 'size' => $this->header->size, |
|
208 | 36 | 'blksize' => $this->header->extentSize, |
|
209 | 36 | 'blocks' => \ceil($this->header->size / $this->header->extentSize), |
|
210 | ]; |
||
211 | } |
||
212 | } |
||
213 |
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 theid
property of an instance of theAccount
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.