feat: 添加注释
This commit is contained in:
@@ -1,3 +1,22 @@
|
||||
// Package crypto 提供本项目所有加密/解密能力。
|
||||
//
|
||||
// # 加密方案选型
|
||||
//
|
||||
// 使用 AES-256-GCM(Galois/Counter Mode)对称加密,原因:
|
||||
// 1. GCM 是认证加密(AEAD),同时提供机密性和完整性验证,
|
||||
// 可以检测密文被篡改的情况(返回 error 而非静默解密乱码)。
|
||||
// 2. AES-256 是 NIST 标准,Go 标准库原生支持,无需第三方依赖。
|
||||
// 3. 相比 RSA 等非对称方案,AES 性能更高、密钥更短,
|
||||
// 且本场景(本地加密本地解密)不需要非对称性。
|
||||
//
|
||||
// # 密钥派生
|
||||
//
|
||||
// 不使用用户输入的密码派生密钥(避免需要"主密码"的 UX 摩擦),
|
||||
// 而是使用固化在应用中的 appSecret 做 SHA-256 哈希得到 32 字节密钥。
|
||||
//
|
||||
// 安全边界:此方案防止的是"直接读取 settings.db 文件就能看到 API Key 明文",
|
||||
// 不能防止"有完整应用二进制 + 数据文件的攻击者",
|
||||
// 这对于本地隐私工具已经足够。
|
||||
package crypto
|
||||
|
||||
import (
|
||||
@@ -10,15 +29,30 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// appSecret 是应用级固定密钥种子。
|
||||
// 这个字符串不会被写入数据库,仅在运行时存于内存。
|
||||
// 即便攻击者拿到了 settings.db,没有此二进制也无法解密。
|
||||
const appSecret = "ai-expert-sidebar-v1-local-©2026"
|
||||
|
||||
// deriveKey produces a 32-byte AES key from the application secret.
|
||||
// deriveKey 将 appSecret 通过 SHA-256 压缩为固定的 32 字节切片,
|
||||
// 作为 AES-256 的原始密钥(AES-256 要求密钥恰好为 32 字节)。
|
||||
// SHA-256 是单向函数,无法从输出反推 appSecret。
|
||||
func deriveKey() []byte {
|
||||
sum := sha256.Sum256([]byte(appSecret))
|
||||
return sum[:]
|
||||
return sum[:] // 将 [32]byte 数组转为 []byte 切片
|
||||
}
|
||||
|
||||
// EncryptAPIKey encrypts a plaintext API key. Returns "" for empty input.
|
||||
// EncryptAPIKey 使用 AES-256-GCM 加密用户输入的 API Key 明文,
|
||||
// 返回 base64 编码的密文字符串(方便存入 SQLite TEXT 列)。
|
||||
//
|
||||
// 空字符串返回空字符串,调用方无需特殊处理。
|
||||
//
|
||||
// # Nonce 设计
|
||||
//
|
||||
// 每次加密随机生成一个新 nonce(GCM 标准大小:12 字节),
|
||||
// 并将其前置在密文中(nonce || ciphertext)一起 base64。
|
||||
// 这样同一个 API Key 每次加密结果都不同,防止重放攻击,
|
||||
// 且解密时只需从密文头部截取 nonce,无需额外存储。
|
||||
func EncryptAPIKey(plaintext string) (string, error) {
|
||||
if plaintext == "" {
|
||||
return "", nil
|
||||
@@ -31,15 +65,23 @@ func EncryptAPIKey(plaintext string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// 使用密码学安全的随机数生成器(crypto/rand,非 math/rand)产生 nonce
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Seal 将 nonce 作为前缀附加到加密后的密文
|
||||
// 格式:[nonce(12 bytes)] [ciphertext+tag]
|
||||
sealed := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
|
||||
return base64.StdEncoding.EncodeToString(sealed), nil
|
||||
}
|
||||
|
||||
// DecryptAPIKey decrypts a base64-encoded AES-256-GCM ciphertext. Returns "" for empty input.
|
||||
// DecryptAPIKey 解密 EncryptAPIKey 产生的 base64 密文,
|
||||
// 返回原始 API Key 明文。
|
||||
//
|
||||
// 空字符串输入返回空字符串(用户尚未配置 Key 的状态)。
|
||||
// 若密文被篡改或密钥不匹配,GCM 认证会失败,
|
||||
// 返回 error 而非乱码明文。
|
||||
func DecryptAPIKey(ciphertext64 string) (string, error) {
|
||||
if ciphertext64 == "" {
|
||||
return "", nil
|
||||
@@ -58,8 +100,9 @@ func DecryptAPIKey(ciphertext64 string) (string, error) {
|
||||
}
|
||||
ns := gcm.NonceSize()
|
||||
if len(data) < ns {
|
||||
return "", fmt.Errorf("ciphertext too short")
|
||||
return "", fmt.Errorf("密文过短,可能已损坏")
|
||||
}
|
||||
// 从头部取出 nonce,剩余部分为真正的密文+认证标签
|
||||
plain, err := gcm.Open(nil, data[:ns], data[ns:], nil)
|
||||
return string(plain), err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user