feat(diagnostics): crypto 健檢 + backup 目錄探測 + 15 個 usermeta 欄位(PR-G)
- Crypto::is_key_derivable()(A v3.3.2):AUTH_KEY / SECURE_AUTH_SALT 皆缺
時回 false,讓金鑰推導優雅短路
- wp tmdo doctor 新增兩項健檢:
· backup 目錄 HTTP 可及性探測(200 = 紅旗並附 nginx 設定建議,
403/404 = OK,0 = 離線時退回檢查 .htaccess)(A v3.1.9)
· crypto 金鑰可推導性(缺常數時警告 notifier secrets 會以明文儲存)
- doctor_callback 呼叫簽章 3 參數 → 1 參數(A v3.0.3 修復)。**ABI 變更**:
AddOn 註冊的 callback 若依賴 rows / full_name 需自行調整
- Member_Fields::register_admin_prefs_group() 由 7 欄擴到 22 欄(A v3.1.4),
補上 wp_user_level / show_welcome_panel / wp_persisted_preferences /
wp_user-settings / community-events-location 等 15 個 WP 原生 usermeta,
這些先前全部落在 wp_usermeta
- capture_before_value 預設 true → false(A v3.1.5):每次受管寫入省一次
DB read。**行為變更**:需要 value_before 的消費者(audit log)要
add_filter( 'wpdo_capture_before_value', '__return_true' ) 明確開啟
unit 451 / integration 398 GREEN
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
* TMDO_Crypto::set_option( 'wpdo_slack_webhook', $url ); // writes v2
|
||||
* $url = TMDO_Crypto::get_option( 'wpdo_slack_webhook' ); // reads v1 or v2
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
* @package TMDO
|
||||
* @since 2.6.4 (v1 CBC)
|
||||
* @since 2.15.0 (v2 GCM, AEAD authentication)
|
||||
*/
|
||||
@@ -65,17 +65,34 @@ class TMDO_Crypto {
|
||||
* elsewhere. Both v1 and v2 use the same derived key (same secret material,
|
||||
* different cipher) so v1 ciphertext can be read after the v2 upgrade.
|
||||
*
|
||||
* Falls back to a sha256 of ABSPATH when constants are not defined
|
||||
* (unit-test environments). The fallback must be stable per request.
|
||||
* Returns empty string when both AUTH_KEY and SECURE_AUTH_SALT are absent —
|
||||
* callers treat '' as "encryption unavailable" and store plaintext instead.
|
||||
*
|
||||
* @return string 32 raw bytes.
|
||||
*/
|
||||
private static function derived_key(): string {
|
||||
$salt = defined( 'AUTH_KEY' ) ? AUTH_KEY : '';
|
||||
$salt .= defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : ABSPATH;
|
||||
$has_auth_key = defined( 'AUTH_KEY' ) && AUTH_KEY !== '';
|
||||
$has_auth_salt = defined( 'SECURE_AUTH_SALT' ) && SECURE_AUTH_SALT !== '';
|
||||
|
||||
if ( ! $has_auth_key && ! $has_auth_salt ) {
|
||||
// Both salts missing — key would be derived from predictable ABSPATH. Refuse.
|
||||
return '';
|
||||
}
|
||||
|
||||
$salt = $has_auth_key ? AUTH_KEY : '';
|
||||
$salt .= $has_auth_salt ? SECURE_AUTH_SALT : '';
|
||||
return substr( hash_hmac( 'sha256', 'wpdo_notifier_secrets_v1', $salt, true ), 0, 32 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the site has valid WP auth constants for key derivation.
|
||||
*
|
||||
* @return bool False when both AUTH_KEY and SECURE_AUTH_SALT are absent/empty.
|
||||
*/
|
||||
public static function is_key_derivable(): bool {
|
||||
return self::derived_key() !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a plaintext string with AES-256-GCM (v2 format).
|
||||
*
|
||||
@@ -89,13 +106,17 @@ class TMDO_Crypto {
|
||||
if ( ! function_exists( 'openssl_encrypt' ) ) {
|
||||
return $plaintext;
|
||||
}
|
||||
$key = self::derived_key();
|
||||
if ( '' === $key ) {
|
||||
return $plaintext;
|
||||
}
|
||||
$iv = random_bytes( self::IV_LEN_V2 );
|
||||
$tag = '';
|
||||
// phpcs:ignore -- $tag is reference output for GCM auth tag.
|
||||
$ciphertext = openssl_encrypt(
|
||||
$plaintext,
|
||||
self::CIPHER_V2,
|
||||
self::derived_key(),
|
||||
$key,
|
||||
OPENSSL_RAW_DATA,
|
||||
$iv,
|
||||
$tag,
|
||||
@@ -127,6 +148,9 @@ class TMDO_Crypto {
|
||||
if ( ! function_exists( 'openssl_decrypt' ) ) {
|
||||
return $stored;
|
||||
}
|
||||
if ( '' === self::derived_key() ) {
|
||||
return $stored;
|
||||
}
|
||||
if ( str_starts_with( $stored, self::PREFIX_V2 ) ) {
|
||||
return self::decrypt_v2( $stored );
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user