chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -18,6 +18,9 @@ import {
encrypt,
decrypt,
generateMasterKey,
generateSalt,
arrayToBase64,
base64ToArray,
} from './crypto-utils';
// Cache for keyring availability check
@@ -27,9 +30,6 @@ let keyringAvailable: boolean | null = null;
const ENCRYPTED_PREFIX = 'enc_';
const MASTER_KEY_NAME = 'zclaw-master-key';
// Cache for the derived crypto key
let cachedCryptoKey: CryptoKey | null = null;
/**
* Check if secure storage (keyring) is available
*/
@@ -138,25 +138,6 @@ export const secureStorage = {
* Now with AES-GCM encryption for non-Tauri environments
*/
/**
* Get or create the master encryption key for localStorage fallback
*/
async function getOrCreateMasterKey(): Promise<CryptoKey> {
if (cachedCryptoKey) {
return cachedCryptoKey;
}
let masterKeyRaw = localStorage.getItem(MASTER_KEY_NAME);
if (!masterKeyRaw) {
masterKeyRaw = generateMasterKey();
localStorage.setItem(MASTER_KEY_NAME, masterKeyRaw);
}
cachedCryptoKey = await deriveKey(masterKeyRaw);
return cachedCryptoKey;
}
/**
* Check if a stored value is encrypted (has iv and data fields)
*/
@@ -169,8 +150,21 @@ function isEncrypted(value: string): boolean {
}
}
/**
* Check if a stored value uses the v2 format (with random salt)
*/
function isV2Encrypted(value: string): boolean {
try {
const parsed = JSON.parse(value);
return parsed && parsed.version === 2 && typeof parsed.salt === 'string' && typeof parsed.iv === 'string' && typeof parsed.data === 'string';
} catch {
return false;
}
}
/**
* Write encrypted data to localStorage
* Uses random salt per encryption (v2 format) for forward secrecy
*/
async function writeEncryptedLocalStorage(key: string, value: string): Promise<void> {
try {
@@ -178,45 +172,78 @@ async function writeEncryptedLocalStorage(key: string, value: string): Promise<v
if (!value) {
localStorage.removeItem(encryptedKey);
// Also remove legacy unencrypted key
localStorage.removeItem(key);
return;
}
try {
const cryptoKey = await getOrCreateMasterKey();
let masterKeyRaw = localStorage.getItem(MASTER_KEY_NAME);
if (!masterKeyRaw) {
masterKeyRaw = generateMasterKey();
localStorage.setItem(MASTER_KEY_NAME, masterKeyRaw);
}
// Generate a random salt for each encryption (v2 format)
const salt = generateSalt(16);
const cryptoKey = await deriveKey(masterKeyRaw, salt);
const encrypted = await encrypt(value, cryptoKey);
localStorage.setItem(encryptedKey, JSON.stringify(encrypted));
// Remove legacy unencrypted key if it exists
const encryptedPayload = {
version: 2,
salt: arrayToBase64(salt),
iv: encrypted.iv,
data: encrypted.data,
};
localStorage.setItem(encryptedKey, JSON.stringify(encryptedPayload));
localStorage.removeItem(key);
} catch (error) {
console.error('[SecureStorage] Encryption failed:', error);
// Fallback to plaintext if encryption fails (should not happen)
localStorage.setItem(key, value);
// Do NOT fall back to plaintext — throw to signal the error
throw error;
}
} catch {
// Ignore localStorage failures
} catch (error) {
console.error('[SecureStorage] Failed to write encrypted localStorage:', error);
throw error;
}
}
/**
* Read and decrypt data from localStorage
* Supports both encrypted and legacy unencrypted formats
* Supports v2 (random salt), v1 (static salt), and legacy unencrypted formats
*/
async function readEncryptedLocalStorage(key: string): Promise<string | null> {
try {
// Try encrypted key first
const encryptedKey = ENCRYPTED_PREFIX + key;
const encryptedRaw = localStorage.getItem(encryptedKey);
if (encryptedRaw && isEncrypted(encryptedRaw)) {
try {
const cryptoKey = await getOrCreateMasterKey();
const encrypted = JSON.parse(encryptedRaw);
return await decrypt(encrypted, cryptoKey);
} catch (error) {
console.error('[SecureStorage] Decryption failed:', error);
// Fall through to try legacy key
if (encryptedRaw) {
const masterKeyRaw = localStorage.getItem(MASTER_KEY_NAME);
// Try v2 format (random salt)
if (masterKeyRaw && isV2Encrypted(encryptedRaw)) {
try {
const parsed = JSON.parse(encryptedRaw);
const salt = base64ToArray(parsed.salt);
const cryptoKey = await deriveKey(masterKeyRaw, salt);
return await decrypt(
{ iv: parsed.iv, data: parsed.data },
cryptoKey,
);
} catch (error) {
console.error('[SecureStorage] v2 decryption failed:', error);
// Fall through to try v1
}
}
// Try v1 format (static salt, backward compat)
if (masterKeyRaw && isEncrypted(encryptedRaw)) {
try {
const cryptoKey = await deriveKey(masterKeyRaw); // uses legacy static salt
const encrypted = JSON.parse(encryptedRaw);
return await decrypt(encrypted, cryptoKey);
} catch (error) {
console.error('[SecureStorage] v1 decryption failed:', error);
}
}
}