-
Notifications
You must be signed in to change notification settings - Fork 39
修复前端明文传输了Key,会被攻击 #224
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
修复前端明文传输了Key,会被攻击 #224
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // settings 变化即写回(必要时加密) | ||
| useEffect(() => { | ||
| (async () => { | ||
| await writeStoredSettings(settings); | ||
| })(); | ||
| }, [settings]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid overwriting encrypted API keys on load without passphrase
The hook writes whatever is in state back to localStorage on every change, even when the stored keys cannot be decrypted yet. On a fresh browser session the passphrase lives only in sessionStorage, so readStoredSettings() returns empty strings when decryptIfNeeded fails. The subsequent [settings] effect immediately persists those empty values (and setPassphrase does the same before re‑reading), destroying the original ciphertext. As a result, previously encrypted API keys are irretrievably lost as soon as the provider mounts or the user supplies the passphrase after a reload. The write should be deferred until decryption succeeds or the user intentionally replaces the value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR adds encryption functionality to the assistant settings hook to securely store API keys. The implementation uses AES-GCM encryption with PBKDF2 key derivation, storing the passphrase in sessionStorage and encrypted API keys in localStorage.
Key changes:
- Implements client-side encryption/decryption for API keys using Web Crypto API (AES-256-GCM with PBKDF2)
- Adds passphrase management stored in sessionStorage
- Refactors storage logic to handle both encrypted and plaintext data for backward compatibility
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ); | ||
| return ""; | ||
| } | ||
| const [, rest] = token.split(ENC_PREFIX); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The split operation on line 134 will create an array with only 2 elements when ENC_PREFIX is found once. However, if the token contains multiple instances of ENC_PREFIX, this logic will fail. Consider using token.slice(ENC_PREFIX.length) instead to safely extract everything after the prefix.
| const [, rest] = token.split(ENC_PREFIX); | |
| const rest = token.slice(ENC_PREFIX.length); |
| useEffect(() => { | ||
| (async () => { | ||
| await writeStoredSettings(settings); | ||
| })(); | ||
| }, [settings]); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This effect will trigger on initial mount, writing default empty settings to storage before the initial readStoredSettings completes in the first effect (lines 247-256). This creates a race condition that could overwrite existing stored settings. Add a flag to skip the first write, or use a ref to track if initial load is complete.
| const setPassphrase = useCallback( | ||
| (pass: string | null) => { | ||
| writePassphrase(pass && pass.length ? pass : null); | ||
| setHasPassphrase(!!(pass && pass.length)); | ||
| // 口令变化后立即重写一份(把已有明文转密文或反之) | ||
| (async () => { | ||
| await writeStoredSettings(settings); | ||
| const s = await readStoredSettings(); | ||
| setSettings(s); | ||
| })(); | ||
| }, | ||
| [settings], | ||
| ); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The async operation in setPassphrase (lines 289-293) can race with the effect on line 271. If a user changes the passphrase while settings is being updated, multiple concurrent writes to localStorage could occur with different passphrases, potentially corrupting data. Consider debouncing writes or using a write queue to serialize operations.
| const s = await readStoredSettings(); | ||
| setSettings(s); |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The async event handler onStorage has no error handling. If readStoredSettings throws an error (e.g., corrupted data in localStorage), the handler fails silently. Add try-catch with appropriate error logging or user notification.
| const s = await readStoredSettings(); | |
| setSettings(s); | |
| try { | |
| const s = await readStoredSettings(); | |
| setSettings(s); | |
| } catch (error) { | |
| console.error("Failed to read stored settings from storage event:", error); | |
| } |
| plain: string, | ||
| passphrase: string | null, | ||
| ): Promise<string> { | ||
| if (!passphrase || typeof window === "undefined" || !window.crypto?.subtle) { |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When encryption fails due to missing passphrase or unsupported environment, the function returns plaintext API keys. This could lead to unintentional storage of sensitive data in plaintext. Consider throwing an error or requiring explicit opt-in for plaintext storage, especially in production environments.
| /* ---------------- Constants ---------------- */ | ||
|
|
||
| const SETTINGS_KEY = "assistant-settings-storage"; | ||
| const PASSPHRASE_KEY = "assistant-settings-passphrase"; // 仅 sessionStorage |
Copilot
AI
Nov 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storing the passphrase in sessionStorage means it persists for the entire browser session and is accessible to any JavaScript code on the same origin. Consider using in-memory storage only (a module-level variable) and requiring re-entry on page refresh for better security, especially since the passphrase is the key to decrypt sensitive API keys.
| const PASSPHRASE_KEY = "assistant-settings-passphrase"; // 仅 sessionStorage | |
| // Passphrase is now only stored in memory, not in sessionStorage, for security. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
稍等合并,我补充一点改动 |
Crokily
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
没有入口调用setPassphrase,口令一直为null,因此加密功能没有生效。
有首轮渲染错误覆盖已存储内容,解密失败似乎也会覆盖,等问题,代码可以进一步解耦。
辛苦啦,目前需要的调整,我改好后会直接在这个分支上commit。
原因
localStorage,风险较高(导出本地存储、磁盘快照、共享计算机等)。deriveKey/encrypt/decrypt处)。新增加密格式:
enc:v1:<base64(salt)>:<base64(iv)>:<base64(cipher)>PBKDF2:iterations=310_000,hash=SHA-256,salt=16BAES-GCM:key=256bit,iv=12B新增 Context API:
setPassphrase(passphrase: string | null):设置/清空口令(保存在sessionStorage,浏览器关闭即清空)hasPassphrase: boolean:是否已设置口令存取逻辑:
localStorage;否则以明文写入(兼容旧行为)enc:v1:前缀 → 尝试用口令解密;无口令或失败 → 返回空字符串TS 修复:
salt/iv的底层buffer为ArrayBuffer(而非ArrayBufferLike),统一标注为BufferSourceinstanceof检查,改用通用视图viewOf处理ArrayBuffer | ArrayBufferView...展开大数组,提供安全的 Base64 编解码工具小提示
sessionStorage,关闭浏览器后清空,减少长期驻留风险。window.crypto;所有加解密仅在客户端执行。