Passed
Push — master ( 139e6e...5fc211 )
by Scrutinizer
01:11
created

CypherString   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

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