Issues (11)

src/StreamWrapper.php (4 issues)

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;
0 ignored issues
show
The private property $blocks is not used, and could be removed.
Loading history...
63
64
    /**
65
     * Current block
66
     *
67
     * @var int
68
     */
69
    private $block;
0 ignored issues
show
The private property $block is not used, and could be removed.
Loading history...
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
$options & STREAM_USE_PATH !== 0 of type integer is incompatible with the type boolean expected by parameter $use_include_path of fopen(). ( Ignorable by Annotation )

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

138
                $this->encrypted = \fopen($realPath, $mode, /** @scrutinizer ignore-type */ ($options & \STREAM_USE_PATH !== 0), $this->context);
Loading history...
Documentation Bug introduced by
It seems like fopen($realPath, $mode, ... !== 0, $this->context) can also be of type false. However, the property $encrypted 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...
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