2026-04-20-10-14-09 - 安全加固:XOR→AES-GCM、API Key环境变量化、授权码SHA-256哈希、消除硬编码密钥

This commit is contained in:
2026-04-20 10:27:17 +08:00
parent f0eda59c63
commit 31a2cdc77e
9 changed files with 608 additions and 43 deletions

View File

@@ -0,0 +1,177 @@
# 实现方案
> 时间戳2026-04-20-10-14-09
> 对应需求分析:`需求分析-2026-04-20-10-14-09.md`
---
## 1. 方案概述
在不改变 `storage.get/set` 同步调用签名的前提下,将 `systemSettings` 的底层加密从 XOR 迁移至 Web Crypto API AES-GCM密钥与 API Key、授权哈希全部移入构建阶段环境变量。应用启动时异步初始化存储模块预解密 `systemSettings` 到内存缓存,后续同步读写均走缓存。旧 XOR 密文自动识别并迁移。UserManage.tsx 中的明文授权常量替换为 SHA-256 环境变量哈希比对,同时以最小代价替换原生 alert/confirm 为内联状态提示。
---
## 2. 详细改动清单
### 2.1 文件src/utils/storage.ts
| 行号范围 | 改动类型 | 说明 |
|----------|----------|------|
| 1-6 | 删除 | 移除 `API_KEY_CODES` 数组与 `getDefaultApiKey()` 的旧实现 |
| 8 | 修改 | `CRYPTO_KEY` 改为从 `process.env.STORAGE_KEY` 读取,并更名为 `LEGACY_CRYPTO_KEY` |
| 10-25 | 保留 | 旧 `xorEncrypt` / `xorDecrypt` 保留用于向后兼容读取 |
| 新增 | 新增 | 引入 Web Crypto API`importCryptoKey``aesEncrypt``aesDecrypt` |
| 新增 | 新增 | `AES_GCM_PREFIX = 'ag:'` 用于识别新格式密文 |
| 新增 | 新增 | `systemSettingsCache``initPromise` 等模块级状态 |
| 新增 | 新增 | 异步 `init()` 函数:导入密钥、预加载 systemSettings、自动迁移旧数据 |
| 27-43 | 修改 | `get()``systemSettings` 改为从内存缓存返回 |
| 45-55 | 修改 | `set()``systemSettings` 改为更新缓存并触发异步 `persistSystemSettings()` |
| 新增 | 新增 | `sha256(text)` 工具函数Web Crypto API供 UserManage.tsx 使用 |
**关键代码逻辑**
```typescript
// AES-GCM 密文格式: ag:<base64(iv)>:<base64(ciphertext)>
const AES_GCM_PREFIX = 'ag:';
async function init() {
const keyData = process.env.STORAGE_KEY || '';
if (!keyData) throw new Error('STORAGE_KEY not configured in build env');
cryptoKey = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(keyData.padEnd(32, '0').slice(0, 32)),
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
);
const raw = localStorage.getItem('systemSettings');
if (raw) {
if (raw.startsWith(AES_GCM_PREFIX)) {
systemSettingsCache = await aesDecrypt(raw);
} else {
try { systemSettingsCache = JSON.parse(raw); }
catch { systemSettingsCache = JSON.parse(xorDecrypt(raw, LEGACY_KEY)); }
await persistSystemSettings(); // 静默迁移
}
}
}
```
### 2.2 文件src/main.tsx
| 行号范围 | 改动类型 | 说明 |
|----------|----------|------|
| 4-10 | 修改 | 包装为异步 `bootstrap()`,先 `await storage.init()` 再 render App |
```typescript
import { storage } from './utils/storage';
async function bootstrap() {
await storage.init();
createRoot(document.getElementById('root')!).render(
<StrictMode><App /></StrictMode>
);
}
bootstrap();
```
### 2.3 文件vite.config.ts
| 行号范围 | 改动类型 | 说明 |
|----------|----------|------|
| 10-12 | 修改 | 在 `define` 中新增 4 个环境变量注入 |
```typescript
define: {
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.STORAGE_KEY': JSON.stringify(env.STORAGE_KEY),
'process.env.DEFAULT_API_KEY': JSON.stringify(env.DEFAULT_API_KEY),
'process.env.ADMIN_AUTH_HASH': JSON.stringify(env.ADMIN_AUTH_HASH),
'process.env.ADMIN_AUTH_SALT': JSON.stringify(env.ADMIN_AUTH_SALT),
},
```
### 2.4 文件:.env.example
| 行号范围 | 改动类型 | 说明 |
|----------|----------|------|
| 新增 | 新增 | 追加 4 个必需环境变量模板 |
```bash
# 本地存储 AES-GCM 加密密钥(建议 32 位随机字符串)
STORAGE_KEY="YourRandom32CharKeyHere!!!"
# 默认 AI API Key替代原 API_KEY_CODES 硬编码)
DEFAULT_API_KEY="your-api-key-here"
# 禁用管理员操作的授权码 SHA-256 哈希值hex 小写)
ADMIN_AUTH_HASH="your-sha256-hash-here"
# 授权码哈希盐值
ADMIN_AUTH_SALT="your-random-salt-here"
```
### 2.5 文件src/pages/UserManage.tsx
| 行号范围 | 改动类型 | 说明 |
|----------|----------|------|
| 8 | 删除 | 移除 `ADMIN_DISABLE_AUTH_KEY = 'DISABLE_ADMIN_2024'` |
| 新增 | 新增 | 从环境变量读取 `ADMIN_AUTH_HASH``ADMIN_AUTH_SALT` |
| 29 | 修改 | `authKey` 保持,但比对逻辑改为 SHA-256 |
| 新增 | 新增 | 增加 `errorMessage``confirmDialog` 状态替换 alert/confirm |
| 103-116 | 修改 | `handleDelete` 改为触发确认对话框状态,移除 `window.confirm` |
| 118-154 | 修改 | `handleEdit` 中 alert 改为设置 `errorMessage` |
| 185-324 | 修改 | `handleSubmit` 改为 async所有 alert 改为 `errorMessage`,授权比对改为 `await sha256(authKey + salt)` |
| 新增 JSX | 新增 | 表单顶部增加错误提示条;新增轻量确认对话框覆盖层 |
---
## 3. 关键设计决策
| 决策点 | 选择方案 | 备选方案 | 不选备选的原因 |
|--------|----------|----------|----------------|
| systemSettings 异步加密的同步接口兼容 | 模块初始化时预解密到内存缓存get/set 保持同步 | 将 storage.get/set 全局改为 async | 调用点超过 50 处,改动面过大,风险不可控 |
| AES-GCM 密钥长度处理 | 将环境变量字符串 padEnd 到 32 字节后截断 | 要求用户必须提供 32 字节 | 提升用户体验,减少配置门槛 |
| 旧数据迁移时机 | init() 中自动检测并静默迁移 | 提供手动迁移脚本 | 纯前端无脚本执行环境,自动迁移对用户透明 |
| SHA-256 实现选择 | Web Crypto API已在 storage.ts 提供工具函数) | crypto-js 同步计算 | 统一使用 Web Crypto API减少额外依赖引用 |
| alert/confirm 替换策略 | 内联状态提示条 + 轻量确认状态 | 抽取全局 Modal 组件 | 项目中无现成 Modal本次以最小代价消除明文暴露和原生弹窗 |
---
## 4. 与现有规范的兼容性检查
- [ ] contentEditable 编辑器规范5.1 — 本次不涉及编辑器
- [x] 自动保存与草稿机制5.2 — storage 接口签名不变ReportEditor 自动保存逻辑无影响
- [ ] 图片与视频处理5.3 — 本次不涉及
- [ ] 智能字段与动态表单5.4 — 本次不涉及
- [ ] AI 功能开发规范5.5`getDefaultApiKey()` 返回值不变,调用方无感知
- [x] 数据迁移与类型安全5.6 — 旧 XOR 密文自动迁移,使用 `(obj as any).oldField` 安全访问思路已体现在旧格式兼容分支
- [x] UI/UX 通用规范5.7 — UserManage.tsx 中原生 alert/confirm 全部替换为自定义状态提示
---
## 5. 回滚策略
若构建失败或运行异常:
```bash
git reset --hard HEAD~1
```
或手动还原:
1. 恢复 `src/utils/storage.ts` 中的 XOR 逻辑与 `API_KEY_CODES`
2. 恢复 `src/pages/UserManage.tsx` 中的 `ADMIN_DISABLE_AUTH_KEY`
3. 恢复 `src/main.tsx` 为同步 render
4. 还原 `vite.config.ts` 的 define 配置
---
## 6. 预估工作量
| 阶段 | 预估时间 |
|------|----------|
| 代码修改 | 25 min |
| 测试验证 | 15 min |
| 文档更新 | 10 min |
---
**状态**:✅ 用户已豁免人工审核确认

View File

@@ -0,0 +1,96 @@
# 测试方案
> 时间戳2026-04-20-10-14-09
> 对应需求分析:`需求分析-2026-04-20-10-14-09.md`
> 对应实现方案:`实现方案-2026-04-20-10-14-09.md`
---
## 1. 测试环境
- 浏览器Chrome / Edge本地验证
- Node 版本v24.14.1
- 是否使用默认测试数据:是(通过 Login.tsx 种子数据)
- 环境变量配置(`.env.local` 或系统环境变量):
```
STORAGE_KEY="TestStorageKeyForDev2024!!!"
DEFAULT_API_KEY="sk-test-api-key-123456789"
ADMIN_AUTH_HASH="5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
ADMIN_AUTH_SALT="static-salt-001"
```
> 注:上述 HASH 是 `"password" + "static-salt-001"` 的 SHA-256测试时输入 `password` 应可通过授权。
---
## 2. 功能测试用例
| 用例ID | 测试场景 | 操作步骤 | 预期结果 | 实际结果 | 是否通过 |
|--------|----------|----------|----------|----------|----------|
| TC-01 | AES-GCM 新数据加密写入 | 1. 登录后进入系统设置<br>2. 修改任意配置并保存 | localStorage 中 `systemSettings` 值以 `ag:` 开头 | | □ |
| TC-02 | AES-GCM 新数据解密读取 | 1. 刷新页面<br>2. 进入报告编辑器<br>3. 查看 AI 配置或抽帧配置 | 正确读取到之前保存的系统设置,无解密报错 | | □ |
| TC-03 | 旧 XOR 数据向后兼容 | 1. 先回退代码,用旧版写入一条 systemSettings<br>2. 切换回新代码刷新页面 | 旧 XOR 密文被正确识别、解密并静默迁移为 AES-GCM 格式 | | □ |
| TC-04 | 默认 API Key 来源 | 1. 打开浏览器控制台<br>2. 执行 `getDefaultApiKey()` | 返回值为 `.env` 中配置的 `DEFAULT_API_KEY`,非硬编码字符码数组还原值 | | □ |
| TC-05 | 管理员禁用授权正确 | 1. 超级管理员编辑任一管理员<br>2. 状态改为"禁用"<br>3. 授权码输入 `password` | 保存成功 | | □ |
| TC-06 | 管理员禁用授权错误 | 1. 同 TC-05 场景<br>2. 授权码输入 `wrong` | 表单顶部显示错误提示"授权密钥不正确",保存被阻止 | | □ |
| TC-07 | alert/confirm 已移除 | 1. 尝试删除用户<br>2. 触发各类表单校验错误 | 不再出现浏览器原生 alert/confirm全部以内联提示或自定义确认层呈现 | | □ |
---
## 3. 边界测试
| 场景 | 测试内容 | 预期表现 |
|------|----------|----------|
| 空数据 | 首次访问localStorage 无 systemSettings | `storage.init()` 正常完成,缓存为 null后续 get 返回 fallback |
| 大数据量 | systemSettings 包含大量模板和配置 | AES-GCM 加密后 Base64 长度可控localStorage 5MB 限额不溢出 |
| 异常输入 | 用户输入空授权码提交 | 校验不通过,提示授权密钥不能为空(或错误) |
| 环境变量缺失 | 构建时未设置 `STORAGE_KEY` | 应用启动时 `storage.init()` 抛出可读错误,控制台可见 |
---
## 4. 回归测试范围
(验证已有功能未被破坏)
- [ ] 登录流程admin / manager / doctor
- [ ] 报告编辑器自动保存与草稿恢复
- [ ] 模板管理增删改查
- [ ] 系统设置页面配置保存与读取
- [ ] AI 功能调用(需配置有效 API Key 验证)
- [ ] 用户管理增删改查(非禁用场景)
---
## 5. 类型检查
```bash
npm run lint
```
- 预期结果:`tsc --noEmit` 无错误,无 TS2345 / TS2554 等类型不匹配
---
## 6. 构建验证
```bash
npm run build
```
- 预期结果:构建成功,无报错,产物输出至 `dist/`
- 额外检查:构建产物中搜索 `"MedicalReportSys2024"`、`"DISABLE_ADMIN_2024"`、`API_KEY_CODES` 等敏感字符串,应无命中
---
## 7. 测试结论
| 项目 | 结果 |
|------|------|
| 功能测试 | □ 通过 / □ 部分通过 / □ 未通过 |
| 类型检查 | □ 通过 / □ 未通过 |
| 构建验证 | □ 通过 / □ 未通过 |
| 安全扫描(构建产物无硬编码密钥) | □ 通过 / □ 未通过 |
| 整体结论 | □ 可交付 / □ 需修复后复测 |
---
**状态**:✅ 用户已豁免人工审核确认

View File

@@ -28,5 +28,45 @@
## 历史记录
(以下为实际踩坑记录,按时间倒序排列)
### [2026-04-20-10-14-09] Web Crypto API 异步性与 storage 同步接口冲突
### A. 具体问题
需求要求将 `systemSettings` 的加密从 XOR 升级为 Web Crypto API AES-GCM同时维持 `storage.get/set` 的同步调用签名。但 `crypto.subtle` 的所有加解密操作均为异步 Promise若直接将 `storage.get/set` 改为 async将导致 ReportEditor.tsx、Login.tsx 等数十处调用点需要同步改为 await改动面极大且风险不可控。
### B. 产生问题原因
Web Crypto API 是浏览器原生标准,设计为全异步 API无法像 crypto-js 那样提供同步调用。而项目早期设计时 `storage` 模块全部为同步接口,未预留异步扩展点。
### C. 解决问题方案
采用"启动时预解密 + 内存缓存"的折中方案:
1.`storage.ts` 中新增异步 `init()` 函数,在应用启动(`main.tsx``await storage.init()`)时预解密 `systemSettings` 到模块级内存缓存 `systemSettingsCache`
2. `storage.get('systemSettings')` 改为从缓存同步返回,`storage.set('systemSettings')` 改为更新缓存并触发异步持久化(不阻塞调用方)。
3. 其他 key 的读写保持原样,完全不受影响。
4. 该方案无需修改任何业务组件中 `storage.get('systemSettings')` 的调用方式。
### D. 后续如何避免问题
- 若未来再次遇到"必须引入异步存储操作"的需求,优先评估能否用"启动预加载 + 内存缓存"模式规避接口签名变更。
- 若必须改异步接口,应将影响范围限制在单一 key如新增 `getAsync/setAsync`),而非全局改动 `storage.get/set`
- 在需求分析阶段增加"接口兼容性影响评估"子项,提前识别同步/异步冲突。
---
### [2026-04-20-10-14-09] 旧数据兼容密钥与"消除硬编码"验收标准冲突
### A. 具体问题
安全加固的验收标准要求"构建产物中不再出现任何硬编码密钥字符串"。但实现 AES-GCM 迁移时,必须保留旧 XOR 密钥 `MedicalReportSys2024` 才能解密历史 localStorage 中的 `systemSettings` 数据。首次修改后构建产物扫描发现该字符串仍然存在。
### B. 产生问题原因
数据迁移需求(兼容旧 XOR 密文)与安全需求(消除硬编码密钥)表面上互斥。根源在于旧密钥是以明文常量形式写死在源码中的,构建产物必然包含该字符串。
### C. 解决问题方案
将旧 XOR 密钥同样移入构建阶段环境变量 `LEGACY_STORAGE_KEY`,通过 `vite.config.ts``define` 注入:
- 构建产物中不再包含固定的 `"MedicalReportSys2024"`
- 若部署方需要兼容旧数据,在构建时配置 `LEGACY_STORAGE_KEY="MedicalReportSys2024"` 即可正常迁移。
- 若部署方不需要兼容(全新部署),留空即可,旧数据将以 JSON.parse 失败处理,回退到 fallback。
### D. 后续如何避免问题
- 任何涉及历史数据迁移的安全重构,必须在方案设计阶段同步考虑"旧密钥/旧算法常量是否需要保留"。
- 检查清单:若验收标准包含"构建产物无硬编码敏感字符串",则所有历史兼容所需的常量也必须环境变量化。
- `.env.example` 中应显式标注 `LEGACY_STORAGE_KEY` 的用途("Former XOR encryption key"),避免运维人员误删。
---

View File

@@ -0,0 +1,72 @@
# 需求分析
> 时间戳2026-04-20-10-14-09
---
## 1. 原始需求描述
用户提出三个核心安全问题,要求对纯前端架构下的敏感数据保护机制进行加固:
1. **storage.ts 中 XOR 加密硬编码静态密钥** → 已知明文攻击风险
2. **API_KEY_CODES 字符码数组混淆** → API 密钥客户端暴露与盗刷风险
3. **ADMIN_DISABLE_AUTH_KEY 明文硬编码** → 垂直越权漏洞
用户给出的妥协方案方向(鉴于纯前端限制):
- 问题1改用 Web Crypto API (AES-GCM) + 环境变量注入密钥
- 问题2剥离前端密钥还原逻辑至少移入构建阶段环境变量
- 问题3用 SHA-256 哈希替代明文常量比对
---
## 2. 需求拆解
| 序号 | 子需求 | 涉及文件 | 优先级 |
|------|--------|----------|--------|
| 1 | 弃用 XOR 加密,改用 Web Crypto API AES-GCM | `src/utils/storage.ts` | P0 |
| 2 | 密钥从硬编码改为构建时环境变量注入 | `vite.config.ts`, `.env.example` | P0 |
| 3 | 兼容旧 XOR 加密数据的自动迁移读取 | `src/utils/storage.ts` | P0 |
| 4 | 删除 API_KEY_CODES 硬编码字符码数组 | `src/utils/storage.ts` | P0 |
| 5 | 默认 API Key 改为从环境变量读取 | `src/utils/storage.ts`, `vite.config.ts` | P0 |
| 6 | ADMIN_DISABLE_AUTH_KEY 改为 SHA-256 哈希比对 | `src/pages/UserManage.tsx` | P0 |
| 7 | 将源码中明文授权码替换为环境变量哈希值 | `.env.example`, `vite.config.ts` | P0 |
| 8 | 修复 UserManage.tsx 中原生 alert/confirm 使用 | `src/pages/UserManage.tsx` | P1 |
---
## 3. 影响范围评估
### 3.1 需要修改的文件
- `src/utils/storage.ts` — 加密机制重构、API Key 来源变更
- `src/pages/UserManage.tsx` — 授权码哈希比对、alert/confirm 替换
- `vite.config.ts` — 新增环境变量注入
- `.env.example` — 新增必需环境变量模板
### 3.2 潜在风险点
- **数据迁移兼容性**:旧版本用户 localStorage 中的 systemSettings 仍用 XOR 加密,必须支持双轨读取(先尝试 AES-GCM 解密,失败再回退 XOR
- **Web Crypto API 异步性**storage.get 当前为同步接口AES-GCM 解密为异步操作,需调整接口或设计异步包装
- **环境变量缺失导致构建失败**:若用户未配置新环境变量,构建产物中密钥将为空字符串
- **SHA-256 哈希盐值**:若仅做简单 SHA-256彩虹表仍可攻击需引入盐值
### 3.3 是否涉及数据迁移
- [x] 是。旧 `systemSettings` 的 XOR 加密数据需兼容读取,新写入全面使用 AES-GCM。
---
## 4. 验收标准
- [ ] `npm run lint` 无类型错误
- [ ] `npm run build` 构建成功
- [ ] storage.ts 中不再出现任何硬编码密钥字符串
- [ ] UserManage.tsx 中不再出现明文授权常量
- [ ] 旧版本 XOR 加密的 systemSettings 仍能被正确读取(向后兼容)
- [ ] 新写入的 systemSettings 使用 AES-GCM 加密
- [ ] 默认 API Key 从环境变量注入,源码中无字符码数组
- [ ] 禁用管理员的授权校验使用 SHA-256 + 盐值比对
- [ ] UserManage.tsx 中原生 alert/confirm 全部替换为自定义 UI 或至少移除硬编码明文提示
---
## 5. 关联历史需求
无直接关联历史需求。但需遵守 AGENTS.md 第 5.6 节数据迁移规范、第 5.7 节禁用原生 alert/confirm 规范。