Files

133 lines
3.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"
)
// Encryptor 字段加密器接口。Encrypt 永远用主密钥,Decrypt 先试主密钥、失败试 legacy 密钥。
type Encryptor interface {
Encrypt(plaintext string) (string, error)
Decrypt(ciphertext string) (string, error)
}
// aesKey 持有一个 AES-GCM 密钥及其 cipher。
type aesKey struct {
key []byte
}
func newAESKey(key string) (*aesKey, error) {
keyBytes := []byte(key)
if len(keyBytes) != 16 && len(keyBytes) != 24 && len(keyBytes) != 32 {
return nil, errors.New("invalid key length: must be 16, 24, or 32 bytes")
}
return &aesKey{key: keyBytes}, nil
}
// FieldEncryptor 持有主密钥 + 可选 legacy 密钥列表。
// Encrypt 永远用 primary;Decrypt 先试 primary,gcm.Open 认证失败依次试 legacy,
// 用于密钥轮换期间透明解出旧密文。
type FieldEncryptor struct {
primary *aesKey
legacy []*aesKey
}
// NewFieldEncryptor 创建字段加密器。primary 为主密钥,legacy 为可选的旧密钥(用于回退解密)。
func NewFieldEncryptor(primary string, legacy ...string) (*FieldEncryptor, error) {
primaryKey, err := newAESKey(primary)
if err != nil {
return nil, err
}
legacyKeys := make([]*aesKey, 0, len(legacy))
for _, l := range legacy {
if l == "" || l == primary {
continue
}
k, err := newAESKey(l)
if err != nil {
return nil, err
}
legacyKeys = append(legacyKeys, k)
}
return &FieldEncryptor{primary: primaryKey, legacy: legacyKeys}, nil
}
// Encrypt 使用 AES-GCM 加密明文,输出 base64(nonce || ciphertext || gcmTag)。
func (e *FieldEncryptor) Encrypt(plaintext string) (string, error) {
if plaintext == "" {
return "", nil
}
gcm, err := e.gcm(e.primary)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 解密密文。先试主密钥,gcm.Open 认证失败依次试 legacy 密钥。
func (e *FieldEncryptor) Decrypt(ciphertext string) (string, error) {
if ciphertext == "" {
return "", nil
}
decoded, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return "", err
}
for _, k := range append([]*aesKey{e.primary}, e.legacy...) {
plaintext, err := decryptWithKey(k, decoded)
if err == nil {
return plaintext, nil
}
}
return "", errors.New("decrypt failed: no matching key")
}
func (e *FieldEncryptor) gcm(k *aesKey) (cipher.AEAD, error) {
block, err := aes.NewCipher(k.key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
func decryptWithKey(k *aesKey, decoded []byte) (string, error) {
block, err := aes.NewCipher(k.key)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonceSize := gcm.NonceSize()
if len(decoded) < nonceSize {
return "", errors.New("ciphertext too short")
}
nonce, body := decoded[:nonceSize], decoded[nonceSize:]
plaintext, err := gcm.Open(nil, nonce, body, nil)
if err != nil {
return "", err
}
return string(plaintext), nil
}
// MockEncryptor 测试用直通加密器,明文不加密。
type MockEncryptor struct{}
func (e *MockEncryptor) Encrypt(plaintext string) (string, error) {
return plaintext, nil
}
func (e *MockEncryptor) Decrypt(ciphertext string) (string, error) {
return ciphertext, nil
}