PluginProbe
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions / trunk
s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions vtrunk
260909 260829 260814 260805 110710 110731 110812 110815 110912 110913 110915 110926 110927 111002 111003 111011 111017 111029 111105 111206 111216 111220 120213 120219 120301 All 187 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 trunk, at src/includes/classes/utils-defuse.inc.php

326 lines 14.9 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 * 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;
107 }
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 }
120
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;
129 }
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) {
136 c_ws_plugin__s2member_menu_pages::update_all_options($options, true, false, false, false, false);
137 }
138 }
139
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];
164 }
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];
169 }
170 return '';
171 }
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] : '';
181 }
182
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 /**
266 * Defuse encryption.
267 *
268 * @since 170418 Defuse.
269 *
270 * @param string $string String to encrypt.
271 * @param string $key Optional custom encryption secret key, or an existing Defuse key.
272 * @return string Encrypted string w/ a URL-safe base64 wrapper.
273 */
274 public static function encrypt($string, $key = '')
275 {
276 $string = (string) $string;
277
278 if (!isset($string[0])) {
279 return ''; // Not possible.
280 } // Empty string is an empty string.
281
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);
286 return $base64 = s::base64_url_safe_encode($encrypted);
287 } catch (Throwable $Exception) {
288 throw new Exception($Exception->getMessage());
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 }
294 }
295
296 /**
297 * Defuse decryption.
298 *
299 * @since 170418 Defuse.
300 *
301 * @param string $base64 String to decrypt.
302 * @param string $key Optional custom encryption secret key, or an existing Defuse key.
303 * @return string Decrypted string.
304 */
305 public static function decrypt($base64, $key = '')
306 {
307 if (!is_string($base64) || !isset($base64[0])) {
308 return ''; // Not possible.
309 } // Fail when not a string or empty.
310
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));
314 $encrypted = s::base64_url_safe_decode($base64);
315 return $string = Crypto::decrypt($encrypted, $defuse_crypto, false);
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) {
321 return ''; // Soft failure.
322 }
323 }
324 }
325 }
326