Move all domain folders from src/AKSRT/{Domain} to src/{Domain}; remap
Composer PSR-4 "AKSRT\\": "src/"; update audit script + README + ADR
paths; remove dead tools/build-psr4-map.php (scanned deleted inc/classes).
136 lines
5.0 KiB
PHP
136 lines
5.0 KiB
PHP
<?php
|
|
/**
|
|
* Encrypted identity provider credential storage.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Identity;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
final class SecretStore {
|
|
|
|
public const OPTION_KEY = 'aksrt_identity_secrets';
|
|
private const CONTEXT = 'aksrt-identity-secrets-v1';
|
|
|
|
/** @return array<string,string> */
|
|
public static function get(string $provider): array {
|
|
$stored = get_option(self::OPTION_KEY, []);
|
|
$record = is_array($stored) ? ($stored[sanitize_key($provider)] ?? null) : null;
|
|
if (!is_array($record)) {
|
|
return [];
|
|
}
|
|
|
|
$plain = self::decrypt($record);
|
|
if ($plain === '') {
|
|
return [];
|
|
}
|
|
$data = json_decode($plain, true);
|
|
if (!is_array($data)) {
|
|
return [];
|
|
}
|
|
|
|
$clean = [];
|
|
foreach ($data as $key => $value) {
|
|
if (is_string($key) && is_string($value)) {
|
|
$clean[sanitize_key($key)] = $value;
|
|
}
|
|
}
|
|
return $clean;
|
|
}
|
|
|
|
/** @param array<string,string|null> $changes */
|
|
public static function update(string $provider, array $changes): bool {
|
|
$provider = sanitize_key($provider);
|
|
if (!in_array($provider, ['aliyun', 'tencent'], true)) {
|
|
return false;
|
|
}
|
|
|
|
$current = self::get($provider);
|
|
foreach ($changes as $key => $value) {
|
|
$key = sanitize_key((string) $key);
|
|
if ($key === '') {
|
|
continue;
|
|
}
|
|
if ($value === null) {
|
|
unset($current[$key]);
|
|
} elseif (is_string($value) && $value !== '') {
|
|
$current[$key] = $value;
|
|
}
|
|
}
|
|
|
|
$stored = get_option(self::OPTION_KEY, []);
|
|
$stored = is_array($stored) ? $stored : [];
|
|
if ($current === []) {
|
|
unset($stored[$provider]);
|
|
} else {
|
|
$record = self::encrypt((string) wp_json_encode($current));
|
|
if (is_wp_error($record)) {
|
|
return false;
|
|
}
|
|
$stored[$provider] = $record;
|
|
}
|
|
return update_option(self::OPTION_KEY, $stored, false) || get_option(self::OPTION_KEY, []) === $stored;
|
|
}
|
|
|
|
/** @return array<string,array{configured:bool,fingerprint:string}> */
|
|
public static function status(string $provider): array {
|
|
$status = [];
|
|
foreach (self::get($provider) as $key => $value) {
|
|
$status[$key] = [
|
|
'configured' => $value !== '',
|
|
'fingerprint' => $value !== '' ? substr(hash('sha256', $value), -4) : '',
|
|
];
|
|
}
|
|
return $status;
|
|
}
|
|
|
|
/** @return array<string,string>|\WP_Error */
|
|
private static function encrypt(string $plain) {
|
|
$key = self::key();
|
|
if (function_exists('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
|
|
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
|
|
$cipher = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($plain, self::CONTEXT, $nonce, $key);
|
|
return ['driver' => 'sodium', 'nonce' => base64_encode($nonce), 'cipher' => base64_encode($cipher)];
|
|
}
|
|
if (function_exists('openssl_encrypt')) {
|
|
$nonce = random_bytes(12);
|
|
$tag = '';
|
|
$cipher = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, self::CONTEXT);
|
|
if (!is_string($cipher)) {
|
|
return new \WP_Error('aksrt_identity_encrypt', __('身份认证凭证加密失败', 'aksrt'));
|
|
}
|
|
return ['driver' => 'openssl', 'nonce' => base64_encode($nonce), 'tag' => base64_encode($tag), 'cipher' => base64_encode($cipher)];
|
|
}
|
|
return new \WP_Error('aksrt_identity_crypto', __('服务器不支持安全凭证加密', 'aksrt'));
|
|
}
|
|
|
|
/** @param array<string,mixed> $record */
|
|
private static function decrypt(array $record): string {
|
|
$nonce = base64_decode((string) ($record['nonce'] ?? ''), true);
|
|
$cipher = base64_decode((string) ($record['cipher'] ?? ''), true);
|
|
if (!is_string($nonce) || !is_string($cipher)) {
|
|
return '';
|
|
}
|
|
$key = self::key();
|
|
if (($record['driver'] ?? '') === 'sodium' && function_exists('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
|
|
$plain = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($cipher, self::CONTEXT, $nonce, $key);
|
|
return is_string($plain) ? $plain : '';
|
|
}
|
|
if (($record['driver'] ?? '') === 'openssl' && function_exists('openssl_decrypt')) {
|
|
$tag = base64_decode((string) ($record['tag'] ?? ''), true);
|
|
if (!is_string($tag)) {
|
|
return '';
|
|
}
|
|
$plain = openssl_decrypt($cipher, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $nonce, $tag, self::CONTEXT);
|
|
return is_string($plain) ? $plain : '';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
private static function key(): string {
|
|
return hash_hmac('sha256', self::CONTEXT, wp_salt('auth') . wp_salt('secure_auth'), true);
|
|
}
|
|
}
|