PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 261011
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v261011
261011 261001 260927 260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 All 192 releases
s2member / src / includes / classes / utils-defuse.inc.php

utils-defuse.inc.php in s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions 261011, at src/includes/classes/utils-defuse.inc.php

360 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // @codingStandardsIgnoreFile
3 /**
4 * Encryption utilities.
5 *
6 * @since 170418 Defuse.
7 */
8 use Defuse\Crypto\Key;
9 use Defuse\Crypto\Crypto;
10 use c_ws_plugin__s2member_utils_strings as s;
11
12 if (!defined('WPINC')) { // MUST have.
13 exit('Do not access this file directly.');
14 }
15 if (!class_exists('c_ws_plugin__s2member_utils_defuse')) {
16 /**
17 * Encryption utilities.
18 *
19 * @since 170418 Defuse.
20 */
21 class c_ws_plugin__s2member_utils_defuse
22 {
23 /**
24 * Creates a one-way hash for a secret key.
25 *
26 * @since 260809
27 *
28 * @param string $secret_key Secret encryption key.
29 * @return string Secret key hash, else an empty string.
30 */
31 private static function secret_key_hash($secret_key)
32 {
33 $secret_key = (string) $secret_key;
34
35 // Identify a secret key without storing the secret key itself in the new mapping.
36 return isset($secret_key[0]) ? s::hmac_sha256_sign('s2member:defuse:key-mapping', $secret_key) : '';
37 }
38
39 /**
40 * Converts one legacy default Defuse key mapping.
41 *
42 * @since 260809
43 *
44 * @param string $legacy_mapping Legacy `secret-key\nDefuse-key` mapping.
45 * @return array Hashed secret-key-to-Defuse-key mapping, else an empty array.
46 */
47 private static function convert_legacy_defuse_key_mapping($legacy_mapping)
48 {
49 $legacy_mapping = (string) $legacy_mapping;
50 $separator_pos = strrpos($legacy_mapping, "\ndef00000");
51
52 if ($separator_pos === false) {
53 return array();
54 }
55 $secret_key = substr($legacy_mapping, 0, $separator_pos);
56 $defuse_key = substr($legacy_mapping, $separator_pos + 1);
57
58 if (!isset($secret_key[0]) || !isset($defuse_key[0])) {
59 return array();
60 }
61 // Validate the existing Defuse key before preserving its mapping.
62 try {
63 Key::loadFromAsciiSafeString($defuse_key);
64 }
65 // Catch Defuse exceptions and other Throwables on PHP 7+.
66 catch (Throwable $Exception) {
67 return array();
68 }
69 // Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
70 catch (Exception $Exception) {
71 return array();
72 }
73 return array(self::secret_key_hash($secret_key) => $defuse_key);
74 }
75
76 /**
77 * Checks whether a stored Defuse key can be loaded.
78 *
79 * @since 261006
80 *
81 * @param string $defuse_key Stored ASCII-safe Defuse key.
82 * @return bool True for a valid key, else false.
83 */
84 private static function valid_defuse_key($defuse_key)
85 {
86 if (!is_string($defuse_key) || !isset($defuse_key[0])) {
87 return false;
88 }
89 try {
90 Key::loadFromAsciiSafeString($defuse_key);
91 return true;
92 } catch (Throwable $Exception) {
93 return false;
94 } catch (Exception $Exception) {
95 return false;
96 }
97 }
98
99 /**
100 * Migrates legacy Defuse key mappings to hashed mappings.
101 *
102 * Preserves the existing Defuse keys while creating hashed mappings for
103 * the current default key, recovery history, and custom secret keys.
104 * Legacy mappings remain untouched for rollback compatibility.
105 *
106 * @since 260809
107 *
108 * @return void
109 */
110 public static function migrate_legacy_defuse_key_mappings()
111 {
112 $options = array();
113
114 //261006.0424 Fingerprinted associations are authoritative; legacy data fills only missing or invalid keys, including direct upgrades.
115 $legacy_mapping = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key'];
116 $converted_defuse_keys = self::convert_legacy_defuse_key_mapping($legacy_mapping);
117 $defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key'];
118 foreach ($converted_defuse_keys as $_secret_key_hash => $_defuse_key) {
119 if (!isset($defuse_keys[$_secret_key_hash]) || !self::valid_defuse_key($defuse_keys[$_secret_key_hash])) {
120 $defuse_keys[$_secret_key_hash] = $_defuse_key;
121 }
122 }
123 if ($defuse_keys !== $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key']) {
124 $defuse_keys_update = $defuse_keys;
125 array_unshift($defuse_keys_update, 'update-signal');
126 $options['ws_plugin__s2member_secret_key_to_defuse_key'] = $defuse_keys_update;
127 }
128
129 //261006.0424 Keep canonical recovery history ahead of legacy additions; conflicting keys remain available for explicit recovery, not normal lookup.
130 $defuse_key_history = $defuse_keys ? array($defuse_keys) : array();
131 foreach ($GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key_history'] as $_defuse_keys) {
132 if (is_array($_defuse_keys) && count($_defuse_keys) === 1 && !in_array($_defuse_keys, $defuse_key_history, true)) {
133 $defuse_key_history[] = $_defuse_keys;
134 }
135 }
136 if ($converted_defuse_keys && !in_array($converted_defuse_keys, $defuse_key_history, true)) {
137 $defuse_key_history[] = $converted_defuse_keys;
138 }
139 foreach ($GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key_history'] as $_legacy_mapping) {
140 $_defuse_keys = self::convert_legacy_defuse_key_mapping($_legacy_mapping);
141 if ($_defuse_keys && !in_array($_defuse_keys, $defuse_key_history, true)) {
142 $defuse_key_history[] = $_defuse_keys;
143 }
144 }
145 $defuse_key_history = array_slice($defuse_key_history, 0, 10);
146 if ($defuse_key_history !== $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key_history']) {
147 $defuse_key_history_update = $defuse_key_history;
148 array_unshift($defuse_key_history_update, 'update-signal');
149 $options['ws_plugin__s2member_secret_key_to_defuse_key_history'] = $defuse_key_history_update;
150 }
151
152 // Migrate custom mappings without storing custom secret keys in the new mapping.
153 $custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key'];
154 foreach ($GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'] as $_secret_key => $_defuse_key) {
155 $_secret_key = (string) $_secret_key;
156 $_defuse_key = (string) $_defuse_key;
157
158 if (isset($_secret_key[0]) && self::valid_defuse_key($_defuse_key)) {
159 $_secret_key_hash = self::secret_key_hash($_secret_key);
160 if (!isset($custom_defuse_keys[$_secret_key_hash]) || !self::valid_defuse_key($custom_defuse_keys[$_secret_key_hash])) {
161 $custom_defuse_keys[$_secret_key_hash] = $_defuse_key;
162 }
163 }
164 }
165 if ($custom_defuse_keys !== $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key']) {
166 array_unshift($custom_defuse_keys, 'update-signal');
167 $options['ws_plugin__s2member_custom_secret_key_to_defuse_key'] = $custom_defuse_keys;
168 }
169 if ($options) {
170 c_ws_plugin__s2member_menu_pages::update_all_options($options, true, false, false, false, false);
171 }
172 }
173
174 /**
175 * Gets an existing Defuse encryption key.
176 *
177 * @since 260809
178 *
179 * @param string $secret_key Optional custom encryption secret key, or an existing Defuse key.
180 * @return string Defuse encryption key, else an empty string.
181 */
182 public static function get_defuse_key($secret_key = '')
183 {
184 $secret_key = (string) $secret_key;
185
186 if (isset($secret_key[0]) && strpos($secret_key, 'def00000') === 0) {
187 return $secret_key; // Preserve direct Defuse-key input supported by the original accessor.
188 }
189 $has_custom_secret_key = isset($secret_key[0]);
190 $secret_key = c_ws_plugin__s2member_utils_encryption::key($secret_key);
191 $secret_key_hash = self::secret_key_hash($secret_key);
192
193 if ($has_custom_secret_key) {
194 //261006.0424 Prefer valid fingerprinted keys while retaining legacy lookup for installations not yet migrated.
195 $custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key'];
196 if ($secret_key_hash && isset($custom_defuse_keys[$secret_key_hash]) && self::valid_defuse_key($custom_defuse_keys[$secret_key_hash])) {
197 return $custom_defuse_keys[$secret_key_hash];
198 }
199 $legacy_custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'];
200 if ($secret_key && isset($legacy_custom_defuse_keys[$secret_key]) && self::valid_defuse_key($legacy_custom_defuse_keys[$secret_key])) {
201 return $legacy_custom_defuse_keys[$secret_key];
202 }
203 return '';
204 }
205 $defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key'];
206 if ($secret_key_hash && isset($defuse_keys[$secret_key_hash]) && self::valid_defuse_key($defuse_keys[$secret_key_hash])) {
207 return $defuse_keys[$secret_key_hash];
208 }
209 $legacy_mapping = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key'];
210 if ($secret_key && $legacy_mapping && strpos($legacy_mapping, $secret_key."\n") === 0) {
211 $legacy_defuse_key = substr($legacy_mapping, strlen($secret_key) + 1);
212 return self::valid_defuse_key($legacy_defuse_key) ? $legacy_defuse_key : '';
213 }
214 return '';
215 }
216
217 /**
218 * Creates and stores a new Defuse encryption key.
219 *
220 * @since 260809
221 *
222 * @param string $secret_key Optional custom encryption secret key.
223 * @return string New Defuse encryption key.
224 */
225 public static function create_defuse_key($secret_key = '')
226 {
227 $secret_key = (string) $secret_key;
228 $has_custom_secret_key = isset($secret_key[0]);
229 $secret_key = c_ws_plugin__s2member_utils_encryption::key($secret_key);
230 $secret_key_hash = self::secret_key_hash($secret_key);
231 $options = array();
232
233 // Generate a new Defuse encryption key.
234 try {
235 if (!($defuse_key = Key::createNewRandomKey()->saveToAsciiSafeString())) {
236 throw new Exception('Defuse keygen failure.');
237 }
238 }
239 // Catch Defuse exceptions and other Throwables on PHP 7+.
240 catch (Throwable $Exception) {
241 throw new Exception($Exception->getMessage());
242 }
243 // Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
244 catch (Exception $Exception) {
245 throw new Exception($Exception->getMessage());
246 }
247 if ($has_custom_secret_key) {
248 $legacy_custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'];
249 $custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key'];
250
251 // Dual-write legacy and hashed custom mappings for rollback compatibility.
252 $legacy_custom_defuse_keys[$secret_key] = $defuse_key;
253 $custom_defuse_keys[$secret_key_hash] = $defuse_key;
254 array_unshift($legacy_custom_defuse_keys, 'update-signal');
255 array_unshift($custom_defuse_keys, 'update-signal');
256 $options['ws_plugin__s2member_def_custom_combo_encryption_keys'] = $legacy_custom_defuse_keys;
257 $options['ws_plugin__s2member_custom_secret_key_to_defuse_key'] = $custom_defuse_keys;
258 } else {
259 // Dual-write legacy and hashed default mappings for rollback compatibility.
260 $defuse_keys = array($secret_key_hash => $defuse_key);
261 array_unshift($defuse_keys, 'update-signal');
262 $options['ws_plugin__s2member_def_combo_encryption_key'] = $secret_key."\n".$defuse_key;
263 $options['ws_plugin__s2member_secret_key_to_defuse_key'] = $defuse_keys;
264 }
265 c_ws_plugin__s2member_menu_pages::update_all_options($options, true, false, false, false, false);
266
267 return $defuse_key;
268 }
269
270 /**
271 * Gets an existing Defuse key, or creates one when needed.
272 *
273 * @since 260809
274 *
275 * @param string $secret_key Optional custom encryption secret key, or an existing Defuse key.
276 * @return string Defuse encryption key.
277 */
278 public static function get_or_create_defuse_key($secret_key = '')
279 {
280 // Keep lookup read-only; persist a Defuse key only when one must be created.
281 return ($defuse_key = self::get_defuse_key($secret_key)) ? $defuse_key : self::create_defuse_key($secret_key);
282 }
283
284 /**
285 * Gets or creates a Defuse encryption key.
286 *
287 * @since 170418 Defuse.
288 * @since 260809 Retained as a compatibility wrapper around the explicit Defuse key routines.
289 *
290 * @param string $key Optional custom encryption secret key, or an existing Defuse key.
291 * @return string Defuse encryption key.
292 */
293 public static function key($key = '')
294 {
295 //260809 Preserve the original public method while making new internal key handling explicit.
296 return self::get_or_create_defuse_key($key);
297 }
298
299 /**
300 * Defuse encryption.
301 *
302 * @since 170418 Defuse.
303 *
304 * @param string $string String to encrypt.
305 * @param string $key Optional custom encryption secret key, or an existing Defuse key.
306 * @return string Encrypted string w/ a URL-safe base64 wrapper.
307 */
308 public static function encrypt($string, $key = '')
309 {
310 $string = (string) $string;
311
312 if (!isset($string[0])) {
313 return ''; // Not possible.
314 } // Empty string is an empty string.
315
316 try {
317 //260809 Create a Defuse key only when encryption needs one.
318 $defuse_crypto = Key::loadFromAsciiSafeString(self::get_or_create_defuse_key($key));
319 $encrypted = Crypto::encrypt($string, $defuse_crypto, false);
320 return $base64 = s::base64_url_safe_encode($encrypted);
321 } catch (Throwable $Exception) {
322 throw new Exception($Exception->getMessage());
323 }
324 //260809 Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
325 catch (Exception $Exception) {
326 throw new Exception($Exception->getMessage());
327 }
328 }
329
330 /**
331 * Defuse decryption.
332 *
333 * @since 170418 Defuse.
334 *
335 * @param string $base64 String to decrypt.
336 * @param string $key Optional custom encryption secret key, or an existing Defuse key.
337 * @return string Decrypted string.
338 */
339 public static function decrypt($base64, $key = '')
340 {
341 if (!is_string($base64) || !isset($base64[0])) {
342 return ''; // Not possible.
343 } // Fail when not a string or empty.
344
345 try {
346 //260809 Keep decryption read-only by retrieving only an existing Defuse key.
347 $defuse_crypto = Key::loadFromAsciiSafeString(self::get_defuse_key($key));
348 $encrypted = s::base64_url_safe_decode($base64);
349 return $string = Crypto::decrypt($encrypted, $defuse_crypto, false);
350 } catch (Throwable $Exception) {
351 return ''; // Soft failure.
352 }
353 //260809 Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
354 catch (Exception $Exception) {
355 return ''; // Soft failure.
356 }
357 }
358 }
359 }
360