import { ref, watch } from "vue"; import { useData, useRouter } from "vitepress"; import { isClient } from "vitepress-theme-teek"; // ========================= 核心配置 ========================= const ENCRYPTION_KEY = "your-32-char-key-here-12345678"; const SALT = new TextEncoder().encode("vp-protect-salt-2024"); const PBKDF2_ITERATIONS = 1000; const LOCAL_STORAGE_KEY: string = "tk:vpVerifiedPages"; export const DEFAULT_PROTECTED_ROUTES: ProtectedRoute[] = [ // { path: "/Encrypt/*", password: "1234" }, { path: "/Encrypt/*", password: "DowneyRem" }, ]; export interface ProtectedRoute { path: string; password: string; } // ========================= 安全工具函数 ========================= const isSecureContext = () => isClient && window.isSecureContext && !!window.crypto?.subtle; const uint8ToBase64 = (array: Uint8Array): string => { return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); }; const base64ToUint8 = (str: string): Uint8Array => { str = str.replace(/-/g, "+").replace(/_/g, "/"); const pad = str.length % 4; if (pad) str += "=".repeat(4 - pad); const binary = atob(str); const array = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) array[i] = binary.charCodeAt(i); return array; }; // ========================= 核心加密逻辑 (支持降级) ========================= const generateVerifyKey = async (key: string): Promise => { if (!isSecureContext()) { return btoa(encodeURIComponent(`${key}::${ENCRYPTION_KEY}`)); } const encoder = new TextEncoder(); const data = encoder.encode(`${key}::${ENCRYPTION_KEY}`); const hashBuffer = await crypto.subtle.digest("SHA-256", data); return uint8ToBase64(new Uint8Array(hashBuffer)); }; const getEncryptionKey = async (): Promise => { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(ENCRYPTION_KEY), { name: "PBKDF2" }, false, ["deriveKey"]); return crypto.subtle.deriveKey( { name: "PBKDF2", salt: SALT, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" }, keyMaterial, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] ); }; const storeData = async (data: string[]) => { if (!isSecureContext()) { // 降级:普通 Base64 存储 localStorage.setItem(LOCAL_STORAGE_KEY, btoa(JSON.stringify(data))); return; } const key = await getEncryptionKey(); const dataBuffer = new TextEncoder().encode(JSON.stringify(data)); const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, dataBuffer); const encryptedArray = new Uint8Array(encrypted); const result = `${uint8ToBase64(iv)}|${uint8ToBase64(encryptedArray.slice(0, -16))}|${uint8ToBase64(encryptedArray.slice(-16))}`; localStorage.setItem(LOCAL_STORAGE_KEY, result); }; const loadData = async (): Promise => { const stored = localStorage.getItem(LOCAL_STORAGE_KEY); if (!stored) return []; if (!isSecureContext() || !stored.includes('|')) { // 尝试降级解析 try { return JSON.parse(atob(stored)); } catch { return []; } } try { const [ivB, cipherB, tagB] = stored.split("|"); const iv = base64ToUint8(ivB); const encrypted = new Uint8Array([...base64ToUint8(cipherB), ...base64ToUint8(tagB)]); const key = await getEncryptionKey(); const decryptedBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, encrypted); return JSON.parse(new TextDecoder().decode(decryptedBuffer)); } catch (e) { return []; } }; // ========================= 核心逻辑 ========================= const matchProtectedRule = (currentPath: string, routes: ProtectedRoute[]) => { for (const rule of routes) { if (rule.path.endsWith("/*")) { const prefix = rule.path.slice(0, -1); if (currentPath.startsWith(prefix) && currentPath !== prefix) return { rule, prefix }; } else if (currentPath === rule.path) { return { rule, prefix: rule.path }; } } return { rule: null, prefix: null }; }; export function usePasswordProtection(customRoutes?: ProtectedRoute[]) { const { frontmatter } = useData(); const router = useRouter(); const showPassword = ref(false); const currentPassword = ref(""); const currentVerifyPrefix = ref(null); const checkProtection = async (path: string) => { if (!isClient) return; const verifiedList = await loadData(); const verifiedHashes = new Set(verifiedList); // 1. 检查 Frontmatter if (frontmatter.value?.password) { const hash = await generateVerifyKey(path); const isVerified = verifiedHashes.has(hash); showPassword.value = !isVerified; currentPassword.value = String(frontmatter.value.password); currentVerifyPrefix.value = path; return; } // 2. 匹配规则 const { rule, prefix } = matchProtectedRule(path, customRoutes || DEFAULT_PROTECTED_ROUTES); if (rule && prefix) { const hash = await generateVerifyKey(prefix); const isVerified = verifiedHashes.has(hash); showPassword.value = !isVerified; currentPassword.value = String(rule.password); currentVerifyPrefix.value = prefix; } else { showPassword.value = false; } }; watch(() => router.route.path, checkProtection, { immediate: true }); const handleVerified = async (success: boolean) => { if (success && currentVerifyPrefix.value) { const verifiedList = await loadData(); const hash = await generateVerifyKey(currentVerifyPrefix.value); if (!verifiedList.includes(hash)) { verifiedList.push(hash); await storeData(verifiedList); } showPassword.value = false; } }; return { showPassword, currentPassword, currentVerifyPrefix, handleVerified }; }