Passed
Push — master ( 21e8f6...710a8e )
by Scrutinizer
01:23
created

CypherString   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 123
c 5
b 0
f 0
dl 0
loc 361
rs 9.0399
wmc 42

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getKeyPrivate() 0 3 1
A load() 0 17 3
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 setPassphrase() 0 7 2
A isKeysAvailable() 0 3 1
B decodeJsonData() 0 25 7
A decodeJsonConf() 0 20 3

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