PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / 260917
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions v260917
260917 260913 260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 All 189 releases
← All changes | src/includes/classes/utils-defuse.inc.php +243 -49 260805260917 View file →
@@ -20,71 +20,256 @@
20 20 */
21 21 class c_ws_plugin__s2member_utils_defuse
22 22 {
23 23 /**
24 - * Defuse key.
24 + * Creates a one-way hash for a secret key.
25 25 *
26 - * @since 170418 Defuse.
26 + * @since 260809
27 27 *
28 - * @param string $key A custom key.
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.
29 41 *
30 - * @return string Defuse encryption key.
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.
31 46 */
32 - public static function key($key = '')
47 + private static function convert_legacy_defuse_key_mapping($legacy_mapping)
33 48 {
34 - $key = (string) $key;
49 + $legacy_mapping = (string) $legacy_mapping;
50 + $separator_pos = strrpos($legacy_mapping, "\ndef00000");
35 51
36 - if (isset($key[0])) { // Custom?
37 - if (strpos($key, 'def00000') === 0) {
38 - return $key; // Defuse key.
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 + * Migrates legacy Defuse key mappings to hashed mappings.
78 + *
79 + * Preserves the existing Defuse keys while creating hashed mappings for
80 + * the current default key, recovery history, and custom secret keys.
81 + * Legacy mappings remain untouched for rollback compatibility.
82 + *
83 + * @since 260809
84 + *
85 + * @return void
86 + */
87 + public static function migrate_legacy_defuse_key_mappings()
88 + {
89 + $options = array();
90 +
91 + // Migrate the current default mapping without changing its Defuse key.
92 + $legacy_mapping = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key'];
93 + $converted_defuse_keys = self::convert_legacy_defuse_key_mapping($legacy_mapping);
94 + $defuse_keys = $converted_defuse_keys ? $converted_defuse_keys : $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key'];
95 + if ($converted_defuse_keys && $converted_defuse_keys !== $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key']) {
96 + $defuse_keys_update = $defuse_keys;
97 + array_unshift($defuse_keys_update, 'update-signal');
98 + $options['ws_plugin__s2member_secret_key_to_defuse_key'] = $defuse_keys_update;
99 + }
100 +
101 + // Preserve default mappings as recovery-only history; normal decryption does not search it.
102 + $defuse_key_history = $defuse_keys ? array($defuse_keys) : array();
103 + foreach ($GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key_history'] as $_legacy_mapping) {
104 + $_defuse_keys = self::convert_legacy_defuse_key_mapping($_legacy_mapping);
105 + if ($_defuse_keys && !in_array($_defuse_keys, $defuse_key_history, true)) {
106 + $defuse_key_history[] = $_defuse_keys;
39 107 }
40 - $sec_key = c_ws_plugin__s2member_utils_encryption::key($key);
41 - $def_combo_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'];
108 + }
109 + foreach ($GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key_history'] as $_defuse_keys) {
110 + if (is_array($_defuse_keys) && count($_defuse_keys) === 1 && !in_array($_defuse_keys, $defuse_key_history, true)) {
111 + $defuse_key_history[] = $_defuse_keys;
112 + }
113 + }
114 + $defuse_key_history = array_slice($defuse_key_history, 0, 10);
115 + if ($defuse_key_history !== $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key_history']) {
116 + $defuse_key_history_update = $defuse_key_history;
117 + array_unshift($defuse_key_history_update, 'update-signal');
118 + $options['ws_plugin__s2member_secret_key_to_defuse_key_history'] = $defuse_key_history_update;
119 + }
42 120
43 - if ($sec_key && !empty($def_combo_keys[$sec_key])) {
44 - return $def_combo_keys[$sec_key]; // Use existing key.
121 + // Migrate custom mappings without storing custom secret keys in the new mapping.
122 + $custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key'];
123 + foreach ($GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'] as $_secret_key => $_defuse_key) {
124 + $_secret_key = (string) $_secret_key;
125 + $_defuse_key = (string) $_defuse_key;
126 +
127 + if (isset($_secret_key[0]) && isset($_defuse_key[0])) {
128 + $custom_defuse_keys[self::secret_key_hash($_secret_key)] = $_defuse_key;
45 129 }
46 - try { // Catch Defuse exceptions.
47 - if (!($def_key = Key::createNewRandomKey()->saveToAsciiSafeString())) {
48 - throw new Exception('Defuse keygen failure.');
49 - }
50 - } catch (Throwable $Exception) {
51 - throw new Exception($Exception->getMessage());
52 - }
53 - $def_combo_keys[$sec_key] = $def_key;
54 - array_unshift($def_combo_keys, 'update-signal');
55 - $options['ws_plugin__s2member_def_custom_combo_encryption_keys'] = $def_combo_keys;
130 + }
131 + if ($custom_defuse_keys !== $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key']) {
132 + array_unshift($custom_defuse_keys, 'update-signal');
133 + $options['ws_plugin__s2member_custom_secret_key_to_defuse_key'] = $custom_defuse_keys;
134 + }
135 + if ($options) {
56 136 c_ws_plugin__s2member_menu_pages::update_all_options($options, true, false, false, false, false);
57 - //
58 - } else { // Default behavior is to use the configured key.
59 - $sec_key = c_ws_plugin__s2member_utils_encryption::key();
60 - $def_combo_key = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key'];
137 + }
138 + }
61 139
62 - if ($sec_key && $def_combo_key && strpos($def_combo_key, $sec_key."\n") === 0
63 - && ($def_key = str_replace($sec_key."\n", '', $def_combo_key))) {
64 - return $def_key; // Use existing key.
140 + /**
141 + * Gets an existing Defuse encryption key.
142 + *
143 + * @since 260809
144 + *
145 + * @param string $secret_key Optional custom encryption secret key, or an existing Defuse key.
146 + * @return string Defuse encryption key, else an empty string.
147 + */
148 + public static function get_defuse_key($secret_key = '')
149 + {
150 + $secret_key = (string) $secret_key;
151 +
152 + if (isset($secret_key[0]) && strpos($secret_key, 'def00000') === 0) {
153 + return $secret_key; // Preserve direct Defuse-key input supported by the original accessor.
154 + }
155 + $has_custom_secret_key = isset($secret_key[0]);
156 + $secret_key = c_ws_plugin__s2member_utils_encryption::key($secret_key);
157 + $secret_key_hash = self::secret_key_hash($secret_key);
158 +
159 + if ($has_custom_secret_key) {
160 + // Prefer the legacy custom mapping during the compatibility release.
161 + $legacy_custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'];
162 + if ($secret_key && !empty($legacy_custom_defuse_keys[$secret_key])) {
163 + return $legacy_custom_defuse_keys[$secret_key];
65 164 }
66 - try { // Catch Defuse exceptions.
67 - if (!($def_key = Key::createNewRandomKey()->saveToAsciiSafeString())) {
68 - throw new Exception('Defuse keygen failure.');
69 - }
70 - } catch (Throwable $Exception) {
71 - throw new Exception($Exception->getMessage());
165 + // Fall back to the hashed custom mapping prepared for later releases.
166 + $custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key'];
167 + if ($secret_key_hash && !empty($custom_defuse_keys[$secret_key_hash])) {
168 + return $custom_defuse_keys[$secret_key_hash];
72 169 }
73 - $options['ws_plugin__s2member_def_combo_encryption_key'] = $sec_key."\n".$def_key;
74 - c_ws_plugin__s2member_menu_pages::update_all_options($options, true, false, false, false, false);
170 + return '';
75 171 }
76 - return $def_key;
172 + // Prefer the legacy default mapping during the compatibility release.
173 + $legacy_mapping = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_combo_encryption_key'];
174 + if ($secret_key && $legacy_mapping && strpos($legacy_mapping, $secret_key."\n") === 0) {
175 + return substr($legacy_mapping, strlen($secret_key) + 1);
176 + }
177 + // Fall back to the hashed default mapping prepared for later releases.
178 + $defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['secret_key_to_defuse_key'];
179 +
180 + return ($secret_key_hash && !empty($defuse_keys[$secret_key_hash])) ? $defuse_keys[$secret_key_hash] : '';
77 181 }
78 182
79 183 /**
184 + * Creates and stores a new Defuse encryption key.
185 + *
186 + * @since 260809
187 + *
188 + * @param string $secret_key Optional custom encryption secret key.
189 + * @return string New Defuse encryption key.
190 + */
191 + public static function create_defuse_key($secret_key = '')
192 + {
193 + $secret_key = (string) $secret_key;
194 + $has_custom_secret_key = isset($secret_key[0]);
195 + $secret_key = c_ws_plugin__s2member_utils_encryption::key($secret_key);
196 + $secret_key_hash = self::secret_key_hash($secret_key);
197 + $options = array();
198 +
199 + // Generate a new Defuse encryption key.
200 + try {
201 + if (!($defuse_key = Key::createNewRandomKey()->saveToAsciiSafeString())) {
202 + throw new Exception('Defuse keygen failure.');
203 + }
204 + }
205 + // Catch Defuse exceptions and other Throwables on PHP 7+.
206 + catch (Throwable $Exception) {
207 + throw new Exception($Exception->getMessage());
208 + }
209 + // Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
210 + catch (Exception $Exception) {
211 + throw new Exception($Exception->getMessage());
212 + }
213 + if ($has_custom_secret_key) {
214 + $legacy_custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['def_custom_combo_encryption_keys'];
215 + $custom_defuse_keys = $GLOBALS['WS_PLUGIN__']['s2member']['o']['custom_secret_key_to_defuse_key'];
216 +
217 + // Dual-write legacy and hashed custom mappings for rollback compatibility.
218 + $legacy_custom_defuse_keys[$secret_key] = $defuse_key;
219 + $custom_defuse_keys[$secret_key_hash] = $defuse_key;
220 + array_unshift($legacy_custom_defuse_keys, 'update-signal');
221 + array_unshift($custom_defuse_keys, 'update-signal');
222 + $options['ws_plugin__s2member_def_custom_combo_encryption_keys'] = $legacy_custom_defuse_keys;
223 + $options['ws_plugin__s2member_custom_secret_key_to_defuse_key'] = $custom_defuse_keys;
224 + } else {
225 + // Dual-write legacy and hashed default mappings for rollback compatibility.
226 + $defuse_keys = array($secret_key_hash => $defuse_key);
227 + array_unshift($defuse_keys, 'update-signal');
228 + $options['ws_plugin__s2member_def_combo_encryption_key'] = $secret_key."\n".$defuse_key;
229 + $options['ws_plugin__s2member_secret_key_to_defuse_key'] = $defuse_keys;
230 + }
231 + c_ws_plugin__s2member_menu_pages::update_all_options($options, true, false, false, false, false);
232 +
233 + return $defuse_key;
234 + }
235 +
236 + /**
237 + * Gets an existing Defuse key, or creates one when needed.
238 + *
239 + * @since 260809
240 + *
241 + * @param string $secret_key Optional custom encryption secret key, or an existing Defuse key.
242 + * @return string Defuse encryption key.
243 + */
244 + public static function get_or_create_defuse_key($secret_key = '')
245 + {
246 + // Keep lookup read-only; persist a Defuse key only when one must be created.
247 + return ($defuse_key = self::get_defuse_key($secret_key)) ? $defuse_key : self::create_defuse_key($secret_key);
248 + }
249 +
250 + /**
251 + * Gets or creates a Defuse encryption key.
252 + *
253 + * @since 170418 Defuse.
254 + * @since 260809 Retained as a compatibility wrapper around the explicit Defuse key routines.
255 + *
256 + * @param string $key Optional custom encryption secret key, or an existing Defuse key.
257 + * @return string Defuse encryption key.
258 + */
259 + public static function key($key = '')
260 + {
261 + //260809 Preserve the original public method while making new internal key handling explicit.
262 + return self::get_or_create_defuse_key($key);
263 + }
264 +
265 + /**
80 266 * Defuse encryption.
81 267 *
82 268 * @since 170418 Defuse.
83 269 *
84 270 * @param string $string String to encrypt.
85 - * @param string $key A custom key passed to `::key()`.
86 - *
271 + * @param string $key Optional custom encryption secret key, or an existing Defuse key.
87 272 * @return string Encrypted string w/ a URL-safe base64 wrapper.
88 273 */
89 274 public static function encrypt($string, $key = '')
90 275 {
@@ -93,15 +278,20 @@
93 278 if (!isset($string[0])) {
94 279 return ''; // Not possible.
95 280 } // Empty string is an empty string.
96 281
97 - try { // Catch Defuse exceptions.
98 - $Key = Key::loadFromAsciiSafeString(self::key($key));
99 - $encrypted = Crypto::encrypt($string, $Key, false);
282 + try {
283 + //260809 Create a Defuse key only when encryption needs one.
284 + $defuse_crypto = Key::loadFromAsciiSafeString(self::get_or_create_defuse_key($key));
285 + $encrypted = Crypto::encrypt($string, $defuse_crypto, false);
100 286 return $base64 = s::base64_url_safe_encode($encrypted);
101 287 } catch (Throwable $Exception) {
102 288 throw new Exception($Exception->getMessage());
103 289 }
290 + //260809 Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
291 + catch (Exception $Exception) {
292 + throw new Exception($Exception->getMessage());
293 + }
104 294 }
105 295
106 296 /**
107 297 * Defuse decryption.
@@ -108,10 +298,9 @@
108 298 *
109 299 * @since 170418 Defuse.
110 300 *
111 301 * @param string $base64 String to decrypt.
112 - * @param string $key A custom key passed to `::key()`.
113 - *
302 + * @param string $key Optional custom encryption secret key, or an existing Defuse key.
114 303 * @return string Decrypted string.
115 304 */
116 305 public static function decrypt($base64, $key = '')
117 306 {
@@ -118,13 +307,18 @@
118 307 if (!is_string($base64) || !isset($base64[0])) {
119 308 return ''; // Not possible.
120 309 } // Fail when not a string or empty.
121 310
122 - try { // Catch Defuse exceptions.
123 - $Key = Key::loadFromAsciiSafeString(self::key($key));
311 + try {
312 + //260809 Keep decryption read-only by retrieving only an existing Defuse key.
313 + $defuse_crypto = Key::loadFromAsciiSafeString(self::get_defuse_key($key));
124 314 $encrypted = s::base64_url_safe_decode($base64);
125 - return $string = Crypto::decrypt($encrypted, $Key, false);
315 + return $string = Crypto::decrypt($encrypted, $defuse_crypto, false);
126 316 } catch (Throwable $Exception) {
317 + return ''; // Soft failure.
318 + }
319 + //260809 Catch Defuse exceptions on PHP 5.6, where Throwable is unavailable.
320 + catch (Exception $Exception) {
127 321 return ''; // Soft failure.
128 322 }
129 323 }
130 324 }