Passed
Push — master ( c01b91...0e919b )
by Scrutinizer
01:24
created

CypherString::getPassphraseDefault()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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