|
<?php |
|
/** |
|
* Encryption operations for working with WordPress options to store data |
|
* in the options table. Not all options will be encrypted. You will need |
|
* to wire up selection of options to be protected separately. |
|
* |
|
* @package Fortress |
|
*/ |
|
|
|
namespace DisplaceTech\Fortress\Options; |
|
|
|
/** |
|
* Attempt to get the encryption key for the system. Will return an error |
|
* on any encountered failure. |
|
* |
|
* @return string|\WP_Error |
|
*/ |
|
function getCryptoKey() |
|
{ |
|
if (!defined('FORTRESS_ENCRYPTION_KEY')) { |
|
return new \WP_Error('KEYERROR', 'Invalid site configuration. Missing encryption key.'); |
|
} |
|
|
|
$encoded = FORTRESS_ENCRYPTION_KEY; |
|
$raw = hex2bin($encoded); |
|
|
|
if (strlen($raw) !== SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { |
|
return new \WP_Error('KEYERROR', 'Invalid site configuration. Encryption key is invalid.'); |
|
} |
|
|
|
if ($raw === false) { |
|
return new \WP_Error('KEYERROR', 'Invalid site configuration. Encryption key is invalid.'); |
|
} |
|
|
|
return $raw; |
|
} |
|
|
|
/** |
|
* Decrypt a single specific option based on the installation's encryption key. |
|
* |
|
* @param string $value |
|
* @param string $option |
|
* |
|
* @return mixed |
|
*/ |
|
function decrypt(string $value, string $option) |
|
{ |
|
if (!is_string($value) || substr($value, 0, 8) !== 'fortress') { |
|
return $value; |
|
} |
|
$value = substr($value, 8); |
|
|
|
$key = getCryptoKey(); |
|
if (is_wp_error($key)) return $key; |
|
|
|
$encrypted = hex2bin($value); |
|
$nonce = substr($encrypted, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); |
|
$cipher = substr($encrypted, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); |
|
|
|
$plaintext = sodium_crypto_secretbox_open($cipher, $nonce, $key); |
|
if (false === $plaintext) { |
|
return new \WP_Error('CRYPTOERROR', sprintf('Invalid message authentication tag on %s option.', $option)); |
|
} |
|
|
|
return maybe_unserialize($plaintext); |
|
} |
|
|
|
/** |
|
* Encrypt the value of an option before we update it in the database. |
|
* |
|
* @param mixed $value |
|
* @param mixed $old_value |
|
* @param string $option |
|
* @return string |
|
*/ |
|
function upCrypt($value, $old_value, string $option): string |
|
{ |
|
// Short circuit and return if nothing's changed |
|
if ( $value === $old_value || maybe_serialize( $value ) === maybe_serialize( $old_value ) ) { |
|
return $value; |
|
} |
|
|
|
$key = getCryptoKey(); |
|
if (is_wp_error($key)) return $key; |
|
|
|
try { |
|
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); |
|
} catch (\Exception $e) { |
|
return new \WP_Error('CRYPTOERROR', 'Unable to source enough entropy to create a random nonce.'); |
|
} |
|
|
|
$plaintext = maybe_serialize($value); |
|
|
|
try { |
|
$cipher = sodium_crypto_secretbox($plaintext, $nonce, $key); |
|
} catch (\SodiumException $e) { |
|
return new \WP_Error('CRYPTOERROR', 'Error while encrypting the option payload'); |
|
} |
|
|
|
$encrypted = $nonce . $cipher; |
|
|
|
return 'fortress' . bin2hex($encrypted); |
|
} |
|
|
|
/** |
|
* When we add an option, there are no default WordPress hooks that allow us to encrypt it. |
|
* So we need to immediately update the option to the same value as we are adding by first |
|
* changing it to some other value, then updating it _back_ to the value we actually want to |
|
* store. |
|
* |
|
* @param string $option |
|
* @param $value |
|
*/ |
|
function addCrypt(string $option, $value) |
|
{ |
|
delete_option($option); |
|
update_option($option, $value, false); |
|
} |