mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
89085173f9
* feat: remove admin mode * feat: bugfix * feat(crypto): 添加RSA-OAEP + AES-GCM混合加密服务 - 实现CryptoService加密服务,支持RSA-OAEP-2048 + AES-256-GCM混合加密 - 集成数据库层加密,自动加密存储敏感字段(API密钥、私钥等) - 支持环境变量DATA_ENCRYPTION_KEY配置数据加密密钥 - 适配SQLite数据库加密存储(从PostgreSQL移植) - 保持Hyperliquid代理钱包处理兼容性 - 更新.gitignore以正确处理crypto模块代码 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(scripts): 添加加密环境一键设置脚本 - setup_encryption.sh: 一键生成RSA密钥对+数据加密密钥+JWT密钥 - generate_rsa_keys.sh: 专业的RSA-2048密钥对生成工具 - generate_data_key.sh: 生成AES-256数据加密密钥和JWT认证密钥 - ENCRYPTION_README.md: 详细的加密系统说明文档 - 支持自动检测现有密钥并只生成缺失的密钥 - 完善的权限管理和安全验证 - 兼容macOS和Linux的跨平台支持 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(api): 添加加密API端点和Gin框架集成 - 新增CryptoHandler处理加密相关API请求 - 提供/api/crypto/public-key端点获取RSA公钥 - 提供/api/crypto/decrypt端点解密敏感数据 - 适配Gin框架的HTTP处理器格式 - 集成CryptoService到API服务器 - 支持前端加密数据传输和解密 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(web): 添加前端加密服务和两阶段密钥输入组件 - CryptoService: Web Crypto API集成,支持RSA-OAEP加密 - TwoStageKeyModal: 安全的两阶段私钥输入组件,支持剪贴板混淆 - 完善国际化翻译支持加密相关UI文本 - 修复TypeScript类型错误和编译问题 - 支持前端敏感数据加密传输到后端 - 增强用户隐私保护和数据安全 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(auth): 增强JWT认证安全性 - 优先使用环境变量JWT_SECRET而不是数据库配置 - 支持通过.env文件安全配置JWT认证密钥 - 保留数据库配置作为回退机制 - 改进JWT密钥来源日志显示 - 增强系统启动时的安全配置检查 - 支持运行时动态JWT密钥切换 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(docker): 集成加密环境变量到Docker部署 - 添加DATA_ENCRYPTION_KEY环境变量传递到容器 - 添加JWT_SECRET环境变量支持 - 挂载secrets目录使容器可访问RSA密钥文件 - 确保容器内加密服务正常工作 - 解决容器启动失败和加密初始化问题 - 完善Docker Compose加密环境配置 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(start): 集成自动加密环境检测和设置 - 增强check_encryption()函数检测JWT_SECRET和DATA_ENCRYPTION_KEY - 自动运行setup_encryption.sh当检测到缺失密钥时 - 改进加密状态显示,包含RSA+AES+JWT全套加密信息 - 优化用户体验,提供清晰的加密配置反馈 - 支持一键设置完整加密环境 - 确保容器启动前加密环境就绪 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat: format fix * fix(security): 修复前端模型和交易所配置敏感数据明文传输 - 在handleSaveModelConfig中对API密钥进行RSA-OAEP加密 - 在handleSaveExchangeConfig中对API密钥、Secret密钥和Aster私钥进行加密 - 只有非空敏感数据才进行加密处理 - 添加加密失败错误处理和用户友好提示 - 增加encryptionFailed翻译键的中英文支持 - 使用用户ID和会话ID作为加密上下文增强安全性 这修复了之前敏感数据在网络传输中以明文形式发送的安全漏洞。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix(crypto): 修复后端加密服务集成和缺失的加密端点 - 添加Server结构体缺少的cryptoService字段 - 实现handleUpdateModelConfigsEncrypted处理器用于模型配置加密传输 - 修复handleUpdateExchangeConfigsEncrypted中的函数调用 - 在前端API中添加updateModelConfigsEncrypted方法 - 统一RSA密钥路径从secrets/rsa_key改为keys/rsa_private.key - 确保前端可以使用加密端点安全传输敏感数据 - 兼容原有加密通信模式和二段输入私钥功能 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> --------- Co-authored-by: icy <icyoung520@gmail.com> Co-authored-by: tinkle-community <tinklefund@gmail.com>
394 lines
9.2 KiB
Go
394 lines
9.2 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
storagePrefix = "ENC:v1:"
|
|
storageDelimiter = ":"
|
|
dataKeyEnvName = "DATA_ENCRYPTION_KEY"
|
|
)
|
|
|
|
type EncryptedPayload struct {
|
|
WrappedKey string `json:"wrappedKey"`
|
|
IV string `json:"iv"`
|
|
Ciphertext string `json:"ciphertext"`
|
|
AAD string `json:"aad,omitempty"`
|
|
KID string `json:"kid,omitempty"`
|
|
TS int64 `json:"ts,omitempty"`
|
|
}
|
|
|
|
type AADData struct {
|
|
UserID string `json:"userId"`
|
|
SessionID string `json:"sessionId"`
|
|
TS int64 `json:"ts"`
|
|
Purpose string `json:"purpose"`
|
|
}
|
|
|
|
type CryptoService struct {
|
|
privateKey *rsa.PrivateKey
|
|
publicKey *rsa.PublicKey
|
|
dataKey []byte
|
|
}
|
|
|
|
func NewCryptoService(privateKeyPath string) (*CryptoService, error) {
|
|
// 读取私钥文件
|
|
privateKeyPEM, err := ioutil.ReadFile(privateKeyPath)
|
|
if err != nil {
|
|
// 如果私钥文件不存在,生成新的密钥对
|
|
if err := GenerateRSAKeyPair(privateKeyPath); err != nil {
|
|
return nil, fmt.Errorf("failed to generate RSA key pair: %w", err)
|
|
}
|
|
privateKeyPEM, err = ioutil.ReadFile(privateKeyPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read generated private key: %w", err)
|
|
}
|
|
}
|
|
|
|
// 解析私钥
|
|
privateKey, err := ParseRSAPrivateKeyFromPEM(privateKeyPEM)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
|
}
|
|
|
|
dataKey, err := loadDataKeyFromEnv()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load data encryption key: %w", err)
|
|
}
|
|
|
|
return &CryptoService{
|
|
privateKey: privateKey,
|
|
publicKey: &privateKey.PublicKey,
|
|
dataKey: dataKey,
|
|
}, nil
|
|
}
|
|
|
|
func GenerateRSAKeyPair(privateKeyPath string) error {
|
|
// 确保目录存在
|
|
dir := filepath.Dir(privateKeyPath)
|
|
if dir != "." {
|
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
|
}
|
|
}
|
|
|
|
// 生成 RSA 密钥对
|
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 编码私钥
|
|
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
})
|
|
|
|
// 保存私钥
|
|
if err := ioutil.WriteFile(privateKeyPath, privateKeyPEM, 0600); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 编码公钥
|
|
publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PUBLIC KEY",
|
|
Bytes: publicKeyDER,
|
|
})
|
|
|
|
// 保存公钥
|
|
publicKeyPath := privateKeyPath + ".pub"
|
|
if err := ioutil.WriteFile(publicKeyPath, publicKeyPEM, 0644); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ParseRSAPrivateKeyFromPEM(pemBytes []byte) (*rsa.PrivateKey, error) {
|
|
block, _ := pem.Decode(pemBytes)
|
|
if block == nil {
|
|
return nil, errors.New("no PEM block found")
|
|
}
|
|
|
|
switch block.Type {
|
|
case "RSA PRIVATE KEY":
|
|
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
case "PRIVATE KEY":
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rsaKey, ok := key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return nil, errors.New("not an RSA key")
|
|
}
|
|
return rsaKey, nil
|
|
default:
|
|
return nil, errors.New("unsupported key type: " + block.Type)
|
|
}
|
|
}
|
|
|
|
func loadDataKeyFromEnv() ([]byte, error) {
|
|
keyStr := strings.TrimSpace(os.Getenv(dataKeyEnvName))
|
|
if keyStr == "" {
|
|
return nil, fmt.Errorf("%s not set", dataKeyEnvName)
|
|
}
|
|
|
|
if key, ok := decodePossibleKey(keyStr); ok {
|
|
return key, nil
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte(keyStr))
|
|
key := make([]byte, len(sum))
|
|
copy(key, sum[:])
|
|
return key, nil
|
|
}
|
|
|
|
func decodePossibleKey(value string) ([]byte, bool) {
|
|
decoders := []func(string) ([]byte, error){
|
|
base64.StdEncoding.DecodeString,
|
|
base64.RawStdEncoding.DecodeString,
|
|
func(s string) ([]byte, error) { return hex.DecodeString(s) },
|
|
}
|
|
|
|
for _, decoder := range decoders {
|
|
if decoded, err := decoder(value); err == nil {
|
|
if key, ok := normalizeAESKey(decoded); ok {
|
|
return key, true
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func normalizeAESKey(raw []byte) ([]byte, bool) {
|
|
switch len(raw) {
|
|
case 16, 24, 32:
|
|
return raw, true
|
|
case 0:
|
|
return nil, false
|
|
default:
|
|
sum := sha256.Sum256(raw)
|
|
key := make([]byte, len(sum))
|
|
copy(key, sum[:])
|
|
return key, true
|
|
}
|
|
}
|
|
|
|
func (cs *CryptoService) HasDataKey() bool {
|
|
return len(cs.dataKey) > 0
|
|
}
|
|
|
|
func (cs *CryptoService) GetPublicKeyPEM() string {
|
|
publicKeyDER, err := x509.MarshalPKIXPublicKey(cs.publicKey)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
publicKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PUBLIC KEY",
|
|
Bytes: publicKeyDER,
|
|
})
|
|
|
|
return string(publicKeyPEM)
|
|
}
|
|
|
|
func (cs *CryptoService) EncryptForStorage(plaintext string, aadParts ...string) (string, error) {
|
|
if plaintext == "" {
|
|
return "", nil
|
|
}
|
|
if !cs.HasDataKey() {
|
|
return "", errors.New("data encryption key not configured")
|
|
}
|
|
if isEncryptedStorageValue(plaintext) {
|
|
return plaintext, nil
|
|
}
|
|
|
|
block, err := aes.NewCipher(cs.dataKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
aad := composeAAD(aadParts)
|
|
ciphertext := gcm.Seal(nil, nonce, []byte(plaintext), aad)
|
|
|
|
return storagePrefix +
|
|
base64.StdEncoding.EncodeToString(nonce) + storageDelimiter +
|
|
base64.StdEncoding.EncodeToString(ciphertext), nil
|
|
}
|
|
|
|
func (cs *CryptoService) DecryptFromStorage(value string, aadParts ...string) (string, error) {
|
|
if value == "" {
|
|
return "", nil
|
|
}
|
|
if !cs.HasDataKey() {
|
|
return "", errors.New("data encryption key not configured")
|
|
}
|
|
if !isEncryptedStorageValue(value) {
|
|
return "", errors.New("value is not encrypted")
|
|
}
|
|
|
|
payload := strings.TrimPrefix(value, storagePrefix)
|
|
parts := strings.SplitN(payload, storageDelimiter, 2)
|
|
if len(parts) != 2 {
|
|
return "", errors.New("invalid encrypted payload format")
|
|
}
|
|
|
|
nonce, err := base64.StdEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
return "", fmt.Errorf("decode nonce failed: %w", err)
|
|
}
|
|
|
|
ciphertext, err := base64.StdEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return "", fmt.Errorf("decode ciphertext failed: %w", err)
|
|
}
|
|
|
|
block, err := aes.NewCipher(cs.dataKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if len(nonce) != gcm.NonceSize() {
|
|
return "", fmt.Errorf("invalid nonce size: expected %d, got %d", gcm.NonceSize(), len(nonce))
|
|
}
|
|
|
|
aad := composeAAD(aadParts)
|
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, aad)
|
|
if err != nil {
|
|
return "", fmt.Errorf("decryption failed: %w", err)
|
|
}
|
|
|
|
return string(plaintext), nil
|
|
}
|
|
|
|
func (cs *CryptoService) IsEncryptedStorageValue(value string) bool {
|
|
return isEncryptedStorageValue(value)
|
|
}
|
|
|
|
func composeAAD(parts []string) []byte {
|
|
if len(parts) == 0 {
|
|
return nil
|
|
}
|
|
return []byte(strings.Join(parts, "|"))
|
|
}
|
|
|
|
func isEncryptedStorageValue(value string) bool {
|
|
return strings.HasPrefix(value, storagePrefix)
|
|
}
|
|
|
|
func (cs *CryptoService) DecryptPayload(payload *EncryptedPayload) ([]byte, error) {
|
|
// 1. 验证时间戳(防止重放攻击)
|
|
if payload.TS != 0 {
|
|
elapsed := time.Since(time.Unix(payload.TS, 0))
|
|
if elapsed > 5*time.Minute || elapsed < -1*time.Minute {
|
|
return nil, errors.New("timestamp invalid or expired")
|
|
}
|
|
}
|
|
|
|
// 2. 解码 base64url
|
|
wrappedKey, err := base64.RawURLEncoding.DecodeString(payload.WrappedKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode wrapped key: %w", err)
|
|
}
|
|
|
|
iv, err := base64.RawURLEncoding.DecodeString(payload.IV)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode IV: %w", err)
|
|
}
|
|
|
|
ciphertext, err := base64.RawURLEncoding.DecodeString(payload.Ciphertext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode ciphertext: %w", err)
|
|
}
|
|
|
|
var aad []byte
|
|
if payload.AAD != "" {
|
|
aad, err = base64.RawURLEncoding.DecodeString(payload.AAD)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode AAD: %w", err)
|
|
}
|
|
|
|
// 验证 AAD
|
|
var aadData AADData
|
|
if err := json.Unmarshal(aad, &aadData); err == nil {
|
|
// 可以在这里添加额外的验证逻辑
|
|
// 例如:验证 sessionID、userID 等
|
|
}
|
|
}
|
|
|
|
// 3. 使用 RSA-OAEP 解密 AES 密钥
|
|
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, cs.privateKey, wrappedKey, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unwrap AES key: %w", err)
|
|
}
|
|
|
|
// 4. 使用 AES-GCM 解密数据
|
|
block, err := aes.NewCipher(aesKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create AES cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
if len(iv) != gcm.NonceSize() {
|
|
return nil, fmt.Errorf("invalid IV size: expected %d, got %d", gcm.NonceSize(), len(iv))
|
|
}
|
|
|
|
// 解密并验证认证标签
|
|
plaintext, err := gcm.Open(nil, iv, ciphertext, aad)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("authentication/decryption failed: %w", err)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
func (cs *CryptoService) DecryptSensitiveData(payload *EncryptedPayload) (string, error) {
|
|
plaintext, err := cs.DecryptPayload(payload)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(plaintext), nil
|
|
} |