Files
BackupX/server/internal/storage/codec/cipher.go
2026-03-17 13:29:09 +08:00

156 lines
3.5 KiB
Go

package codec
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io"
)
const maskedValue = "********"
type ConfigCipher struct {
key []byte
}
type Cipher = ConfigCipher
func New(secret string) *ConfigCipher {
hash := sha256.Sum256([]byte(secret))
return &ConfigCipher{key: hash[:]}
}
func NewConfigCipher(secret string) *ConfigCipher {
return New(secret)
}
func (c *ConfigCipher) Key() []byte {
copyKey := make([]byte, len(c.key))
copy(copyKey, c.key)
return copyKey
}
func (c *ConfigCipher) Encrypt(raw []byte) (string, error) {
block, err := aes.NewCipher(c.key)
if err != nil {
return "", fmt.Errorf("create cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", fmt.Errorf("create gcm: %w", err)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", fmt.Errorf("generate nonce: %w", err)
}
sealed := gcm.Seal(nonce, nonce, raw, nil)
return base64.RawURLEncoding.EncodeToString(sealed), nil
}
func (c *ConfigCipher) Decrypt(encoded string) ([]byte, error) {
payload, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("decode ciphertext: %w", err)
}
block, err := aes.NewCipher(c.key)
if err != nil {
return nil, fmt.Errorf("create cipher: %w", err)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("create gcm: %w", err)
}
if len(payload) < gcm.NonceSize() {
return nil, fmt.Errorf("ciphertext too short")
}
nonce, ciphertext := payload[:gcm.NonceSize()], payload[gcm.NonceSize():]
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, fmt.Errorf("decrypt ciphertext: %w", err)
}
return plain, nil
}
func (c *ConfigCipher) EncryptJSON(value any) (string, error) {
plain, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("marshal plaintext: %w", err)
}
return c.Encrypt(plain)
}
func (c *ConfigCipher) DecryptJSON(encoded string, out any) error {
plain, err := c.Decrypt(encoded)
if err != nil {
return err
}
if err := json.Unmarshal(plain, out); err != nil {
return fmt.Errorf("unmarshal plaintext: %w", err)
}
return nil
}
func (c *ConfigCipher) EncryptValue(value any) (string, error) {
return c.EncryptJSON(value)
}
func (c *ConfigCipher) DecryptValue(encoded string, out any) error {
return c.DecryptJSON(encoded, out)
}
func MaskConfig(raw map[string]any, sensitiveFields []string) map[string]any {
masked := cloneMap(raw)
for _, field := range sensitiveFields {
value, ok := masked[field]
if !ok {
continue
}
switch actual := value.(type) {
case string:
if actual != "" {
masked[field] = maskedValue
}
default:
masked[field] = maskedValue
}
}
return masked
}
func MergeMaskedConfig(next map[string]any, existing map[string]any, sensitiveFields []string) map[string]any {
merged := cloneMap(existing)
for key, value := range next {
merged[key] = value
}
for _, field := range sensitiveFields {
value, ok := merged[field]
if !ok {
continue
}
switch actual := value.(type) {
case string:
if actual == "" || actual == maskedValue {
merged[field] = existing[field]
}
}
}
return merged
}
func IsMaskedString(value string) bool {
return bytes.Equal([]byte(value), []byte(maskedValue))
}
func cloneMap(source map[string]any) map[string]any {
result := make(map[string]any, len(source))
for key, value := range source {
result[key] = value
}
return result
}