Initial commit

This commit is contained in:
2026-07-29 22:52:47 +05:00
commit cb5a058984
13 changed files with 2064 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
<?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 );
}
}