This commit is contained in:
2026-10-16 16:34:15 +08:00
parent 7a3769670b
commit ff366a4daf
80 changed files with 9961 additions and 888 deletions

View File

@@ -0,0 +1,260 @@
// main/secret-store.js —— aiApiKey safeStorage 加密层单测(audit R3 P0-1)
//
// 覆盖:
// - encrypt / decrypt 的对称与幂等
// - safeStorage 不可用(典型 Linux 无 keyring)降级 + warn-once
// - decrypt 遇损坏包络 / keyring 不可用 → 返回 ''(不抛)
// - 旧版明文 passthrough(懒迁移关键:旧 config.json 仍能解密出 Key)
// - isAvailable 处理 safeStorage 自身抛错 / 未注册的情况
//
// 注入模式:模块自暴露的 _setSafeStorage(mock)。原因同 main/config-store.js
// —— vi.mock('electron') 拦不住 CJS require。mock 用闭包记录 isEncryptionAvailable
// 状态和调用计数,单测间通过 _reset() 重置。
// @vitest-environment node
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
// 强制每次 beforeEach 拿一份新模块实例(_reset 也会清 _warnedUnavailable 标志)
const loadModule = async () => {
// 走 ESM dynamic import;secret-store.js 是 CJS,但 Node 处理 CJS↔ESM 互操作
// 给一个 .default 包含 module.exports 的默认导出(如果用 require() 则没有)
// —— secret-store.js 用 module.exports = {...},ESM 默认 import 拿到的就是
// module.exports 本身,不需要 .default 访问。
const mod = await import('../../main/secret-store.js');
return mod;
};
describe('secret-store:encrypt / decrypt 对称', () => {
let mod;
let encryptCalls;
let decryptCalls;
beforeEach(async () => {
mod = await loadModule();
encryptCalls = [];
decryptCalls = [];
// mock safeStorage:明文 ↔ base64('ENC:<plaintext>') 做对称回路
mod._setSafeStorage({
isEncryptionAvailable: () => true,
encryptString: (plain) => {
encryptCalls.push(plain);
return Buffer.from(`ENC:${plain}`, 'utf-8');
},
decryptString: (buf) => {
const s = buf.toString('utf-8');
decryptCalls.push(s);
if (!s.startsWith('ENC:')) throw new Error('mock: bad envelope');
return s.slice('ENC:'.length);
},
});
});
afterEach(() => {
mod._reset();
});
it('encrypt(\'\') → \'\'', () => {
expect(mod.encrypt('')).toBe('');
});
it('encrypt(null) → \'\'', () => {
expect(mod.encrypt(null)).toBe('');
});
it('encrypt(undefined) → \'\'', () => {
expect(mod.encrypt(undefined)).toBe('');
});
it('encrypt(\'sk-test\') → 包络(enc:v1: 前缀 + base64)', () => {
const out = mod.encrypt('sk-test');
expect(out.startsWith(mod.ENVELOPE_PREFIX)).toBe(true);
// base64 decode 应该回到我们 mock 用的 `ENC:sk-test`
const b64 = out.slice(mod.ENVELOPE_PREFIX.length);
const decoded = Buffer.from(b64, 'base64').toString('utf-8');
expect(decoded).toBe('ENC:sk-test');
});
it('encrypt → decrypt 往返:明文完整恢复', () => {
const original = 'sk-中文-emoji-🔑-whitespace \t';
const enc = mod.encrypt(original);
expect(enc.startsWith(mod.ENVELOPE_PREFIX)).toBe(true);
expect(mod.decrypt(enc)).toBe(original);
});
it('encrypt 已经是包络的字符串 → 原样返回(幂等,防止套娃)', () => {
const once = mod.encrypt('sk-x');
const twice = mod.encrypt(once);
expect(twice).toBe(once);
// 验证 mock encryptString 只被调一次
expect(encryptCalls).toEqual(['sk-x']);
});
it('decrypt(\'\') → \'\'', () => {
expect(mod.decrypt('')).toBe('');
});
it('decrypt(null) / decrypt(undefined) → \'\'', () => {
expect(mod.decrypt(null)).toBe('');
expect(mod.decrypt(undefined)).toBe('');
});
it('decrypt 旧版明文(无前缀)→ 原样返回(懒迁移关键)', () => {
// 模拟升级前 config.json 里的 plaintext key
expect(mod.decrypt('sk-old-plaintext')).toBe('sk-old-plaintext');
});
it('decrypt 任意不含前缀的字符串 → 原样返回(防御性)', () => {
expect(mod.decrypt('xxx')).toBe('xxx');
expect(mod.decrypt('not-an-envelope-but-still-string')).toBe('not-an-envelope-but-still-string');
});
});
describe('secret-store:safeStorage 不可用降级', () => {
let warnSpy;
let mod;
beforeEach(async () => {
mod = await loadModule();
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
mod._reset();
warnSpy.mockRestore();
});
it('isEncryptionAvailable() === false → encrypt 返回明文 + warn 一次', () => {
mod._setSafeStorage({
isEncryptionAvailable: () => false,
encryptString: () => Buffer.from('SHOULD-NOT-BE-USED'),
decryptString: () => 'SHOULD-NOT-BE-USED',
});
expect(mod.encrypt('sk-fallback')).toBe('sk-fallback');
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toMatch(/safeStorage 不可用/);
});
it('多次 encrypt 不可用 → 只 warn 一次(不重复打扰用户)', () => {
mod._setSafeStorage({
isEncryptionAvailable: () => false,
encryptString: () => Buffer.from('x'),
decryptString: () => 'x',
});
mod.encrypt('a');
mod.encrypt('b');
mod.encrypt('c');
expect(warnSpy).toHaveBeenCalledTimes(1);
});
it('decrypt 遇到包络但 keyring 不可用 → 返回 \'\'(不抛)', () => {
mod._setSafeStorage({
isEncryptionAvailable: () => false,
encryptString: () => Buffer.from('x'),
decryptString: () => 'x',
});
// 模拟磁盘上有包络(base64 随便编)
const fakeEnvelope = mod.ENVELOPE_PREFIX + Buffer.from('whatever', 'utf-8').toString('base64');
expect(mod.decrypt(fakeEnvelope)).toBe('');
});
it('decrypt 遇到损坏包络(decryptString 抛错)→ 返回 \'\'(不抛)', () => {
mod._setSafeStorage({
isEncryptionAvailable: () => true,
encryptString: (s) => Buffer.from(`ENC:${s}`),
decryptString: () => {
throw new Error('bad ciphertext');
},
});
const fakeEnvelope = mod.ENVELOPE_PREFIX + Buffer.from('corrupt', 'utf-8').toString('base64');
expect(() => mod.decrypt(fakeEnvelope)).not.toThrow();
expect(mod.decrypt(fakeEnvelope)).toBe('');
});
it('isEncryptionAvailable 自身抛错 → isAvailable() 返回 false', () => {
mod._setSafeStorage({
isEncryptionAvailable: () => {
throw new Error('keyring daemon crashed');
},
encryptString: () => Buffer.from('x'),
decryptString: () => 'x',
});
expect(mod.isAvailable()).toBe(false);
});
});
describe('secret-store:safeStorage 完全缺失', () => {
let warnSpy;
let mod;
beforeEach(async () => {
mod = await loadModule();
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
mod._reset();
warnSpy.mockRestore();
});
it('注入 null safeStorage(模拟 require(\'electron\') 无 safeStorage)→ 降级明文', () => {
mod._setSafeStorage(null);
// encrypt 走降级路径
expect(mod.encrypt('sk-x')).toBe('sk-x');
expect(warnSpy).toHaveBeenCalledTimes(1);
});
it('注入 null safeStorage + decrypt 包络 → 返回 \'\'', () => {
mod._setSafeStorage(null);
const fakeEnvelope = mod.ENVELOPE_PREFIX + Buffer.from('x', 'utf-8').toString('base64');
expect(mod.decrypt(fakeEnvelope)).toBe('');
});
it('isAvailable 在无 safeStorage 时 → false', () => {
mod._setSafeStorage(null);
expect(mod.isAvailable()).toBe(false);
});
});
describe('secret-store:_reset 状态隔离', () => {
let mod;
beforeEach(async () => {
mod = await loadModule();
});
it('_reset 后 _setSafeStorage 重新注入 + warn 标志清零', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
try {
mod._setSafeStorage({
isEncryptionAvailable: () => false,
encryptString: () => Buffer.from('x'),
decryptString: () => 'x',
});
mod.encrypt('a');
mod.encrypt('b');
expect(warnSpy).toHaveBeenCalledTimes(1);
mod._reset();
// 重新注入可用 safeStorage
mod._setSafeStorage({
isEncryptionAvailable: () => true,
encryptString: (s) => Buffer.from(`E:${s}`),
decryptString: (b) => b.toString('utf-8').slice(2),
});
// warn 标志清零,再次不可用时重新 warn
mod._setSafeStorage({
isEncryptionAvailable: () => false,
encryptString: () => Buffer.from('x'),
decryptString: () => 'x',
});
mod.encrypt('c');
expect(warnSpy).toHaveBeenCalledTimes(2);
} finally {
warnSpy.mockRestore();
mod._reset();
}
});
});