service.xpcool.com/internal/library/crypto/crypto.go
夏犀麟 4b3c00270f
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 31s
feat(i18n): 中文化校验提示与服务层错误信息
将 api 层校验规则提示语、service 层 gerror.Wrap 与 response.Error
错误信息、panic 未注册提示统一改为中文,并同步中文化 cmd 路由注释
与变更记录。仅涉及注释、文档与字符串改动,无业务逻辑变更。
2026-09-13 23:22:46 +08:00

277 lines
9.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package crypto 提供登录密码「RSA + AES-GCM」混合加密服务。
//
// 设计目标对称加密AES-GCM负责加密实际载荷非对称加密RSA-OAEP
// 负责加密 AES 会话密钥,两者结合兼顾性能与密钥分发安全:
// - 前端:随机生成 AES-256 密钥 → AES-GCM 加密 {username,password,ts} → RSA 公钥加密 AES 密钥
// - 后端:持有 RSA 私钥 → 解出 AES 密钥 → 解出明文载荷(含时间戳防重放)
//
// 密钥来源优先级:配置 encrypt.privateKeyPEM→ 本地文件 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
// fullBody 全量请求/响应加密开关(生产开启):所有 JSON body 整体加密,
// 响应用请求会话密钥AES加密回传开发环境仅加密密码字段。
fullBody bool
}
// defaultSvc 进程级默认实例供各业务模块复用cmd 启动时经 New 注册)。
var defaultSvc *Service
// SetDefault 注册进程级默认加密服务实例。
func SetDefault(s *Service) { defaultSvc = s }
// Get 返回进程级默认加密服务实例,未初始化时 panic启动必经 New 注册)。
func Get() *Service {
if defaultSvc == nil {
panic("加密服务未注册")
}
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(),
fullBody: g.Cfg().MustGet(ctx, "encrypt.fullBody", 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 }
// FullBody 返回是否启用全量请求/响应加密(生产开启)。
func (s *Service) FullBody() bool { return s.fullBody }
// PublicKey 返回 PEM 格式 RSA 公钥SubjectPublicKeyInfo / SPKI
// 供前端 Web Crypto importKey('spki') 做混合加密。
func (s *Service) PublicKey() string {
// MarshalPKIXPublicKey 生成 SPKIPKIX/SubjectPublicKeyInfoDER前端按 "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
}
// DecryptRequest 全量加密请求解密:返回 AES 会话密钥base64与明文 body。
// 会话密钥由中间件暂存,供响应加密回传(前端持同一密钥解密响应)。
func (s *Service) DecryptRequest(encryptedKeyB64, encryptedDataB64 string) (aesKeyB64 string, plain []byte, err error) {
aesKey, plain, err := s.decryptPayload(encryptedKeyB64, encryptedDataB64)
if err != nil {
return "", nil, err
}
return base64.StdEncoding.EncodeToString(aesKey), plain, nil
}
// EncryptResponse 用请求会话密钥AES-256-GCM加密响应 JSON
// 返回 base64(nonce(12B) || ciphertext || tag),前端用同一会话密钥解密。
func (s *Service) EncryptResponse(aesKeyB64 string, plain []byte) (string, error) {
aesKey, err := base64.StdEncoding.DecodeString(aesKeyB64)
if err != nil {
return "", gerror.New("会话密钥格式错误")
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", gerror.Wrap(err, "初始化 AES 失败")
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", gerror.Wrap(err, "初始化 AES-GCM 失败")
}
nonce := make([]byte, aesNonceSize)
if _, err = rand.Read(nonce); err != nil {
return "", gerror.Wrap(err, "生成随机数失败")
}
sealed := gcm.Seal(nil, nonce, plain, nil)
return base64.StdEncoding.EncodeToString(append(nonce, sealed...)), nil
}
// decryptPayload 混合解密公共实现RSA-OAEP 解 AES 密钥 → AES-GCM 解明文。
// 密文结构encryptedData = base64(nonce(12B) || ciphertext || tag)。
func (s *Service) decryptPayload(encryptedKeyB64, encryptedDataB64 string) ([]byte, []byte, error) {
encKey, err := base64.StdEncoding.DecodeString(encryptedKeyB64)
if err != nil {
return nil, nil, gerror.New("加密密钥格式错误")
}
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, s.privKey, encKey, nil)
if err != nil {
return nil, nil, gerror.New("解密 AES 会话密钥失败")
}
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, nil, gerror.Wrap(err, "初始化 AES 失败")
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, gerror.Wrap(err, "初始化 AES-GCM 失败")
}
data, err := base64.StdEncoding.DecodeString(encryptedDataB64)
if err != nil {
return nil, nil, gerror.New("密文格式错误")
}
if len(data) <= aesNonceSize {
return nil, nil, gerror.New("密文长度非法")
}
// 密文结构nonce(12B) || ciphertextGCM 自带完整性校验。
plain, err := gcm.Open(nil, data[:aesNonceSize], data[aesNonceSize:], nil)
if err != nil {
return nil, nil, gerror.New("解密载荷失败")
}
return aesKey, 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)}))
}