Files
SilenceLurker 0be6a86e94 feat: add API config system, FilePipe backend, and ConfigManager
- ConfigManager: route sensitive keys (API keys) to localStorage,
  migrate existing values out of extension_settings on startup
- ApiKeyStore: local/cloud storage modes with RSA+AES hybrid encryption
- ApiProfileManager: named connection profiles (chat/embedding/rerank)
  with per-slot type-validated assignments
- FilePipe: complete IndexedDB backend (read/write/delete/list/stat)
- Amily2Bus: inject FilePipe via forPlugin() capability token
- UI: api-config-panel with profile CRUD and slot assignment
- TableSystemService: initial service layer scaffold
- logger.js: XSS fix
2026-03-10 22:07:15 +08:00

360 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ApiKeyStore — 专用 API 凭证管理模块
*
* 存储策略(用户可选):
*
* 'local'(默认)
* API Key 明文存储在 localStorage前缀 amily2_secure_
* 不随 ST 设置上传,绝对安全,但换设备需重新填写。
*
* 'cloud'
* API Key 使用混合加密RSA-OAEP + AES-256-GCM后存入 extension_settings。
* 私钥仅保存在本设备 localStorage服务端只能看到密文。
* 换设备时密文跟着走,但需要在新设备上重新生成密钥对并重新输入 Key。
* 可大幅提升技术攻击成本(被动读取 settings.json 完全无效)。
*
* 注意API URL 不是凭证,始终存储在 extension_settings云同步方便多端
*
* Bus 注册名:'ApiKeyStore'
*
* 公开接口query('ApiKeyStore')
* getKey(field) — 读取指定凭证(自动按模式解密)
* setKey(field, value) — 写入指定凭证(自动按模式加密)
* getMode() — 返回当前存储模式 'local' | 'cloud'
* setMode(mode) — 切换存储模式(会迁移现有数据)
* isCloudReady() — cloud 模式下密钥对是否已就绪
* generateKeyPair() — 生成新密钥对(会清除旧加密数据)
* getPublicKeyInfo() — 返回公钥摘要字符串(用于 UI 展示)
* exportEncryptedBackup() — 导出加密备份JSON含密文+公钥,不含私钥)
*/
import { extension_settings } from "/scripts/extensions.js";
import { saveSettingsDebounced } from "/script.js";
import { extensionName } from "../../settings.js";
import { SENSITIVE_KEYS } from "../sensitive-keys.js";
import {
generateKeyPair,
serializeKeyPair,
importPublicKey,
importPrivateKey,
encrypt,
decrypt,
} from "./crypto-utils.js";
// ── 存储 key 常量 ─────────────────────────────────────────────────────────────
const LS_MODE_KEY = 'amily2_keystore_mode'; // 'local' | 'cloud'
const LS_PRIVATE_KEY = 'amily2_keypair_private'; // JWK 字符串
const LS_PLAIN_PREFIX = 'amily2_secure_'; // local 模式明文前缀
const EXT_PUBKEY = 'amily2_pubkey'; // extension_settings 中的公钥
const EXT_ENC_PREFIX = 'amily2_enc_'; // extension_settings 中的密文前缀
// ── ApiKeyStore ───────────────────────────────────────────────────────────────
class ApiKeyStore {
constructor() {
this._publicKey = null; // CryptoKey运行时缓存
this._privateKey = null; // CryptoKey运行时缓存
this._keyReady = false;
this._initPromise = null;
}
// ── 初始化 ──────────────────────────────────────────────────────────────
/**
* 异步初始化:若为 cloud 模式则加载密钥对到内存缓存。
* 由 Bus 注册后自动调用,也可手动 await。
*/
async init() {
if (this.getMode() === 'cloud') {
await this._loadKeyPair();
}
}
// ── 公开 API ────────────────────────────────────────────────────────────
/** 读取指定凭证字段SENSITIVE_KEYS 内的字段) */
async getKey(field) {
if (!SENSITIVE_KEYS.has(field)) {
console.warn(`[ApiKeyStore] "${field}" 不是凭证字段,请用 configManager.get() 读取普通配置。`);
return undefined;
}
if (this.getMode() === 'cloud') {
return this._getCloud(field);
}
return this._getLocal(field);
}
/** 写入指定凭证字段 */
async setKey(field, value) {
if (!SENSITIVE_KEYS.has(field)) {
console.warn(`[ApiKeyStore] "${field}" 不是凭证字段。`);
return;
}
if (this.getMode() === 'cloud') {
await this._setCloud(field, value);
} else {
this._setLocal(field, value);
}
}
/** 当前存储模式 */
getMode() {
return localStorage.getItem(LS_MODE_KEY) || 'local';
}
/**
* 切换存储模式并迁移现有数据。
* local → cloud读出明文 → 加密 → 写入 extension_settings → 清除 localStorage 明文
* cloud → local解密 → 写入 localStorage → 清除 extension_settings 密文
* @param {'local'|'cloud'} mode
*/
async setMode(mode) {
const current = this.getMode();
if (current === mode) return;
if (mode === 'cloud') {
if (!this._keyReady) {
await this.generateKeyPair(); // 首次切换自动生成密钥对
}
await this._migrateLocalToCloud();
} else {
await this._migrateCloudToLocal();
}
localStorage.setItem(LS_MODE_KEY, mode);
console.info(`[ApiKeyStore] 存储模式已切换为 "${mode}"。`);
}
/** cloud 模式下密钥对是否已就绪 */
isCloudReady() {
return this._keyReady;
}
/**
* 按任意 ID 存储凭证(供 ApiProfileManager 使用key = `profile_<id>`)。
* 走与 setKey 相同的加密路由。
*/
async storeById(id, value) {
const field = `profile_${id}`;
if (this.getMode() === 'cloud') {
await this._setCloud(field, value);
} else {
this._setLocal(field, value);
}
}
/**
* 按任意 ID 读取凭证(供 ApiProfileManager 使用)。
*/
async retrieveById(id) {
const field = `profile_${id}`;
if (this.getMode() === 'cloud') {
return this._getCloud(field);
}
return this._getLocal(field);
}
/**
* 删除指定 ID 的凭证Profile 删除时调用)。
*/
deleteById(id) {
const field = `profile_${id}`;
localStorage.removeItem(LS_PLAIN_PREFIX + field);
const settings = extension_settings[extensionName];
if (settings?.[EXT_ENC_PREFIX + field]) {
delete settings[EXT_ENC_PREFIX + field];
saveSettingsDebounced();
}
}
/**
* 生成新的 RSA 密钥对。
* 警告:会清除所有已加密的 cloud 模式凭证(旧私钥无法解密)。
*/
async generateKeyPair() {
const keyPair = await generateKeyPair();
const { publicJwk, privateJwk } = await serializeKeyPair(keyPair);
// 清除旧密文(旧私钥无法解密新密钥对加密的内容)
this._clearAllCloudCiphers();
// 私钥存 localStorage公钥存 extension_settings公钥不需要保密
localStorage.setItem(LS_PRIVATE_KEY, privateJwk);
const settings = extension_settings[extensionName] || {};
settings[EXT_PUBKEY] = publicJwk;
saveSettingsDebounced();
// 更新运行时缓存
this._publicKey = await importPublicKey(publicJwk);
this._privateKey = await importPrivateKey(privateJwk);
this._keyReady = true;
console.info('[ApiKeyStore] 新密钥对已生成。请重新输入所有 API Key。');
}
/**
* 返回公钥的简短指纹SHA-256 前 8 字节Base64用于 UI 展示。
* @returns {Promise<string>}
*/
async getPublicKeyInfo() {
const jwkStr = extension_settings[extensionName]?.[EXT_PUBKEY];
if (!jwkStr) return '(未生成)';
const jwk = JSON.parse(jwkStr);
const raw = new TextEncoder().encode(jwk.n); // RSA modulus
const hash = await crypto.subtle.digest('SHA-256', raw);
const hex = Array.from(new Uint8Array(hash)).slice(0, 8)
.map(b => b.toString(16).padStart(2, '0')).join(':');
return `RSA-2048 · ${hex}`;
}
/**
* 导出可备份的加密摘要(包含公钥 + 所有密文,不含私钥)。
* 仅供参考,不能用于在新设备上恢复(因为私钥不在其中)。
* @returns {Object}
*/
exportEncryptedBackup() {
const settings = extension_settings[extensionName] || {};
const backup = { publicKey: settings[EXT_PUBKEY], encrypted: {} };
for (const field of SENSITIVE_KEYS) {
const cipher = settings[EXT_ENC_PREFIX + field];
if (cipher) backup.encrypted[field] = cipher;
}
return backup;
}
// ── 内部local 模式 ────────────────────────────────────────────────────
_getLocal(field) {
return localStorage.getItem(LS_PLAIN_PREFIX + field) ?? '';
}
_setLocal(field, value) {
if (value !== null && value !== undefined && value !== '') {
localStorage.setItem(LS_PLAIN_PREFIX + field, value);
} else {
localStorage.removeItem(LS_PLAIN_PREFIX + field);
}
}
// ── 内部cloud 模式 ────────────────────────────────────────────────────
async _getCloud(field) {
if (!this._keyReady) {
console.warn('[ApiKeyStore] cloud 模式密钥未就绪,无法解密。');
return '';
}
const cipher = extension_settings[extensionName]?.[EXT_ENC_PREFIX + field];
if (!cipher) return '';
try {
return await decrypt(this._privateKey, cipher);
} catch (e) {
console.error(`[ApiKeyStore] 解密 "${field}" 失败(私钥不匹配?):`, e);
return '';
}
}
async _setCloud(field, value) {
if (!this._keyReady) {
console.warn('[ApiKeyStore] cloud 模式密钥未就绪,无法加密。');
return;
}
const settings = extension_settings[extensionName] || {};
if (value !== null && value !== undefined && value !== '') {
settings[EXT_ENC_PREFIX + field] = await encrypt(this._publicKey, value);
} else {
delete settings[EXT_ENC_PREFIX + field];
}
saveSettingsDebounced();
}
// ── 内部:迁移 ──────────────────────────────────────────────────────────
async _migrateLocalToCloud() {
for (const field of SENSITIVE_KEYS) {
const plain = this._getLocal(field);
if (plain) {
await this._setCloud(field, plain);
localStorage.removeItem(LS_PLAIN_PREFIX + field);
console.info(`[ApiKeyStore] "${field}" 已加密迁移至云同步。`);
}
}
}
async _migrateCloudToLocal() {
for (const field of SENSITIVE_KEYS) {
const plain = await this._getCloud(field);
if (plain) {
this._setLocal(field, plain);
console.info(`[ApiKeyStore] "${field}" 已解密迁移至本地存储。`);
}
}
this._clearAllCloudCiphers();
}
_clearAllCloudCiphers() {
const settings = extension_settings[extensionName];
if (!settings) return;
let changed = false;
for (const field of SENSITIVE_KEYS) {
if (settings[EXT_ENC_PREFIX + field]) {
delete settings[EXT_ENC_PREFIX + field];
changed = true;
}
}
if (changed) saveSettingsDebounced();
}
// ── 内部:密钥加载 ──────────────────────────────────────────────────────
async _loadKeyPair() {
const privateJwk = localStorage.getItem(LS_PRIVATE_KEY);
const publicJwk = extension_settings[extensionName]?.[EXT_PUBKEY];
if (!privateJwk || !publicJwk) {
console.warn('[ApiKeyStore] cloud 模式:本地未找到密钥对,请生成新密钥对。');
this._keyReady = false;
return;
}
try {
this._privateKey = await importPrivateKey(privateJwk);
this._publicKey = await importPublicKey(publicJwk);
this._keyReady = true;
console.info('[ApiKeyStore] cloud 模式密钥对已加载。');
} catch (e) {
console.error('[ApiKeyStore] 密钥对加载失败(数据损坏?):', e);
this._keyReady = false;
}
}
}
// ── 单例导出 ─────────────────────────────────────────────────────────────────
export const apiKeyStore = new ApiKeyStore();
// ── Bus 注册 ──────────────────────────────────────────────────────────────────
setTimeout(async () => {
try {
// 先初始化cloud 模式下加载密钥)
await apiKeyStore.init();
const _ctx = window.Amily2Bus?.register('ApiKeyStore');
if (!_ctx) {
console.warn('[ApiKeyStore] Amily2Bus 尚未就绪,注册跳过。');
return;
}
_ctx.expose({
getKey: (field) => apiKeyStore.getKey(field),
setKey: (field, value) => apiKeyStore.setKey(field, value),
getMode: () => apiKeyStore.getMode(),
setMode: (mode) => apiKeyStore.setMode(mode),
isCloudReady: () => apiKeyStore.isCloudReady(),
generateKeyPair: () => apiKeyStore.generateKeyPair(),
getPublicKeyInfo: () => apiKeyStore.getPublicKeyInfo(),
exportEncryptedBackup: () => apiKeyStore.exportEncryptedBackup(),
});
_ctx.log('ApiKeyStore', 'info', 'ApiKeyStore 服务已注册到 Bus。');
} catch (e) {
console.error('[ApiKeyStore] 初始化失败:', e);
}
}, 0);