Issues (11)

src/StreamWrapper.php (1 issue)

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
$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...
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