getMessage() ), 'failed-to-generate-encryption-key' ); } } /** * Retrieves and caches the encryption key from environment variables. * * @return string Binary-safe encryption key. * @throws StoreEngineException If the key is missing or invalid. */ public static function getKey(): string { if ( ! self::$key ) { $hex = null; if ( function_exists( 'getenv' ) && getenv( 'STOREENGINE_ENCRYPTION_KEY' ) ) { try { $hex = sodium_hex2bin( getenv( 'STOREENGINE_ENCRYPTION_KEY' ) ); } catch ( Exception $e ) { throw new StoreEngineException( esc_html__( 'Failed to decode encryption key.', 'storeengine' ), 'failed-to-decode-encryption-key', [ 'error' => esc_html( $e->getMessage() ), 'via' => 'getenv(STOREENGINE_ENCRYPTION_KEY)', ], 0, $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } } elseif ( Constants::get_constant( 'STOREENGINE_ENCRYPTION_KEY' ) ) { try { $hex = sodium_hex2bin( Constants::get_constant( 'STOREENGINE_ENCRYPTION_KEY' ) ); } catch ( Exception $e ) { throw new StoreEngineException( esc_html__( 'Failed to decode encryption key.', 'storeengine' ), 'failed-to-decode-encryption-key', [ 'error' => esc_html( $e->getMessage() ), 'via' => 'STOREENGINE_ENCRYPTION_KEY', ], 0, $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } } elseif ( Constants::get_constant( 'SECURE_AUTH_KEY' ) ) { // Fallback to hashed SECURE_AUTH_KEY if no key is defined $hex = hash( 'sha256', Constants::get_constant( 'SECURE_AUTH_KEY' ), true ); } if ( ! $hex ) { throw new StoreEngineException( esc_html__( 'Encryption key is missing.', 'storeengine' ), 'encryption-key-missing' ); } self::$key = $hex; } return self::$key; } /** * Encrypts the given data using XChaCha20-Poly1305. * * @param string|int|float|bool|array|object $data * The data to encrypt. Arrays/objects are JSON-encoded. * * @return string Base64-encoded ciphertext (nonce + encrypted data). * @throws StoreEngineException */ public static function encrypt( $data ): string { try { // Serialize arrays/objects $data = maybe_serialize( $data ); // Generate a unique random nonce per encryption $nonce = random_bytes( self::NONCE_LEN ); // Perform authenticated encryption (AEAD), without associated data $ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( $data, '', $nonce, self::getKey() ); // Return base64 encoded (nonce + ciphertext) return base64_encode( $nonce . $ciphertext ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } catch ( Exception $e ) { throw new StoreEngineException( esc_html__( 'Failed to encrypt data.', 'storeengine' ), 'failed-to-encrypt-data', [ 'error' => esc_html( $e->getMessage() ) ], 0, $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } } /** * Decrypts data encrypted with encrypt(). * * @param ?string $data Base64-encoded string (nonce + ciphertext). * * @return string|array|false The decrypted value, auto-decoded from JSON if applicable. False on failure. * @throws StoreEngineException */ public static function decrypt( ?string $data ) { try { if ( ! $data ) { return false; } // Decode base64 input $decoded = base64_decode( $data, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode if ( false === $decoded ) { return $data; // Maybe not encrypted. } if ( strlen( $decoded ) <= self::NONCE_LEN ) { return false; // Maybe not encrypted data. } // Extract nonce and ciphertext $nonce = substr( $decoded, 0, self::NONCE_LEN ); $ciphertext = substr( $decoded, self::NONCE_LEN ); // Decrypt the ciphertext, without associated data. $plaintext = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $ciphertext, '', $nonce, self::getKey() ); return maybe_unserialize( $plaintext ); } catch ( Exception $e ) { throw new StoreEngineException( esc_html__( 'Failed to decrypt data.', 'storeengine' ), 'failed-to-decrypt-data', [ 'error' => esc_html( $e->getMessage() ) ], 0, $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped ); } } /** * Retrieves the value of a transient. * * If the transient does not exist, does not have a value, or has expired, * then the return value will be false. * * @param string $transient Transient name. Expected to not be SQL-escaped. * * @return array|false|string Value of transient. * @throws StoreEngineException */ public static function get_transient( string $transient ) { $value = get_transient( $transient ); if ( false === $value ) { return false; } return self::decrypt( $value ); } /** * Sets/updates the value of a transient. * * You do not need to serialize values. If the value needs to be serialized, * then it will be serialized before it is set. * * @param string $transient Transient name. Expected to not be SQL-escaped. * Must be 172 characters or fewer in length. * @param mixed $value Transient value. Must be serializable if non-scalar. * Expected to not be SQL-escaped. * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). * * @return bool True if the value was set, false otherwise. * @throws StoreEngineException */ public static function set_transient( string $transient, $value, int $expiration = 0 ): bool { return set_transient( $transient, self::encrypt( $value ), $expiration ); } } // End of file crypto.php