PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / utils / crypto.php

crypto.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/utils/crypto.php

228 lines 6.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Encryption Utility.
4 *
5 * Utility for encrypting & storing sensitive data and retrieving decrypted data.
6 *
7 * @package StoreEngine\Utils
8 *
9 * @since 1.0.0-beta-6
10 * @version 1.0.1
11 */
12
13 namespace StoreEngine\Utils;
14
15 use Exception;
16 use SodiumException;
17 use StoreEngine\Classes\Exceptions\StoreEngineException;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 /**
24 * Class Crypto
25 *
26 * Provides secure encryption and decryption using XChaCha20-Poly1305 AEAD (libsodium).
27 * Uses a 256-bit key, with random nonces per message and base64-encoded output.
28 */
29 class Crypto {
30 /**
31 * Length of nonce for XChaCha20-Poly1305 (24 bytes).
32 */
33 private const NONCE_LEN = SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES;
34
35 /**
36 * Cached binary key to avoid repeated conversion from hex.
37 *
38 * @var string|null
39 */
40 private static ?string $key = null;
41
42 /**
43 * Generates a new 256-bit key for encryption (hex-encoded).
44 *
45 * @return string Hexadecimal-encoded encryption key.
46 * @throws StoreEngineException
47 */
48 public static function generateKey(): string {
49 try {
50 return sodium_bin2hex( sodium_crypto_aead_xchacha20poly1305_ietf_keygen() );
51 } catch ( SodiumException |Exception $e ) {
52 throw new StoreEngineException( esc_html( $e->getMessage() ), 'failed-to-generate-encryption-key' );
53 }
54 }
55
56 /**
57 * Retrieves and caches the encryption key from environment variables.
58 *
59 * @return string Binary-safe encryption key.
60 * @throws StoreEngineException If the key is missing or invalid.
61 */
62 public static function getKey(): string {
63 if ( ! self::$key ) {
64 $hex = null;
65 if ( function_exists( 'getenv' ) && getenv( 'STOREENGINE_ENCRYPTION_KEY' ) ) {
66 try {
67 $hex = sodium_hex2bin( getenv( 'STOREENGINE_ENCRYPTION_KEY' ) );
68 } catch ( Exception $e ) {
69 throw new StoreEngineException(
70 esc_html__( 'Failed to decode encryption key.', 'storeengine' ),
71 'failed-to-decode-encryption-key',
72 [
73 'error' => esc_html( $e->getMessage() ),
74 'via' => 'getenv(STOREENGINE_ENCRYPTION_KEY)',
75 ],
76 0,
77 $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
78 );
79 }
80 } elseif ( Constants::get_constant( 'STOREENGINE_ENCRYPTION_KEY' ) ) {
81 try {
82 $hex = sodium_hex2bin( Constants::get_constant( 'STOREENGINE_ENCRYPTION_KEY' ) );
83 } catch ( Exception $e ) {
84 throw new StoreEngineException(
85 esc_html__( 'Failed to decode encryption key.', 'storeengine' ),
86 'failed-to-decode-encryption-key',
87 [
88 'error' => esc_html( $e->getMessage() ),
89 'via' => 'STOREENGINE_ENCRYPTION_KEY',
90 ],
91 0,
92 $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
93 );
94 }
95 } elseif ( Constants::get_constant( 'SECURE_AUTH_KEY' ) ) {
96 // Fallback to hashed SECURE_AUTH_KEY if no key is defined
97 $hex = hash( 'sha256', Constants::get_constant( 'SECURE_AUTH_KEY' ), true );
98 }
99
100 if ( ! $hex ) {
101 throw new StoreEngineException( esc_html__( 'Encryption key is missing.', 'storeengine' ), 'encryption-key-missing' );
102 }
103
104 self::$key = $hex;
105 }
106
107 return self::$key;
108 }
109
110 /**
111 * Encrypts the given data using XChaCha20-Poly1305.
112 *
113 * @param string|int|float|bool|array|object $data
114 * The data to encrypt. Arrays/objects are JSON-encoded.
115 *
116 * @return string Base64-encoded ciphertext (nonce + encrypted data).
117 * @throws StoreEngineException
118 */
119 public static function encrypt( $data ): string {
120 try {
121 // Serialize arrays/objects
122 $data = maybe_serialize( $data );
123
124 // Generate a unique random nonce per encryption
125 $nonce = random_bytes( self::NONCE_LEN );
126
127 // Perform authenticated encryption (AEAD), without associated data
128 $ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( $data, '', $nonce, self::getKey() );
129
130 // Return base64 encoded (nonce + ciphertext)
131 return base64_encode( $nonce . $ciphertext ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
132 } catch ( Exception $e ) {
133 throw new StoreEngineException(
134 esc_html__( 'Failed to encrypt data.', 'storeengine' ),
135 'failed-to-encrypt-data',
136 [ 'error' => esc_html( $e->getMessage() ) ],
137 0,
138 $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
139 );
140 }
141 }
142
143 /**
144 * Decrypts data encrypted with encrypt().
145 *
146 * @param ?string $data Base64-encoded string (nonce + ciphertext).
147 *
148 * @return string|array|false The decrypted value, auto-decoded from JSON if applicable. False on failure.
149 * @throws StoreEngineException
150 */
151 public static function decrypt( ?string $data ) {
152 try {
153 if ( ! $data ) {
154 return false;
155 }
156
157 // Decode base64 input
158 $decoded = base64_decode( $data, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
159
160 if ( false === $decoded ) {
161 return $data; // Maybe not encrypted.
162 }
163
164 if ( strlen( $decoded ) <= self::NONCE_LEN ) {
165 return false; // Maybe not encrypted data.
166 }
167
168 // Extract nonce and ciphertext
169 $nonce = substr( $decoded, 0, self::NONCE_LEN );
170 $ciphertext = substr( $decoded, self::NONCE_LEN );
171
172 // Decrypt the ciphertext, without associated data.
173 $plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $ciphertext, '', $nonce, self::getKey() );
174
175 return maybe_unserialize( $plaintext );
176 } catch ( Exception $e ) {
177 throw new StoreEngineException(
178 esc_html__( 'Failed to decrypt data.', 'storeengine' ),
179 'failed-to-decrypt-data',
180 [ 'error' => esc_html( $e->getMessage() ) ],
181 0,
182 $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
183 );
184 }
185 }
186
187 /**
188 * Retrieves the value of a transient.
189 *
190 * If the transient does not exist, does not have a value, or has expired,
191 * then the return value will be false.
192 *
193 * @param string $transient Transient name. Expected to not be SQL-escaped.
194 *
195 * @return array|false|string Value of transient.
196 * @throws StoreEngineException
197 */
198 public static function get_transient( string $transient ) {
199 $value = get_transient( $transient );
200 if ( false === $value ) {
201 return false;
202 }
203
204 return self::decrypt( $value );
205 }
206
207 /**
208 * Sets/updates the value of a transient.
209 *
210 * You do not need to serialize values. If the value needs to be serialized,
211 * then it will be serialized before it is set.
212 *
213 * @param string $transient Transient name. Expected to not be SQL-escaped.
214 * Must be 172 characters or fewer in length.
215 * @param mixed $value Transient value. Must be serializable if non-scalar.
216 * Expected to not be SQL-escaped.
217 * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration).
218 *
219 * @return bool True if the value was set, false otherwise.
220 * @throws StoreEngineException
221 */
222 public static function set_transient( string $transient, $value, int $expiration = 0 ): bool {
223 return set_transient( $transient, self::encrypt( $value ), $expiration );
224 }
225 }
226
227 // End of file crypto.php
228