Files
ai-proxy-manager/includes/class-aipmsp-crypto.php
T
2026-07-29 22:52:47 +05:00

171 lines
4.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 );
}
}