Files
backend/internal/pkg/crypto/message_encryptor.go
lan d9aa4b46c3
All checks were successful
Build Backend / build (push) Successful in 4m55s
Build Backend / build-docker (push) Successful in 10m34s
refactor(server): decouple services and improve architecture
- Introduce interfaces for all major services (JWT, PostAI, Comment, Message, Notification, QRCodeLogin, Upload, Vote, etc.) to support dependency inversion.
- Move query parameters from `internal/dto` to a new `internal/query` package to separate request payloads from data transfer objects.
- Refactor `internal/router` to use embedded `RouterDeps` for cleaner dependency management.
- Decouple handlers from repositories by injecting services instead of direct repository access, ensuring proper layering.
- Improve database initialization by moving it from `internal/model` to `internal/database`.
- Optimize message decryption by implementing a more efficient `BatchDecrypt` method in `MessageEncryptor` using a worker pool.
- Enhance error handling and security by implementing fail-fast checks for encryption key length during startup.
- Clean up unused code, including the `avatar` package and several unused DTOs.
2026-06-15 03:41:59 +08:00

209 lines
4.5 KiB
Go
Raw Permalink 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
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"io"
"sync"
)
var (
ErrInvalidKey = errors.New("invalid encryption key: must be 32 bytes")
ErrInvalidCiphertext = errors.New("invalid ciphertext: too short")
ErrDecryptFailed = errors.New("decryption failed")
)
// MessageEncryptor 消息加密器
// 使用 AES-256-GCM 算法进行加密
type MessageEncryptor struct {
key []byte
gcm cipher.AEAD
keyVersion int
mu sync.RWMutex
}
// globalEncryptor 全局加密器实例
var globalEncryptor *MessageEncryptor
var encryptorOnce sync.Once
// InitMessageEncryptor 初始化全局消息加密器
// key 必须是32字节256位的密钥
func InitMessageEncryptor(key string, keyVersion int) error {
keyBytes := []byte(key)
if len(keyBytes) != 32 {
return ErrInvalidKey
}
block, err := aes.NewCipher(keyBytes)
if err != nil {
return err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
encryptorOnce.Do(func() {
globalEncryptor = &MessageEncryptor{
key: keyBytes,
gcm: gcm,
keyVersion: keyVersion,
}
})
return nil
}
// GetMessageEncryptor 获取全局加密器实例
func GetMessageEncryptor() *MessageEncryptor {
return globalEncryptor
}
// Encrypt 加密数据
// 返回 base64 编码的密文格式nonce + ciphertext + tag
func (e *MessageEncryptor) Encrypt(plaintext []byte) (string, error) {
if e == nil {
return "", errors.New("encryptor not initialized")
}
if len(plaintext) == 0 {
return "", nil
}
e.mu.RLock()
defer e.mu.RUnlock()
// 生成随机nonce12字节
nonce := make([]byte, e.gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 加密数据nonce附加在密文前面
ciphertext := e.gcm.Seal(nonce, nonce, plaintext, nil)
// 返回base64编码的密文
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 解密数据
// 输入 base64 编码的密文
func (e *MessageEncryptor) Decrypt(ciphertextBase64 string) ([]byte, error) {
if e == nil {
return nil, errors.New("encryptor not initialized")
}
if ciphertextBase64 == "" {
return nil, nil
}
e.mu.RLock()
defer e.mu.RUnlock()
// base64解码
ciphertext, err := base64.StdEncoding.DecodeString(ciphertextBase64)
if err != nil {
return nil, err
}
// 检查密文长度
nonceSize := e.gcm.NonceSize()
if len(ciphertext) < nonceSize {
return nil, ErrInvalidCiphertext
}
// 提取nonce和实际密文
nonce := ciphertext[:nonceSize]
actualCiphertext := ciphertext[nonceSize:]
// 解密
plaintext, err := e.gcm.Open(nil, nonce, actualCiphertext, nil)
if err != nil {
return nil, ErrDecryptFailed
}
return plaintext, nil
}
// GetKeyVersion 获取当前密钥版本
func (e *MessageEncryptor) GetKeyVersion() int {
if e == nil {
return 0
}
e.mu.RLock()
defer e.mu.RUnlock()
return e.keyVersion
}
// BatchDecrypt 批量解密多条密文,返回与输入等长的明文字节切片数组。
// 并发度由 workers 指定(<=0 时按密文数量自适应)。
// 解密失败或空密文的位置返回 nil调用方可据此判断。
// 这是 Decrypt 的无锁读优化版AEAD 的 Open 不修改内部状态,
// 但仍走 RLock 以兼容未来可能的密钥轮换语义。
func (e *MessageEncryptor) BatchDecrypt(ciphertexts []string, workers int) [][]byte {
results := make([][]byte, len(ciphertexts))
if e == nil || len(ciphertexts) == 0 {
return results
}
// 自适应并发度:与历史实现保持一致
if workers <= 0 {
switch {
case len(ciphertexts) < 20:
workers = 2
case len(ciphertexts) > 100:
workers = 8
default:
workers = 4
}
}
if workers > len(ciphertexts) {
workers = len(ciphertexts)
}
// 预取一次 AEAD避免 worker 内重复进入 RLock
e.mu.RLock()
gcm := e.gcm
nonceSize := e.gcm.NonceSize()
e.mu.RUnlock()
jobs := make(chan int, len(ciphertexts))
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := range jobs {
ct := ciphertexts[i]
if ct == "" {
continue
}
raw, err := base64.StdEncoding.DecodeString(ct)
if err != nil || len(raw) < nonceSize {
continue
}
nonce := raw[:nonceSize]
actual := raw[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, actual, nil)
if err != nil {
continue
}
results[i] = plaintext
}
}()
}
for i := range ciphertexts {
jobs <- i
}
close(jobs)
wg.Wait()
return results
}