// 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 ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "fmt" "io" ) // appSecret 是应用级固定密钥种子。 // 这个字符串不会被写入数据库,仅在运行时存于内存。 // 即便攻击者拿到了 settings.db,没有此二进制也无法解密。 const appSecret = "ai-expert-sidebar-v1-local-©2026" // deriveKey 将 appSecret 通过 SHA-256 压缩为固定的 32 字节切片, // 作为 AES-256 的原始密钥(AES-256 要求密钥恰好为 32 字节)。 // SHA-256 是单向函数,无法从输出反推 appSecret。 func deriveKey() []byte { sum := sha256.Sum256([]byte(appSecret)) return sum[:] // 将 [32]byte 数组转为 []byte 切片 } // 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 } block, err := aes.NewCipher(deriveKey()) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) 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 解密 EncryptAPIKey 产生的 base64 密文, // 返回原始 API Key 明文。 // // 空字符串输入返回空字符串(用户尚未配置 Key 的状态)。 // 若密文被篡改或密钥不匹配,GCM 认证会失败, // 返回 error 而非乱码明文。 func DecryptAPIKey(ciphertext64 string) (string, error) { if ciphertext64 == "" { return "", nil } data, err := base64.StdEncoding.DecodeString(ciphertext64) if err != nil { return "", fmt.Errorf("base64 decode: %w", err) } block, err := aes.NewCipher(deriveKey()) if err != nil { return "", err } gcm, err := cipher.NewGCM(block) if err != nil { return "", err } ns := gcm.NonceSize() if len(data) < ns { return "", fmt.Errorf("密文过短,可能已损坏") } // 从头部取出 nonce,剩余部分为真正的密文+认证标签 plain, err := gcm.Open(nil, data[:ns], data[ns:], nil) return string(plain), err }