mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-06 02:09:37 +08:00
first commit
This commit is contained in:
155
server/internal/storage/codec/cipher.go
Normal file
155
server/internal/storage/codec/cipher.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user