// Package crypto 提供登录密码「RSA + AES-GCM」混合加密服务。 // // 设计目标:对称加密(AES-GCM)负责加密实际载荷,非对称加密(RSA-OAEP) // 负责加密 AES 会话密钥,两者结合兼顾性能与密钥分发安全: // - 前端:随机生成 AES-256 密钥 → AES-GCM 加密 {username,password,ts} → RSA 公钥加密 AES 密钥 // - 后端:持有 RSA 私钥 → 解出 AES 密钥 → 解出明文载荷(含时间戳防重放) // // 密钥来源优先级:配置 encrypt.privateKey(PEM)→ 本地文件 data/crypto/rsa_private.pem → 自动生成并持久化。 package crypto import ( "context" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "os" "path/filepath" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/text/gstr" ) const ( // rsaKeyBits RSA 密钥位数(2048 可加密 256 位 AES 密钥)。 rsaKeyBits = 2048 // aesNonceSize AES-GCM 推荐随机数长度(字节)。 aesNonceSize = 12 // payloadTTL 明文载荷时间戳容忍窗口:超出即视为重放/过期。 payloadTTL = 5 * time.Minute // keyFilePath 自动生成密钥的持久化路径(相对工作目录)。 keyFilePath = "data/crypto/rsa_private.pem" ) // LoginPayload 是前端混合加密后的明文载荷结构。 type LoginPayload struct { Username string `json:"username"` Password string `json:"password"` // TS 毫秒时间戳,用于防重放校验。 TS int64 `json:"ts"` } // FieldPayload 是通用加密字段载荷(创建管理员/重置密码等场景复用)。 type FieldPayload struct { Value string `json:"value"` // TS 毫秒时间戳,用于防重放校验。 TS int64 `json:"ts"` } // Service 提供登录密码混合加密能力。 type Service struct { privKey *rsa.PrivateKey // allowPlain 允许明文密码登录,仅限开发环境联调(生产必须关闭)。 allowPlain bool } // defaultSvc 进程级默认实例,供各业务模块复用(cmd 启动时经 New 注册)。 var defaultSvc *Service // SetDefault 注册进程级默认加密服务实例。 func SetDefault(s *Service) { defaultSvc = s } // Get 返回进程级默认加密服务实例,未初始化时 panic(启动必经 New 注册)。 func Get() *Service { if defaultSvc == nil { panic("crypto service not registered") } return defaultSvc } // New 加载或生成 RSA 密钥对并构建加密服务。 // 私钥来源:encrypt.privateKey 配置 > data/crypto/rsa_private.pem 文件 > 自动生成并落盘。 func New(ctx context.Context) (*Service, error) { s := &Service{allowPlain: g.Cfg().MustGet(ctx, "encrypt.allowPlain", false).Bool()} // 配置项可能是未注入的 ${ENV} 占位符,按未配置处理,走文件/自动生成。 privPEM := g.Cfg().MustGet(ctx, "encrypt.privateKey", "").String() if gstr.Contains(privPEM, "${") { privPEM = "" } if privPEM != "" { key, err := parsePrivateKeyPEM(privPEM) if err != nil { return nil, gerror.Wrap(err, "解析 encrypt.privateKey 失败") } s.privKey = key SetDefault(s) return s, nil } // 尝试读取本地持久化密钥文件。 if gfile.Exists(keyFilePath) { if b, err := os.ReadFile(keyFilePath); err == nil { if key, perr := parsePrivateKeyPEM(string(b)); perr == nil { s.privKey = key SetDefault(s) return s, nil } } } // 自动生成并尝试持久化(目录不可写时降级为内存持有,重启后公钥变化,前端每次登录重新拉取不受影响)。 key, err := rsa.GenerateKey(rand.Reader, rsaKeyBits) if err != nil { return nil, gerror.Wrap(err, "生成 RSA 密钥对失败") } s.privKey = key if err = os.MkdirAll(filepath.Dir(keyFilePath), 0o700); err == nil { _ = os.WriteFile(keyFilePath, []byte(encodePrivateKeyPEM(key)), 0o600) } SetDefault(s) return s, nil } // AllowPlain 返回是否允许明文密码登录(仅开发联调)。 func (s *Service) AllowPlain() bool { return s.allowPlain } // PublicKey 返回 PEM 格式 RSA 公钥(SubjectPublicKeyInfo / SPKI), // 供前端 Web Crypto importKey('spki') 做混合加密。 func (s *Service) PublicKey() string { // MarshalPKIXPublicKey 生成 SPKI(PKIX/SubjectPublicKeyInfo)DER,前端按 "PUBLIC KEY" PEM 解析。 der, err := x509.MarshalPKIXPublicKey(&s.privKey.PublicKey) if err != nil { panic(err) } return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})) } // DecryptLogin 混合解密前端登录载荷: // 1) RSA-OAEP(SHA-256) 私钥解出 AES 密钥;2) AES-256-GCM 解出明文 JSON; // 3) 校验时间戳窗口(防重放)与必填字段。 func (s *Service) DecryptLogin(_ context.Context, encryptedKeyB64, encryptedDataB64 string) (*LoginPayload, error) { plain, err := s.decryptPayload(encryptedKeyB64, encryptedDataB64) if err != nil { return nil, err } var p LoginPayload if err = json.Unmarshal(plain, &p); err != nil { return nil, gerror.Wrap(err, "解析载荷失败") } now := time.Now() if p.TS == 0 || p.Username == "" || p.Password == "" { return nil, gerror.New("载荷字段缺失") } if now.Sub(time.UnixMilli(p.TS)) > payloadTTL || time.UnixMilli(p.TS).Sub(now) > payloadTTL { return nil, gerror.New("载荷已过期,请重试") } return &p, nil } // DecryptField 混合解密通用加密字段(创建管理员/重置密码等场景): // 载荷 JSON 结构为 {"value":"...","ts":...},解密失败或字段缺失返回错误。 func (s *Service) DecryptField(_ context.Context, encryptedKeyB64, encryptedDataB64 string) (string, error) { plain, err := s.decryptPayload(encryptedKeyB64, encryptedDataB64) if err != nil { return "", err } var p FieldPayload if err = json.Unmarshal(plain, &p); err != nil { return "", gerror.Wrap(err, "解析载荷失败") } now := time.Now() if p.TS == 0 || p.Value == "" { return "", gerror.New("载荷字段缺失") } if now.Sub(time.UnixMilli(p.TS)) > payloadTTL || time.UnixMilli(p.TS).Sub(now) > payloadTTL { return "", gerror.New("载荷已过期,请重试") } return p.Value, nil } // decryptPayload 混合解密公共实现:RSA-OAEP 解 AES 密钥 → AES-GCM 解明文。 // 密文结构:encryptedData = base64(nonce(12B) || ciphertext)。 func (s *Service) decryptPayload(encryptedKeyB64, encryptedDataB64 string) ([]byte, error) { encKey, err := base64.StdEncoding.DecodeString(encryptedKeyB64) if err != nil { return nil, gerror.New("加密密钥格式错误") } aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, s.privKey, encKey, nil) if err != nil { return nil, gerror.New("解密 AES 会话密钥失败") } block, err := aes.NewCipher(aesKey) if err != nil { return nil, gerror.Wrap(err, "初始化 AES 失败") } gcm, err := cipher.NewGCM(block) if err != nil { return nil, gerror.Wrap(err, "初始化 AES-GCM 失败") } data, err := base64.StdEncoding.DecodeString(encryptedDataB64) if err != nil { return nil, gerror.New("密文格式错误") } if len(data) <= aesNonceSize { return nil, gerror.New("密文长度非法") } // 密文结构:nonce(12B) || ciphertext,GCM 自带完整性校验。 plain, err := gcm.Open(nil, data[:aesNonceSize], data[aesNonceSize:], nil) if err != nil { return nil, gerror.New("解密载荷失败") } return plain, nil } // parsePrivateKeyPEM 解析 PEM 格式 RSA 私钥(兼容 PKCS#1 与 PKCS#8)。 func parsePrivateKeyPEM(pemStr string) (*rsa.PrivateKey, error) { block, _ := pem.Decode([]byte(pemStr)) if block == nil { return nil, gerror.New("无效的 PEM 数据") } if k, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { return k, nil } k, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return nil, gerror.Wrap(err, "解析 RSA 私钥失败") } rsaKey, ok := k.(*rsa.PrivateKey) if !ok { return nil, gerror.New("密钥类型不是 RSA") } return rsaKey, nil } // encodePrivateKeyPEM 将 RSA 私钥编码为 PKCS#1 PEM。 func encodePrivateKeyPEM(key *rsa.PrivateKey) string { return string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) }