FileEncrypter   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 21
lcom 1
cbo 1
dl 0
loc 179
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 16 3
A supported() 0 7 4
A encrypt() 0 38 4
B decrypt() 0 42 5
A openDestFile() 0 8 2
A openSourceFile() 0 10 3
1
<?php
2
3
namespace SoareCostin\FileVault;
4
5
use Exception;
6
use Illuminate\Support\Str;
7
use RuntimeException;
8
9
class FileEncrypter
10
{
11
    /**
12
     * Define the number of blocks that should be read from the source file for each chunk.
13
     * We chose 255 because on decryption we want to read chunks of 4kb ((255 + 1)*16).
14
     */
15
    protected const FILE_ENCRYPTION_BLOCKS = 255;
16
17
    /**
18
     * The encryption key.
19
     *
20
     * @var string
21
     */
22
    protected $key;
23
24
    /**
25
     * The algorithm used for encryption.
26
     *
27
     * @var string
28
     */
29
    protected $cipher;
30
31
    /**
32
     * Create a new encrypter instance.
33
     *
34
     * @param  string  $key
35
     * @param  string  $cipher
36
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
37
     *
38
     * @throws \RuntimeException
39
     */
40
    public function __construct($key, $cipher = 'AES-128-CBC')
41
    {
42
        // If the key starts with "base64:", we will need to decode the key before handing
43
        // it off to the encrypter. Keys may be base-64 encoded for presentation and we
44
        // want to make sure to convert them back to the raw bytes before encrypting.
45
        if (Str::startsWith($key, 'base64:')) {
46
            $key = base64_decode(substr($key, 7));
47
        }
48
49
        if (static::supported($key, $cipher)) {
50
            $this->key = $key;
51
            $this->cipher = $cipher;
52
        } else {
53
            throw new RuntimeException('The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths.');
54
        }
55
    }
56
57
    /**
58
     * Determine if the given key and cipher combination is valid.
59
     *
60
     * @param  string  $key
61
     * @param  string  $cipher
62
     * @return bool
63
     */
64
    public static function supported($key, $cipher)
65
    {
66
        $length = mb_strlen($key, '8bit');
67
68
        return ($cipher === 'AES-128-CBC' && $length === 16) ||
69
               ($cipher === 'AES-256-CBC' && $length === 32);
70
    }
71
72
    /**
73
     * Encrypts the source file and saves the result in a new file.
74
     *
75
     * @param string $sourcePath  Path to file that should be encrypted
76
     * @param string $destPath  File name where the encryped file should be written to.
77
     * @return bool
78
     */
79
    public function encrypt($sourcePath, $destPath)
80
    {
81
        $fpOut = $this->openDestFile($destPath);
82
        $fpIn = $this->openSourceFile($sourcePath);
83
84
        // Put the initialzation vector to the beginning of the file
85
        $iv = openssl_random_pseudo_bytes(16);
86
        fwrite($fpOut, $iv);
87
88
        $numberOfChunks = ceil(filesize($sourcePath) / (16 * self::FILE_ENCRYPTION_BLOCKS));
89
90
        $i = 0;
91
        while (! feof($fpIn)) {
92
            $plaintext = fread($fpIn, 16 * self::FILE_ENCRYPTION_BLOCKS);
93
            $ciphertext = openssl_encrypt($plaintext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
94
95
            // Because Amazon S3 will randomly return smaller sized chunks:
96
            // Check if the size read from the stream is different than the requested chunk size
97
            // In this scenario, request the chunk again, unless this is the last chunk
98
            if (strlen($plaintext) !== 16 * self::FILE_ENCRYPTION_BLOCKS
99
                && $i + 1 < $numberOfChunks
100
            ) {
101
                fseek($fpIn, 16 * self::FILE_ENCRYPTION_BLOCKS * $i);
102
                continue;
103
            }
104
105
            // Use the first 16 bytes of the ciphertext as the next initialization vector
106
            $iv = substr($ciphertext, 0, 16);
107
            fwrite($fpOut, $ciphertext);
108
109
            $i++;
110
        }
111
112
        fclose($fpIn);
113
        fclose($fpOut);
114
115
        return true;
116
    }
117
118
    /**
119
     * Decrypts the source file and saves the result in a new file.
120
     *
121
     * @param string $sourcePath   Path to file that should be decrypted
122
     * @param string $destPath  File name where the decryped file should be written to.
123
     * @return bool
124
     */
125
    public function decrypt($sourcePath, $destPath)
126
    {
127
        $fpOut = $this->openDestFile($destPath);
128
        $fpIn = $this->openSourceFile($sourcePath);
129
130
        // Get the initialzation vector from the beginning of the file
131
        $iv = fread($fpIn, 16);
132
133
        $numberOfChunks = ceil((filesize($sourcePath) - 16) / (16 * (self::FILE_ENCRYPTION_BLOCKS + 1)));
134
135
        $i = 0;
136
        while (! feof($fpIn)) {
137
            // We have to read one block more for decrypting than for encrypting because of the initialization vector
138
            $ciphertext = fread($fpIn, 16 * (self::FILE_ENCRYPTION_BLOCKS + 1));
139
            $plaintext = openssl_decrypt($ciphertext, $this->cipher, $this->key, OPENSSL_RAW_DATA, $iv);
140
141
            // Because Amazon S3 will randomly return smaller sized chunks:
142
            // Check if the size read from the stream is different than the requested chunk size
143
            // In this scenario, request the chunk again, unless this is the last chunk
144
            if (strlen($ciphertext) !== 16 * (self::FILE_ENCRYPTION_BLOCKS + 1)
145
                && $i + 1 < $numberOfChunks
146
            ) {
147
                fseek($fpIn, 16 + 16 * (self::FILE_ENCRYPTION_BLOCKS + 1) * $i);
148
                continue;
149
            }
150
151
            if ($plaintext === false) {
152
                throw new Exception('Decryption failed');
153
            }
154
155
            // Get the the first 16 bytes of the ciphertext as the next initialization vector
156
            $iv = substr($ciphertext, 0, 16);
157
            fwrite($fpOut, $plaintext);
158
159
            $i++;
160
        }
161
162
        fclose($fpIn);
163
        fclose($fpOut);
164
165
        return true;
166
    }
167
168
    protected function openDestFile($destPath)
169
    {
170
        if (($fpOut = fopen($destPath, 'w')) === false) {
171
            throw new Exception('Cannot open file for writing');
172
        }
173
174
        return $fpOut;
175
    }
176
177
    protected function openSourceFile($sourcePath)
178
    {
179
        $contextOpts = Str::startsWith($sourcePath, 's3://') ? ['s3' => ['seekable' => true]] : [];
180
181
        if (($fpIn = fopen($sourcePath, 'r', false, stream_context_create($contextOpts))) === false) {
182
            throw new Exception('Cannot open file for reading');
183
        }
184
185
        return $fpIn;
186
    }
187
}
188