false]; return $flags; } /** * 惰性取得 32 字节站点密钥,首次调用时自动生成。 * @return string|false 密钥原始字节;false 表示加密不可用 */ function settingsCryptoKey() { static $key = null; if ($key !== null) return $key; if (!function_exists('openssl_encrypt')) return $key = false; $file = settingsKeyFile(); if (is_file($file)) { $raw = @file_get_contents($file); if ($raw !== false) { $decoded = base64_decode(trim($raw), true); if ($decoded !== false && strlen($decoded) === 32) return $key = $decoded; } // 文件存在但内容损坏:不覆盖(覆盖会让仍可用的旧密钥彻底失效),仅报不可用 return $key = false; } $dir = dirname($file); if (!is_dir($dir) && !@mkdir($dir, 0755, true)) return $key = false; try { $fresh = random_bytes(32); } catch (Exception $e) { return $key = false; } if (@file_put_contents($file, base64_encode($fresh) . "\n") === false) return $key = false; @chmod($file, 0600); return $key = $fresh; } // 加密能力是否可用(设置页展示用) function settingsEncryptionAvailable() { return settingsCryptoKey() !== false; } // 当前实际使用的加密算法名(设置页展示用) function settingsCipherLabel() { if (!settingsEncryptionAvailable()) return ''; return in_array('aes-256-gcm', openssl_get_cipher_methods(), true) ? 'AES-256-GCM' : 'AES-256-CBC + HMAC-SHA256'; } // 是否发生过解密失败(密钥丢失/损坏) function settingsDecryptFailed() { $flags = &settingsCryptoFlags(); return $flags['decrypt_failed']; } /** * 加密一个值。优先 AES-256-GCM(AEAD),老环境退化为 AES-256-CBC + HMAC-SHA256。 * @return string|null null 表示加密不可用 */ function settingsEncrypt($plain) { $plain = (string)$plain; if ($plain === '') return ''; $key = settingsCryptoKey(); if ($key === false) return null; $useGcm = in_array('aes-256-gcm', openssl_get_cipher_methods(), true); if ($useGcm) { $iv = random_bytes(12); $tag = ''; $ct = openssl_encrypt($plain, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16); if ($ct === false) return null; return SETTINGS_ENC_PREFIX . 'gcm:' . base64_encode($iv . $tag . $ct); } $iv = random_bytes(16); $ct = openssl_encrypt($plain, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); if ($ct === false) return null; $mac = hash_hmac('sha256', $iv . $ct, $key, true); return SETTINGS_ENC_PREFIX . 'cbc:' . base64_encode($iv . $mac . $ct); } /** * 解密一个值。 * 非密文(历史遗留的明文)原样返回,便于无损升级; * 密文但解不开时返回 null(调用方应视为"未配置")。 */ function settingsDecrypt($stored) { $stored = (string)$stored; if (strpos($stored, SETTINGS_ENC_PREFIX) !== 0) return $stored; $key = settingsCryptoKey(); if ($key === false) { $flags = &settingsCryptoFlags(); $flags['decrypt_failed'] = true; return null; } $body = substr($stored, strlen(SETTINGS_ENC_PREFIX)); $sep = strpos($body, ':'); if ($sep === false) { $flags = &settingsCryptoFlags(); $flags['decrypt_failed'] = true; return null; } $algo = substr($body, 0, $sep); $raw = base64_decode(substr($body, $sep + 1), true); if ($raw === false) { $flags = &settingsCryptoFlags(); $flags['decrypt_failed'] = true; return null; } $fail = function () { $flags = &settingsCryptoFlags(); $flags['decrypt_failed'] = true; return null; }; if ($algo === 'gcm') { if (strlen($raw) < 12 + 16) return $fail(); $iv = substr($raw, 0, 12); $tag = substr($raw, 12, 16); $ct = substr($raw, 28); $pt = openssl_decrypt($ct, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag); return $pt === false ? $fail() : $pt; } if ($algo === 'cbc') { if (strlen($raw) < 16 + 32 + 16) return $fail(); $iv = substr($raw, 0, 16); $mac = substr($raw, 16, 32); $ct = substr($raw, 48); if (!hash_equals($mac, hash_hmac('sha256', $iv . $ct, $key, true))) return $fail(); $pt = openssl_decrypt($ct, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); return $pt === false ? $fail() : $pt; } return $fail(); } // 是否为敏感设置项 function settingsIsSensitive($name) { static $set = null; if ($set === null) $set = array_flip(settingsSensitiveNames()); return isset($set[$name]); } // 直接落库(加密后写入),绕过内存缓存——仅供内部升级使用 function settingsRawPut($name, $value) { try { $enc = settingsEncrypt($value); if ($enc === null) return false; // 加密不可用时宁可不动,避免写入不可解密的数据 $db = getDB(); $stmt = $db->prepare("INSERT OR REPLACE INTO settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)"); $stmt->execute([$name, $enc]); return true; } catch (PDOException $e) { return false; } } // 读取全部设置到静态缓存(每次请求只查一次库) function &settingsCache() { static $cache = null; if ($cache === null) { $cache = []; try { $db = getDB(); $rows = $db->query("SELECT name, value FROM settings"); $upgrade = []; foreach ($rows as $row) { $name = $row['name']; $raw = (string)$row['value']; if (!settingsIsSensitive($name)) { $cache[$name] = $raw; continue; } if (strpos($raw, SETTINGS_ENC_PREFIX) === 0) { $plain = settingsDecrypt($raw); // 解不开时按"未配置"处理:站点继续可用,只是需要重新填密钥 $cache[$name] = $plain === null ? '' : $plain; } else { // 历史明文:本次读到后自动升级为密文 $cache[$name] = $raw; $upgrade[$name] = $raw; } } foreach ($upgrade as $name => $plain) { settingsRawPut($name, $plain); } } catch (PDOException $e) { // 表尚未建立时退化为"全部使用默认值",不阻断页面 $cache = []; } } return $cache; } // 读取单个设置 function getSetting($name, $default = null) { $cache = &settingsCache(); return array_key_exists($name, $cache) ? $cache[$name] : $default; } // 读取布尔型设置 function getSettingBool($name, $default = false) { $v = getSetting($name, null); if ($v === null || $v === '') return $default; return $v === '1' || $v === 1 || $v === true; } // 读取 JSON 型设置(解析失败时返回 $default) function getSettingJson($name, $default = []) { $raw = getSetting($name, null); if ($raw === null || $raw === '') return $default; $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : $default; } // 写入单个设置(敏感项自动加密后落库,内存缓存里始终是明文) function setSetting($name, $value) { $value = (string)$value; $store = $value; if (settingsIsSensitive($name)) { $enc = settingsEncrypt($value); // 加密不可用时退回明文,保证功能可用(设置页会显著告警) if ($enc !== null) $store = $enc; } $db = getDB(); $stmt = $db->prepare("INSERT OR REPLACE INTO settings (name, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)"); $stmt->execute([$name, $store]); $cache = &settingsCache(); $cache[$name] = $value; } // 写入 JSON 型设置 function setSettingJson($name, array $value) { setSetting($name, json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); } // 删除单个设置 function deleteSetting($name) { $db = getDB(); $stmt = $db->prepare("DELETE FROM settings WHERE name = ?"); $stmt->execute([$name]); $cache = &settingsCache(); unset($cache[$name]); } // ===== 存储相关设置的便捷读取 ===== // 当前生效的存储驱动标识:local / oss / cos / qiniu function currentStorageDriver() { $d = getSetting('storage_driver', 'local'); return in_array($d, ['local', 'oss', 'cos', 'qiniu'], true) ? $d : 'local'; } // 全部驱动的配置(形如 ['oss' => [...], 'cos' => [...]]) function allStorageConfigs() { return getSettingJson('storage_config', []); } // 指定驱动的配置(合并默认值) function storageConfigFor($driver) { $defaults = [ 'oss' => [ 'auth_mode' => 'ak', // ak | ecs_role 'access_key_id' => '', 'access_key_secret' => '', 'bucket' => '', 'endpoint' => '', // 例:oss-cn-beijing.aliyuncs.com 'prefix' => '', // 对象前缀(桶内目录),可留空 'ram_role_name' => '', // 留空则自动发现 'custom_domain' => '', // 可选,CDN / 自定义域名 'internal_endpoint' => '1', // 服务端读取优先走内网 endpoint(同地域免流量费) 'https' => '1', ], 'cos' => [ 'auth_mode' => 'ak', // ak | cvm_role 'secret_id' => '', 'secret_key' => '', 'bucket' => '', // 含 APPID,例:mybucket-1250000000 'region' => '', // 例:ap-beijing 'prefix' => '', // 对象前缀(桶内目录),可留空 'cam_role_name' => '', // 留空则自动发现 'custom_domain' => '', 'https' => '1', ], 'qiniu' => [ 'access_key' => '', 'secret_key' => '', 'bucket' => '', 'region' => 'z0', // z0 华东 / z1 华北 / z2 华南 / na0 北美 / as0 亚太 'prefix' => '', // 对象前缀(桶内目录),可留空 'custom_domain' => '', // 七牛必须绑定域名才能访问 'https' => '1', ], ]; $cfg = allStorageConfigs(); $saved = isset($cfg[$driver]) && is_array($cfg[$driver]) ? $cfg[$driver] : []; return array_merge($defaults[$driver] ?? [], $saved); } // 保存指定驱动的配置 function saveStorageConfig($driver, array $values) { $cfg = allStorageConfigs(); $cfg[$driver] = $values; setSettingJson('storage_config', $cfg); } // 所有驱动的显示名 function storageDriverLabels() { return [ 'local' => '本地硬盘', 'oss' => '阿里云 OSS', 'cos' => '腾讯云 COS', 'qiniu' => '七牛云 Kodo', ]; }