171 lines
4.7 KiB
PHP
171 lines
4.7 KiB
PHP
<?php
|
||
/**
|
||
* Шифрование и расшифровка секретов (AES-256-GCM, аутентифицированное).
|
||
*
|
||
* @package AI_Proxy_Manager
|
||
*/
|
||
|
||
defined( 'ABSPATH' ) || exit;
|
||
|
||
/**
|
||
* Класс шифрования секретов плагина.
|
||
*
|
||
* Использует AES-256-GCM (authenticated encryption): шифротекст защищён
|
||
* тегом аутентификации, поэтому подмена или повреждение значения в БД
|
||
* будет обнаружена при расшифровке. Ключ выводится из солей wp-config.php,
|
||
* поэтому секреты нельзя расшифровать без доступа к конфигурации сайта.
|
||
*
|
||
* @since 1.0.0
|
||
*/
|
||
class AIPMSP_Crypto {
|
||
|
||
/**
|
||
* Используемый шифр.
|
||
*
|
||
* @var string
|
||
*/
|
||
const CIPHER = 'aes-256-gcm';
|
||
|
||
/**
|
||
* Префикс-маркер зашифрованного значения.
|
||
*
|
||
* @var string
|
||
*/
|
||
const PREFIX = 'aipmsp_enc::';
|
||
|
||
/**
|
||
* Длина тега аутентификации GCM в байтах.
|
||
*
|
||
* @var int
|
||
*/
|
||
const TAG_LENGTH = 16;
|
||
|
||
/**
|
||
* Возвращает 256-битный ключ шифрования, производный от солей WordPress.
|
||
*
|
||
* @since 1.0.0
|
||
*
|
||
* @return string Бинарный ключ длиной 32 байта.
|
||
*/
|
||
private static function get_key() {
|
||
$auth_key = defined( 'AUTH_KEY' ) ? AUTH_KEY : 'aipmsp-fallback-auth-key';
|
||
$auth_salt = defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : 'aipmsp-fallback-salt';
|
||
|
||
// hash() с raw_output=true даёт ровно 32 байта для AES-256.
|
||
return hash( 'sha256', $auth_key . $auth_salt, true );
|
||
}
|
||
|
||
/**
|
||
* Шифрует строку.
|
||
*
|
||
* @since 1.0.0
|
||
*
|
||
* @param string $plaintext Открытый текст.
|
||
* @return string Зашифрованное значение (PREFIX + base64(iv|tag|ciphertext)), либо пустая строка.
|
||
*/
|
||
public static function encrypt( $plaintext ) {
|
||
$plaintext = (string) $plaintext;
|
||
|
||
if ( '' === $plaintext ) {
|
||
return '';
|
||
}
|
||
|
||
if ( ! function_exists( 'openssl_encrypt' ) ) {
|
||
// Без OpenSSL шифровать нечем — не сохраняем секрет в открытом виде.
|
||
return '';
|
||
}
|
||
|
||
$iv_length = openssl_cipher_iv_length( self::CIPHER );
|
||
|
||
if ( false === $iv_length || $iv_length <= 0 ) {
|
||
return '';
|
||
}
|
||
|
||
$iv = random_bytes( $iv_length );
|
||
$tag = '';
|
||
|
||
$ciphertext = openssl_encrypt(
|
||
$plaintext,
|
||
self::CIPHER,
|
||
self::get_key(),
|
||
OPENSSL_RAW_DATA,
|
||
$iv,
|
||
$tag,
|
||
'',
|
||
self::TAG_LENGTH
|
||
);
|
||
|
||
if ( false === $ciphertext ) {
|
||
return '';
|
||
}
|
||
|
||
// Храним IV и тег вместе с шифротекстом: PREFIX . base64( iv . tag . ciphertext ).
|
||
return self::PREFIX . base64_encode( $iv . $tag . $ciphertext );
|
||
}
|
||
|
||
/**
|
||
* Расшифровывает строку, зашифрованную методом encrypt().
|
||
*
|
||
* @since 1.0.0
|
||
*
|
||
* @param string $stored Сохранённое значение.
|
||
* @return string Открытый текст, либо пустая строка при ошибке/подмене.
|
||
*/
|
||
public static function decrypt( $stored ) {
|
||
$stored = (string) $stored;
|
||
|
||
if ( '' === $stored ) {
|
||
return '';
|
||
}
|
||
|
||
if ( ! self::is_encrypted( $stored ) ) {
|
||
// Значение не зашифровано (например, осталось от ручной правки) — возвращаем как есть.
|
||
return $stored;
|
||
}
|
||
|
||
if ( ! function_exists( 'openssl_decrypt' ) ) {
|
||
return '';
|
||
}
|
||
|
||
$payload = base64_decode( substr( $stored, strlen( self::PREFIX ) ), true );
|
||
|
||
if ( false === $payload ) {
|
||
return '';
|
||
}
|
||
|
||
$iv_length = openssl_cipher_iv_length( self::CIPHER );
|
||
|
||
if ( false === $iv_length || strlen( $payload ) <= ( $iv_length + self::TAG_LENGTH ) ) {
|
||
return '';
|
||
}
|
||
|
||
$iv = substr( $payload, 0, $iv_length );
|
||
$tag = substr( $payload, $iv_length, self::TAG_LENGTH );
|
||
$ciphertext = substr( $payload, $iv_length + self::TAG_LENGTH );
|
||
|
||
$plaintext = openssl_decrypt(
|
||
$ciphertext,
|
||
self::CIPHER,
|
||
self::get_key(),
|
||
OPENSSL_RAW_DATA,
|
||
$iv,
|
||
$tag
|
||
);
|
||
|
||
// false означает либо ошибку, либо непрошедшую проверку тега аутентификации.
|
||
return ( false === $plaintext ) ? '' : $plaintext;
|
||
}
|
||
|
||
/**
|
||
* Проверяет, является ли значение зашифрованным этим классом.
|
||
*
|
||
* @since 1.0.0
|
||
*
|
||
* @param string $value Проверяемое значение.
|
||
* @return bool
|
||
*/
|
||
public static function is_encrypted( $value ) {
|
||
return is_string( $value ) && 0 === strpos( $value, self::PREFIX );
|
||
}
|
||
}
|