PluginProbe
CryptX / 4.0.10
CryptX v4.0.10
4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 All 92 releases
cryptx / classes / SecureEncryption.php

SecureEncryption.php in CryptX 4.0.10, at classes/SecureEncryption.php

338 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace CryptX;
4
5 /**
6 * Secure encryption class using modern cryptographic standards
7 * Compatible with JavaScript Web Crypto API
8 */
9 class SecureEncryption
10 {
11 private const CIPHER = 'aes-256-gcm';
12 private const KEY_LENGTH = 32; // 256 bits
13 private const IV_LENGTH = 16; // 128 bits
14 private const SALT_LENGTH = 16; // 128 bits
15 private static int $iterations = 10000; // PBKDF2 iterations
16
17 // Performance optimization: Key cache
18 private static array $keyCache = [];
19 private static int $maxCacheSize = 10; // Limit cache size to prevent memory issues
20
21 // Performance optimization: Pre-check cipher availability
22 private static ?bool $cipherAvailable = null;
23
24 /**
25 * Pre-checks if the required cipher is available
26 *
27 * @return bool
28 */
29 private static function isCipherAvailable(): bool
30 {
31 if (self::$cipherAvailable === null) {
32 self::$cipherAvailable = function_exists('openssl_encrypt') &&
33 in_array(self::CIPHER, openssl_get_cipher_methods());
34 }
35 return self::$cipherAvailable;
36 }
37
38 /**
39 * Derives a key from password using PBKDF2 with caching - compatible with JavaScript
40 *
41 * @param string $password
42 * @param string $salt
43 * @return string
44 * @throws \Exception
45 */
46 private static function deriveKey(string $password, string $salt): string
47 {
48 if (!function_exists('hash_pbkdf2')) {
49 throw new \Exception('PBKDF2 not available');
50 }
51
52 // Performance optimization: Cache derived keys
53 $cacheKey = hash('sha256', $password . $salt);
54
55 if (isset(self::$keyCache[$cacheKey])) {
56 return self::$keyCache[$cacheKey];
57 }
58
59 $derivedKey = hash_pbkdf2('sha256', $password, $salt, self::getIterations(), self::KEY_LENGTH, true);
60
61 // Manage cache size to prevent memory issues
62 if (count(self::$keyCache) >= self::$maxCacheSize) {
63 // Remove oldest entry (FIFO)
64 $oldestKey = array_key_first(self::$keyCache);
65 unset(self::$keyCache[$oldestKey]);
66 }
67
68 self::$keyCache[$cacheKey] = $derivedKey;
69 return $derivedKey;
70 }
71
72 /**
73 * Encrypts plaintext using AES-256-GCM - JavaScript compatible format
74 * Optimized for performance
75 *
76 * @param string $plaintext
77 * @param string $password
78 * @return string Base64 encoded encrypted data
79 * @throws \Exception
80 */
81 public static function encrypt(string $plaintext, string $password): string
82 {
83 // Performance optimization: Pre-check cipher availability
84 if (!self::isCipherAvailable()) {
85 throw new \Exception('OpenSSL extension or AES-256-GCM cipher not available');
86 }
87
88 self::setIterations();
89
90 // Performance optimization: Generate salt and IV in one call
91 $randomBytes = random_bytes(self::SALT_LENGTH + self::IV_LENGTH);
92 $salt = substr($randomBytes, 0, self::SALT_LENGTH);
93 $iv = substr($randomBytes, self::SALT_LENGTH);
94
95 // Derive key from password (now with caching)
96 $key = self::deriveKey($password, $salt);
97
98 // Encrypt data
99 $tag = '';
100 $encrypted = openssl_encrypt(
101 $plaintext,
102 self::CIPHER,
103 $key,
104 OPENSSL_RAW_DATA,
105 $iv,
106 $tag
107 );
108
109 if ($encrypted === false) {
110 throw new \Exception('Encryption failed');
111 }
112
113 // Performance optimization: Use direct concatenation instead of multiple operations
114 return base64_encode($salt . $iv . $encrypted . $tag);
115 }
116
117 /**
118 * Batch encrypt multiple plaintexts with same password for better performance
119 *
120 * @param array $plaintexts Array of strings to encrypt
121 * @param string $password
122 * @return array Array of encrypted strings
123 * @throws \Exception
124 */
125 public static function encryptBatch(array $plaintexts, string $password): array
126 {
127 if (!self::isCipherAvailable()) {
128 throw new \Exception('OpenSSL extension or AES-256-GCM cipher not available');
129 }
130
131 $results = [];
132
133 foreach ($plaintexts as $key => $plaintext) {
134 try {
135 $results[$key] = self::encrypt($plaintext, $password);
136 } catch (\Exception $e) {
137 $results[$key] = false; // Or handle error as needed
138 }
139 }
140
141 return $results;
142 }
143
144 /**
145 * Clears the key cache - useful for memory management
146 *
147 * @return void
148 */
149 public static function clearKeyCache(): void
150 {
151 self::$keyCache = [];
152 }
153
154 /**
155 * Gets current cache statistics
156 *
157 * @return array
158 */
159 public static function getCacheStats(): array
160 {
161 return [
162 'cache_size' => count(self::$keyCache),
163 'max_cache_size' => self::$maxCacheSize,
164 'memory_usage_bytes' => memory_get_usage(),
165 ];
166 }
167
168 /**
169 * Decrypts encrypted data using AES-256-GCM - JavaScript compatible
170 *
171 * @param string $encryptedData Base64 encoded encrypted data
172 * @param string $password
173 * @return string Decrypted plaintext
174 * @throws \Exception
175 */
176 public static function decrypt(string $encryptedData, string $password): string
177 {
178 if (!function_exists('openssl_decrypt')) {
179 throw new \Exception('OpenSSL extension not available');
180 }
181
182 try {
183 $combined = base64_decode($encryptedData, true);
184 if ($combined === false) {
185 throw new \Exception('Invalid base64 encoding');
186 }
187
188 $totalLength = strlen($combined);
189 $expectedMinLength = self::SALT_LENGTH + self::IV_LENGTH + 16; // +16 for tag
190
191 if ($totalLength < $expectedMinLength) {
192 throw new \Exception('Encrypted data too short');
193 }
194
195 // Extract components: salt(16) + iv(16) + encrypted_data + tag(16)
196 $salt = substr($combined, 0, self::SALT_LENGTH);
197 $iv = substr($combined, self::SALT_LENGTH, self::IV_LENGTH);
198 $encryptedDataLength = $totalLength - self::SALT_LENGTH - self::IV_LENGTH - 16;
199 $encrypted = substr($combined, self::SALT_LENGTH + self::IV_LENGTH, $encryptedDataLength);
200 $tag = substr($combined, -16); // Last 16 bytes
201
202 if (strlen($salt) !== self::SALT_LENGTH ||
203 strlen($iv) !== self::IV_LENGTH ||
204 strlen($tag) !== 16) {
205 throw new \Exception('Invalid encrypted data format');
206 }
207
208 // Derive key from password (now with caching)
209 $key = self::deriveKey($password, $salt);
210
211 // Decrypt data
212 $decrypted = openssl_decrypt(
213 $encrypted,
214 self::CIPHER,
215 $key,
216 OPENSSL_RAW_DATA,
217 $iv,
218 $tag
219 );
220
221 if ($decrypted === false) {
222 throw new \Exception('Decryption failed or data corrupted');
223 }
224
225 return $decrypted;
226 } catch (\Throwable $e) {
227 throw new \Exception('Decryption failed: ' . esc_html($e->getMessage()));
228 }
229 }
230
231 /**
232 * Test encryption/decryption with debug output
233 *
234 * @param string $plaintext
235 * @param string $password
236 * @return array Debug information
237 */
238 public static function debugEncryption(string $plaintext, string $password): array
239 {
240 try {
241 $startTime = microtime(true);
242 $encrypted = self::encrypt($plaintext, $password);
243 $encryptTime = microtime(true) - $startTime;
244
245 $startTime = microtime(true);
246 $decrypted = self::decrypt($encrypted, $password);
247 $decryptTime = microtime(true) - $startTime;
248
249 return [
250 'success' => true,
251 'plaintext' => $plaintext,
252 'encrypted' => $encrypted,
253 'decrypted' => $decrypted,
254 'match' => ($plaintext === $decrypted),
255 'encrypted_length' => strlen($encrypted),
256 'binary_length' => strlen(base64_decode($encrypted)),
257 'encrypt_time_ms' => round($encryptTime * 1000, 2),
258 'decrypt_time_ms' => round($decryptTime * 1000, 2),
259 'cache_stats' => self::getCacheStats()
260 ];
261 } catch (\Exception $e) {
262 return [
263 'success' => false,
264 'error' => $e->getMessage(),
265 'plaintext' => $plaintext
266 ];
267 }
268 }
269
270 /**
271 * Validates URL for security
272 *
273 * @param string $url
274 * @return bool
275 */
276 public static function validateUrl(string $url): bool
277 {
278 $allowedProtocols = ['http', 'https', 'mailto'];
279 $maxLength = 2048;
280
281 if (strlen($url) > $maxLength) {
282 return false;
283 }
284
285 $parsedUrl = wp_parse_url($url);
286 if (!$parsedUrl || !isset($parsedUrl['scheme'])) {
287 return false;
288 }
289
290 if (!in_array($parsedUrl['scheme'], $allowedProtocols)) {
291 return false;
292 }
293
294 // Additional validation for mailto URLs
295 if ($parsedUrl['scheme'] === 'mailto') {
296 $email = $parsedUrl['path'] ?? '';
297 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
298 return false;
299 }
300 }
301
302 return true;
303 }
304
305 /**
306 * Get the current PBKDF2 iterations for JavaScript compatibility
307 *
308 * @return int
309 */
310 public static function getIterations(): int
311 {
312 return self::$iterations;
313 }
314
315 private static function setIterations(): void
316 {
317 $config = new Config(get_option('cryptX', []));
318 self::$iterations = $config->get('iterations', self::getIterations());
319
320 }
321
322 /**
323 * Get configuration for JavaScript
324 *
325 * @return array
326 */
327 public static function getJavaScriptConfig(): array
328 {
329 self::setIterations();
330 return [
331 'iterations' => self::getIterations(),
332 'keyLength' => self::KEY_LENGTH,
333 'ivLength' => self::IV_LENGTH,
334 'saltLength' => self::SALT_LENGTH,
335 'cipher' => self::CIPHER
336 ];
337 }
338 }