package secret import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "strings" ) var ErrEmpty = errors.New("secret is empty") type Box struct { gcm cipher.AEAD } func NewBox(keyMaterial string) (*Box, error) { if keyMaterial == "" { return nil, fmt.Errorf("encryption key material is required") } sum := sha256.Sum256([]byte(keyMaterial)) block, err := aes.NewCipher(sum[:]) if err != nil { return nil, err } gcm, err := cipher.NewGCM(block) if err != nil { return nil, err } return &Box{gcm: gcm}, nil } func (b *Box) Encrypt(plain string) (string, error) { if plain == "" { return "", ErrEmpty } nonce := make([]byte, b.gcm.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return "", err } ciphertext := b.gcm.Seal(nonce, nonce, []byte(plain), nil) return base64.StdEncoding.EncodeToString(ciphertext), nil } func (b *Box) Decrypt(encoded string) (string, error) { if encoded == "" { return "", ErrEmpty } raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "", err } nonceSize := b.gcm.NonceSize() if len(raw) < nonceSize { return "", fmt.Errorf("invalid ciphertext") } nonce, ciphertext := raw[:nonceSize], raw[nonceSize:] plain, err := b.gcm.Open(nil, nonce, ciphertext, nil) if err != nil { return "", err } return string(plain), nil } func IsDecryptFailure(err error) bool { if err == nil { return false } return strings.Contains(err.Error(), "message authentication failed") } func DecryptHint(err error) error { if err == nil { return nil } if IsDecryptFailure(err) { return fmt.Errorf("DNS API 凭据无法解密,请在 DDNS 页面重新保存 API Token(服务加密密钥可能已变更)") } return err }