PluginProbe
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More / 2.0.2
Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More v2.0.2
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.10 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 All 47 releases
content-control / vendor-prefixed / trustedlogin / client / src / Encryption.php

Encryption.php in Content Control – The Ultimate Content Restriction Plugin! Restrict Content, Create Conditional Blocks & More 2.0.2, at vendor-prefixed/trustedlogin/client/src/Encryption.php

423 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Encryption
4 *
5 * @package ContentControl\Vendor\TrustedLogin\Client
6 *
7 * @copyright 2021 Katz Web Services, Inc.
8 *
9 * @license GPL-2.0-or-later
10 * Modified by code-atlantic on 18-September-2023 using Strauss.
11 * @see https://github.com/BrianHenryIE/strauss
12 */
13 namespace ContentControl\Vendor\TrustedLogin;
14
15 // Exit if accessed directly
16 if ( ! defined('ABSPATH') ) {
17 exit;
18 }
19
20 use \Exception;
21 use \WP_Error;
22 use \Sodium;
23
24 final class Encryption {
25
26 /**
27 * @var Config $config
28 */
29 private $config;
30
31 /**
32 * @var Remote $remote
33 */
34 private $remote;
35
36 /**
37 * @var Logging
38 */
39 private $logging;
40
41 /**
42 * @var string $vendor_public_key_option Where the plugin should store the public key for encrypting data
43 * @since 1.0.0
44 */
45 private $vendor_public_key_option;
46
47 /**
48 * @var string Endpoint path to Vendor public key.
49 */
50 private $vendor_public_key_endpoint = '/trustedlogin/v1/public_key';
51
52 /**
53 * Encryption constructor.
54 *
55 * @param Config $config
56 * @param Remote $remote
57 * @param Logging $logging
58 */
59 public function __construct( Config $config, Remote $remote, Logging $logging ) {
60
61 $this->config = $config;
62 $this->remote = $remote;
63 $this->logging = $logging;
64
65 /**
66 * Filter: Sets the site option name for the Public Key for encryption functions
67 *
68 * @since 1.0.0
69 *
70 * @param string $vendor_public_key_option
71 * @param Config $config
72 */
73 $this->vendor_public_key_option = apply_filters(
74 'trustedlogin/' . $this->config->ns() . '/options/vendor_public_key',
75 'tl_' . $this->config->ns() . '_vendor_public_key',
76 $this->config
77 );
78 }
79
80 /**
81 * Returns true if the site supports encryption using the required Sodium functions.
82 *
83 * These functions are available by extension in PHP 7.0 & 7.1, built-in to PHP 7.2+ and WordPress 5.2+.
84 *
85 * @since 1.4.0
86 *
87 * @return bool True: supports encryption. False: does not support encryption.
88 */
89 static public function meets_requirements() {
90
91 $required_functions = array(
92 'random_bytes',
93 'sodium_hex2bin',
94 'sodium_crypto_box',
95 'sodium_crypto_secretbox',
96 'sodium_crypto_generichash',
97 'sodium_crypto_box_keypair_from_secretkey_and_publickey',
98 );
99
100 foreach ( $required_functions as $function ) {
101 if ( ! function_exists( $function ) ) {
102 return false;
103 }
104 }
105
106 return true;
107 }
108
109 /**
110 * Generates a random hash 64 characters long.
111 *
112 * If random_bytes() and openssl_random_pseudo_bytes() don't exist, returns WP_Error with code generate_hash_failed.
113 *
114 * If random_bytes() does not exist and openssl_random_pseudo_bytes() is unable to return a strong result,
115 * returns a WP_Error with code `openssl_not_strong_crypto`.
116 *
117 * @uses random_bytes
118 * @uses openssl_random_pseudo_bytes Only used if random_bytes() does not exist.
119 *
120 * @param Logging The logging object to use
121 *
122 * @return string|WP_Error 64-character random hash or a WP_Error object explaining what went wrong. See docblock.
123 */
124 static public function get_random_hash( $logging ) {
125
126 $byte_length = 64;
127
128 $hash = false;
129
130 if ( function_exists( 'random_bytes' ) ) {
131 try {
132 $bytes = random_bytes( $byte_length );
133 $hash = bin2hex( $bytes );
134 } catch ( \TypeError $e ) {
135 $logging->log( $e->getMessage(), __METHOD__, 'error' );
136 } catch ( \Error $e ) {
137 $logging->log( $e->getMessage(), __METHOD__, 'error' );
138 } catch ( \Exception $e ) {
139 $logging->log( $e->getMessage(), __METHOD__, 'error' );
140 }
141 } else {
142 $logging->log( 'This site does not have the random_bytes() function.', __METHOD__, 'debug' );
143 }
144
145 if ( $hash ) {
146 return $hash;
147 }
148
149 if ( ! function_exists( 'openssl_random_pseudo_bytes' ) ) {
150 return new \WP_Error( 'generate_hash_failed', 'Could not generate a secure hash with random_bytes or openssl.' );
151 }
152
153 $crypto_strong = false;
154 $hash = openssl_random_pseudo_bytes( $byte_length, $crypto_strong );
155
156 if ( ! $crypto_strong ) {
157 return new \WP_Error( 'openssl_not_strong_crypto', 'Site could not generate a secure hash with OpenSSL.' );
158 }
159
160 return $hash;
161 }
162
163 /**
164 * @param $string
165 *
166 * @return string|WP_Error
167 */
168 static public function hash( $string, $length = 16 ) {
169
170 if ( ! function_exists( 'sodium_crypto_generichash' ) ) {
171 return new \WP_Error( 'sodium_crypto_generichash_not_available', 'sodium_crypto_generichash not available' );
172 }
173
174 try {
175 $hash_bin = sodium_crypto_generichash( $string, '', (int) $length );
176 $hash = sodium_bin2hex( $hash_bin );
177 } catch ( \TypeError $e ) {
178 return new \WP_Error(
179 'encryption_failed_generichash_typeerror',
180 sprintf( 'Error while generating hash: %s (%s)', $e->getMessage(), $e->getCode() )
181 );
182 } catch ( \Error $e ) {
183 return new \WP_Error(
184 'encryption_failed_generichash_error',
185 sprintf( 'Error while generating hash: %s (%s)', $e->getMessage(), $e->getCode() )
186 );
187 } catch ( \SodiumException $e ) {
188 return new \WP_Error(
189 'encryption_failed_generichash_sodium',
190 sprintf( 'Error while generating hash: %s (%s)', $e->getMessage(), $e->getCode() )
191 );
192 } catch ( \Exception $e ) {
193 return new \WP_Error(
194 'encryption_failed_generichash',
195 sprintf( 'Error while generating hash: %s (%s)', $e->getMessage(), $e->getCode() )
196 );
197 }
198
199 return $hash;
200 }
201
202 /**
203 * Fetches the Public Key from local or db
204 *
205 * @since 1.0.0
206 *
207 * @return string|WP_Error If found, it returns the publicKey, if not a WP_Error
208 */
209 public function get_vendor_public_key() {
210
211 // Already stored as transient
212 $public_key = get_site_transient( $this->vendor_public_key_option );
213
214 if ( $public_key ) {
215 // Documented below
216 return apply_filters( 'trustedlogin/' . $this->config->ns() . '/vendor_public_key', $public_key, $this->config );
217 }
218
219 // Fetch a key from Vendor site
220 $remote_key = $this->get_remote_encryption_key();
221
222 if ( is_wp_error( $remote_key ) ) {
223
224 $this->logging->log( sprintf( '(%s) %s', $remote_key->get_error_code(), $remote_key->get_error_message() ), __METHOD__, 'error' );
225
226 return $remote_key;
227 }
228
229 // Attempt to store Vendor public key in the DB for ten minutes (may be overridden by caching plugins)
230 $saved = set_site_transient( $this->vendor_public_key_option, $remote_key, 60 * 10 );
231
232 if ( ! $saved ) {
233 $this->logging->log( 'Public key not saved after being fetched remotely.', __METHOD__, 'warning' );
234 }
235
236 /**
237 * Filter: Override the public key functions.
238 *
239 * @since 1.0.0
240 *
241 * @param string $vendor_public_key
242 * @param Config $config
243 */
244 return apply_filters( 'trustedlogin/' . $this->config->ns() . '/vendor_public_key', $remote_key, $this->config );
245 }
246
247 /**
248 * Returns the URL for the vendor public key endpoint.
249 *
250 * @since 1.5.0
251 *
252 * @return string URL for the vendor public key endpoint, after being filtered.
253 */
254 public function get_remote_encryption_key_url() {
255
256 $vendor_website = $this->config->get_setting( 'vendor/website', '' );
257
258 /**
259 * @see https://docs.trustedlogin.com/Client/hooks#trustedloginnamespacevendorpublic_keywebsite
260 * @since 1.3.2
261 * @param string $public_key_website Root URL of the website from where the vendor's public key is fetched. May be different than the vendor/website configuration setting.
262 */
263 $public_key_website = apply_filters( 'trustedlogin/' . $this->config->ns() . '/vendor/public_key/website', $vendor_website );
264
265 /**
266 * @see https://docs.trustedlogin.com/Client/hooks#trustedloginnamespacevendorpublic_keyendpoint
267 * @param string $key_endpoint Endpoint path on vendor (software vendor's) site.
268 */
269 $key_endpoint = apply_filters( 'trustedlogin/' . $this->config->ns() . '/vendor/public_key/endpoint', $this->vendor_public_key_endpoint );
270
271 $public_key_url = add_query_arg( array( 'rest_route' => $key_endpoint ), trailingslashit( $public_key_website ) );
272
273 return $public_key_url;
274 }
275
276 /**
277 * Fetches the Public Key from the `TrustedLogin-vendor` plugin on support website.
278 *
279 * @since 1.0.0
280 *
281 * @return string|WP_Error If successful, will return the Public Key string. Otherwise WP_Error on failure.
282 */
283 private function get_remote_encryption_key() {
284
285 $headers = array(
286 'Accept' => 'application/json',
287 'Content-Type' => 'application/json',
288 );
289
290 $request_options = array(
291 'method' => 'GET',
292 'timeout' => 45,
293 'httpversion' => '1.1',
294 'headers' => $headers
295 );
296
297 $url = $this->get_remote_encryption_key_url();
298
299 $response = wp_remote_request( $url, $request_options );
300
301 $response_json = $this->remote->handle_response( $response, array( 'publicKey' ) );
302
303 if ( is_wp_error( $response_json ) ) {
304
305 if ( 'not_found' == $response_json->get_error_code() ){
306 return new \WP_Error( 'not_found', __( 'Encryption key could not be fetched, Vendor site returned 404.', 'trustedlogin' ) );
307 }
308
309 return $response_json;
310 }
311
312 return $response_json['publicKey'];
313 }
314
315 /**
316 * Encrypts a string using the Public Key provided by the plugin/theme developers' server.
317 *
318 * @since 1.0.0
319 * @uses \sodium_crypto_box_keypair_from_secretkey_and_publickey() to generate key.
320 * @uses \sodium_crypto_secretbox() to encrypt.
321 *
322 * @param string $data Data to encrypt.
323 * @param string $nonce The nonce generated for this encryption.
324 * @param string $alice_secret_key The key to use when generating the encryption key.
325 *
326 * @return string|WP_Error Encrypted envelope or WP_Error on failure.
327 */
328 public function encrypt( $data, $nonce, $alice_secret_key ) {
329
330 if ( empty( $data ) ) {
331 return new \WP_Error( 'no_data', 'No data provided.' );
332 }
333
334 if ( ! function_exists( 'sodium_crypto_secretbox' ) ) {
335 return new \WP_Error( 'sodium_crypto_secretbox_not_available', 'lib_sodium not available' );
336 }
337
338 $bob_public_key = $this->get_vendor_public_key();
339
340 if ( is_wp_error( $bob_public_key ) ) {
341 return $bob_public_key;
342 }
343
344 try {
345
346 $alice_to_bob_kp = sodium_crypto_box_keypair_from_secretkey_and_publickey( $alice_secret_key, \sodium_hex2bin( $bob_public_key ) );
347 $encrypted = sodium_crypto_box( $data, $nonce, $alice_to_bob_kp );
348
349 } catch ( \SodiumException $e ) {
350 return new \WP_Error(
351 'encryption_failed_cryptobox',
352 sprintf( 'Error while encrypting the envelope: %s (%s)', $e->getMessage(), $e->getCode() )
353 );
354 } catch ( \RangeException $e ) {
355 return new \WP_Error(
356 'encryption_failed_cryptobox_rangeexception',
357 sprintf( 'Error while encrypting the envelope: %s (%s)', $e->getMessage(), $e->getCode() )
358 );
359 } catch ( \TypeError $e ) {
360 return new \WP_Error(
361 'encryption_failed_cryptobox_typeerror',
362 sprintf( 'Error while encrypting the envelope: %s (%s)', $e->getMessage(), $e->getCode() )
363 );
364 }
365
366 return base64_encode( $encrypted );
367 }
368
369 /**
370 * Gets and returns a random nonce.
371 *
372 * @since 1.0.0
373 *
374 * @return string|WP_Error Nonce if created, otherwise WP_Error
375 */
376 public function get_nonce() {
377
378 if ( ! function_exists( 'random_bytes' ) ) {
379 return new \WP_Error( 'missing_function', 'No random_bytes function installed.' );
380 }
381
382 try {
383 $nonce = random_bytes( SODIUM_CRYPTO_SECRETBOX_NONCEBYTES );
384 } catch ( \Exception $e ) {
385 return new \WP_Error( 'encryption_failed_randombytes', sprintf( 'Unable to generate encryption nonce: %s (%s)', $e->getMessage(), $e->getCode() ) );
386 }
387
388 return $nonce;
389 }
390
391 /**
392 * Generate unique Client encryption keys.
393 *
394 * @since 1.0.0
395 *
396 * @uses sodium_crypto_box_keypair()
397 * @uses sodium_crypto_box_publickey()
398 * @uses sodium_crypto_box_secretkey()
399 *
400 * @return object|WP_Error $alice_keys or WP_Error if there's any issues.
401 * $alice_keys = [
402 * 'publicKey' => (string) The public key.
403 * 'privateKey' => (string) The private key.
404 * ]
405 */
406 public function generate_keys() {
407
408 if ( ! function_exists( 'sodium_crypto_box_keypair' ) ) {
409 return new \WP_Error( 'sodium_crypto_secretbox_not_available', 'lib_sodium not available' );
410 }
411
412 // In our build Alice = Client & Bob = Vendor.
413 $aliceKeypair = sodium_crypto_box_keypair();
414
415 $alice_keys = array(
416 'publicKey' => sodium_crypto_box_publickey( $aliceKeypair ),
417 'privateKey' => sodium_crypto_box_secretkey( $aliceKeypair )
418 );
419
420 return (object) $alice_keys;
421 }
422 }
423