Passed
Push — master ( 2941b9...8367c7 )
by Scrutinizer
01:23
created

CypherString   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 127
c 5
b 0
f 0
dl 0
loc 372
rs 8.96
wmc 43

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getKeyPrivate() 0 3 1
A load() 0 17 3
B decodeJsonData() 0 29 7
A getPassphrase() 0 3 1
A getConfigSSL() 0 3 1
B init() 0 41 6
A getPathFileConfig() 0 3 1
A getKeyPublic() 0 3 1
A decrypt() 0 31 4
A setFlagAsKeysAvailable() 0 3 1
A encrypt() 0 32 5
A __construct() 0 15 3
A save() 0 15 2
A decodeJsonConf() 0 24 4
A setPassphrase() 0 7 2
A isKeysAvailable() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like CypherString often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CypherString, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This class creates a RSA key pair, encrypts and decrypts a string.
5
 *
6
 * Usage:
7
 *    $cypher   = new KEINOS\lib\CypherString('/path/to/key/pair.json');
8
 *    $data_enc = $cypher->encrypt('Sample data');
9
 *    $data_dec = $cypher->decrypt($data_enc);
10
 * Note:
11
 *    - Be aware that this script should be compatible with PHP 7.1 or higher.
12
 *    - The encrypted data is not compatible with other languages or
13
 *      decrypt-able with "openssl" command. The encrypted data should be
14
 *      decrypted with the same script and conf file.
15
 *
16
 * Current quality before push
17
 *   - code_quality 9.69
18
 *   - coveralls 85.22
19
 */
20
21
declare(strict_types=1);
22
23
namespace KEINOS\lib;
24
25
final class CypherString
26
{
27
    private const AS_ASSOCIATIVE_ARRAY = true;
28
29
    /** @var array<string,mixed> $config_ssl */
30
    private $config_ssl;
31
    /** @var string $key_private */
32
    private $key_private;
33
    /** @var string $key_public */
34
    private $key_public;
35
    /** @var string $passphrase */
36
    private $passphrase;
37
    /** @var string $path_file_config */
38
    private $path_file_config;
39
    /** @var bool $flag_keys_available */
40
    private $flag_keys_available = false;
41
42
    /**
43
     * Instantiate an object from the provided JSON file.
44
     *
45
     * @param  string $path_file_config  File path of the key pair and configuration.
46
     *                                   If the file doesn't exist it will generate
47
     *                                   a new one. The file should be in JSON format.
48
     * @param  string $passphrase        The pass phrase to use the key pair. (Optional)
49
     *                                   If the pass phrase is empty then it will
50
     *                                   generate from the hash value of this script.
51
     * @return void
52
     */
53
    public function __construct(string $path_file_config, string $passphrase = '')
54
    {
55
        $this->path_file_config = $path_file_config;
56
57
        if (! file_exists($path_file_config)) {
58
            // Creates a key pair and saves to the provided file path
59
            $this->init($passphrase);
60
        } else {
61
            // Loads the existing key pair
62
            $this->load($path_file_config);
63
            $this->setFlagAsKeysAvailable(true);
64
        }
65
66
        if (! file_exists($path_file_config)) {
67
            throw new \Exception('Failed to save conf file at: ' . $path_file_config);
68
        }
69
    }
70
71
    /**
72
     * Decodes JSON string from the configuration file to an array.
73
     *
74
     * @param  string $conf_json    JSON string from conf file.
75
     * @return array<string,mixed>
76
     * @throws \Exception           On any error occurred while decoding or missing requirements.
77
     */
78
    private function decodeJsonConf(string $conf_json): array
79
    {
80
        $data = json_decode($conf_json, self::AS_ASSOCIATIVE_ARRAY);
81
82
        if (empty($data)) {
83
            throw new \Exception('Failed to decode JSON. Conf data is empty.');
84
        }
85
86
        // Check if $data contains must keys required
87
        $keys_required = [
88
            'key_private',
89
            'key_public',
90
            'passphrase',
91
            'config_ssl',
92
        ];
93
        if (array_diff_key(array_flip($keys_required), $data)) {
94
            throw new \Exception('Missing information in JSON config file.');
95
        }
96
97
        if (! is_array($data['config_ssl'])) {
98
            throw new \Exception('Data of "config_ssl" should be an array.');
99
        }
100
101
        return $data;
102
    }
103
104
    /**
105
     * Decodes JSON string from the "encrypt()" method to an array.
106
     *
107
     * @param  string $data_json
108
     * @return array<string>
109
     * @throws \Exception
110
     *     On any error occurred while decoding or missing requirements.
111
     */
112
    private function decodeJsonData(string $data_json): array
113
    {
114
        $data = json_decode($data_json, self::AS_ASSOCIATIVE_ARRAY);
115
116
        if (empty($data)) {
117
            throw new \Exception('Empty data given. Conf file should not be empty.');
118
        }
119
120
        if (! is_array($data)) {
121
            throw new \Exception('Type error. Conf file should be compatible to array.');
122
        }
123
124
        if (! isset($data['data_encrypted'])) {
125
            throw new \Exception('"data_encrypted" key missing. The JSON string does not contain the encoded data.');
126
        }
127
128
        if (! isset($data['data_sealed'])) {
129
            throw new \Exception('"data_sealed" key missing. The JSON string does not contain the sealed data.');
130
        }
131
132
        // Decode base64 encoded basic requirements
133
        $data['data_encrypted'] = base64_decode($data['data_encrypted']);
134
        $data['data_sealed']    = base64_decode($data['data_sealed']);
135
136
        // Sets optional requirements
137
        $data['key_private_pem'] = isset($data['key_private_pem']) ? $data['key_private_pem'] : $this->getKeyPrivate();
138
        $data['passphrase']      = isset($data['passphrase'])      ? $data['passphrase']      : $this->getPassphrase();
139
140
        return $data;
141
    }
142
143
    /**
144
     * @param  string $data_json  JSON data from encrypt() method with additional data.
145
     * @return string
146
     * @throws \Exception         On any error occurred while decryption.
147
     */
148
    public function decrypt(string $data_json): string
149
    {
150
        if ($this->isKeysAvailable() === false) {
151
            throw new \Exception('No SSL configuration is loaded. Use init() method to initiate configuration.');
152
        }
153
154
        $data = $this->decodeJsonData($data_json);
155
156
        // Get resource id of the key
157
        $key_private = $data['key_private_pem'];
158
        $passphrase  = $data['passphrase'];
159
        $id_resource = openssl_pkey_get_private($key_private, $passphrase);
160
        if ($id_resource === false) {
161
            $msg  = PHP_EOL . 'Data:' . PHP_EOL . print_r($data, true) . PHP_EOL;
162
            throw new \Exception('Failed to decrypt data. Could NOT get resource ID of private key.' . $msg);
163
        }
164
165
        // Requirements to decrypt
166
        $data_sealed    = $data['data_sealed'];
167
        $data_decrypted = '';
168
        $data_encrypted = $data['data_encrypted'];
169
170
        // Decrypt data
171
        $result = openssl_open($data_sealed, $data_decrypted, $data_encrypted, $id_resource);
172
        if ($result === false) {
173
            $msg = 'Failed to decrypt data. Could NOT open the data with keys provided.' . PHP_EOL
174
                 . 'Keys provided:' . PHP_EOL . print_r($data, true);
175
            throw new \Exception($msg);
176
        }
177
178
        return $data_decrypted;
179
    }
180
181
    /**
182
     * @param  string $string      Data should be Base64 encoded.
183
     * @param  string $key_public  Public key in PEM to encrypt. (Optional)
184
     * @return string              JSON object string of the encrypted data and it's envelope key.
185
     * @throws \Exception          On any error occurred while encryption.
186
     */
187
    public function encrypt(string $string, string $key_public = ''): string
188
    {
189
        $key_public = empty($key_public) ? $this->getKeyPublic() : $key_public;
190
        $list_key_envelope = [];
191
        $data_sealed = '';
192
193
        // Encrypt/seal data
194
        $result = openssl_seal($string, $data_sealed, $list_key_envelope, [$key_public]);
195
        if ($result === false) {
196
            throw new \Exception('Failed to encrypt data.');
197
        }
198
199
        // Get envelope key
200
        if (! isset($list_key_envelope[0])) {
201
            throw new \Exception('Bad envelope keys returned.');
202
        }
203
204
        // Create a data pair of sealed and encrypted data to return
205
        $data = [
206
            'data_encrypted' => base64_encode($list_key_envelope[0]),
207
            'data_sealed'    => base64_encode($data_sealed),
208
        ];
209
210
        // Encode data to return as JSON string
211
        $result = json_encode($data, JSON_PRETTY_PRINT);
212
213
        if ($result === false) {
214
            $msg = print_r($data, true) . PHP_EOL . PHP_EOL;
215
            throw new \Exception('Failed to encode as JSON. Data to encode: ' . $msg);
216
        }
217
218
        return $result;
219
    }
220
221
    /**
222
     * @return array<string,mixed>
223
     */
224
    public function getConfigSSL(): array
225
    {
226
        return $this->config_ssl;
227
    }
228
229
    /**
230
     * Gets PEM format private key.
231
     *
232
     * @return string
233
     */
234
    public function getKeyPrivate(): string
235
    {
236
        return $this->key_private;
237
    }
238
239
    /**
240
     * Gets PEM format public key.
241
     *
242
     * @return string
243
     */
244
    public function getKeyPublic(): string
245
    {
246
        return $this->key_public;
247
    }
248
249
    /**
250
     * @return string
251
     */
252
    public function getPassphrase(): string
253
    {
254
        return $this->passphrase;
255
    }
256
257
    /**
258
     * @return string
259
     */
260
    public function getPathFileConfig(): string
261
    {
262
        return $this->path_file_config;
263
    }
264
265
    /**
266
     * Creates a brand-new SSL (SHA-512, 4096 bit RSA) key pair.
267
     *
268
     * @param  string $passphrase (Optional)
269
     * @return void
270
     * @throws \Exception         On any error occurred while creating the keys.
271
     */
272
    private function init(string $passphrase = ''): void
273
    {
274
        // Set/generate passphrase (Fix Bug #73833 on PHP 7.1.23)
275
        $this->setPassphrase($passphrase);
276
277
        // Configuration/settings of the key pair
278
        $this->config_ssl = [
279
            "digest_alg"       => "sha512",
280
            "private_key_bits" => 4096,
281
            "private_key_type" => OPENSSL_KEYTYPE_RSA,
282
        ];
283
284
        // Create the key pair
285
        $resource = openssl_pkey_new($this->config_ssl);
286
        if ($resource === false) {
287
            throw new \Exception('Failed to create key pair. Probably old OpenSSL module used.');
288
        }
289
290
        // Get and set private key
291
        $result = openssl_pkey_export($resource, $key_private, $this->getPassphrase());
292
        if ($result === false) {
293
            throw new \Exception('Failed to create key pair.');
294
        }
295
        if (empty($key_private)) {
296
            throw new \Exception('Failed to create key pair.');
297
        }
298
        $this->key_private = $key_private;
299
300
        // Get and set public key
301
        $array = openssl_pkey_get_details($resource);
302
        if ($array === false) {
303
            throw new \Exception('Failed to get public key.');
304
        }
305
        if (! isset($array['key'])) {
306
            throw new \Exception('Failed to get public key.');
307
        }
308
        $this->key_public = $array['key'];
309
310
        // Save the above data and flag up ready to use
311
        $this->save();
312
        $this->setFlagAsKeysAvailable(true);
313
    }
314
315
    /**
316
     * @return bool
317
     */
318
    private function isKeysAvailable(): bool
319
    {
320
        return $this->flag_keys_available;
321
    }
322
323
    /**
324
     * Loads the JSON file of SSL data such as key pair and the passphrase.
325
     *
326
     * @param  string $path_file_config
327
     * @return void
328
     * @throws \Exception
329
     *     On any error occurred while creating the keys.
330
     */
331
    public function load(string $path_file_config): void
332
    {
333
        if (! file_exists($path_file_config)) {
334
            throw new \Exception('File not found at: ' . $path_file_config);
335
        }
336
337
        $json = file_get_contents($path_file_config);
338
        if ($json === false) {
339
            throw new \Exception('Failed to read file from: ' . $path_file_config);
340
        }
341
        $data = $this->decodeJsonConf($json);
342
343
        // Re-dump properties.
344
        $this->key_private = $data['key_private'];
345
        $this->key_public  = $data['key_public'];
346
        $this->passphrase  = $data['passphrase'];
347
        $this->config_ssl  = $data['config_ssl'];
348
    }
349
350
    /**
351
     * Saves/overwrites the SSL data such as key pair and passphrase as JSON file.
352
     *
353
     * - NOTE:
354
     *   BE CAREFUL where to save the data! If the saved file gets public,
355
     *   then there's no meaning to encrypt the data.
356
     *
357
     * @param  string $path_file_config
358
     * @return bool
359
     */
360
    public function save(string $path_file_config = ''): bool
361
    {
362
        $path_file_config = (empty($path_file_config)) ? $this->getPathFileConfig() : $path_file_config;
363
364
        $data = [
365
            'key_private' => $this->getKeyPrivate(),
366
            'key_public'  => $this->getKeyPublic(),
367
            'passphrase'  => $this->getPassphrase(),
368
            'config_ssl'  => $this->getConfigSSL(),
369
        ];
370
371
        $data   = json_encode($data, JSON_PRETTY_PRINT);
372
        $result = file_put_contents($path_file_config, $data, LOCK_EX);
373
374
        return ($result !== false);
375
    }
376
377
    /**
378
     * @param  bool $flag
379
     * @return void
380
     */
381
    private function setFlagAsKeysAvailable(bool $flag): void
382
    {
383
        $this->flag_keys_available = $flag;
384
    }
385
386
    /**
387
     * @param  string $passphrase
388
     * @return void
389
     */
390
    public function setPassphrase(string $passphrase): void
391
    {
392
        // In PHP 7.1.23 there's a bug that with an empty pass phrase "openssl_pkey_get_private()"
393
        // fails to create the resource ID. So set a default pass phrase if empty.
394
        //   - Ref: Bug #73833 https://bugs.php.net/bug.php?id=73833
395
        $passphrase = (empty(trim($passphrase))) ? strval(hash_file('md5', __FILE__)) : trim($passphrase);
396
        $this->passphrase = $passphrase;
397
    }
398
}
399