mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 03:56:38 +08:00
88 lines
2.8 KiB
Go
88 lines
2.8 KiB
Go
// Package helper — AES-256-GCM at-rest secret encryption.
|
|
//
|
|
// Sensitive values (third-party API keys, service passwords, book-source
|
|
// login credentials) are stored in the database. Encrypting them keyed off
|
|
// a per-install secret means a stolen DB file alone is not enough to
|
|
// recover the plaintext.
|
|
//
|
|
// Format on disk: "enc:v1:" + base64(nonce || ciphertext || tag).
|
|
// Legacy plaintext rows (no prefix) round-trip unchanged so an upgraded
|
|
// install needs no migration step.
|
|
package helper
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"strings"
|
|
)
|
|
|
|
// EncPrefix tags ciphertext rows so they can be told apart from plaintext.
|
|
const EncPrefix = "enc:v1:"
|
|
|
|
// SecretCipher encrypts/decrypts strings with AES-256-GCM derived from a
|
|
// stable per-install secret. A zero-value SecretCipher (empty key) is a
|
|
// pass-through, which keeps unit tests and keyless dev setups working.
|
|
type SecretCipher struct {
|
|
aead cipher.AEAD
|
|
}
|
|
|
|
// NewSecretCipher derives a 256-bit key from secret via SHA-256.
|
|
func NewSecretCipher(secret string) *SecretCipher {
|
|
if strings.TrimSpace(secret) == "" {
|
|
return &SecretCipher{}
|
|
}
|
|
sum := sha256.Sum256([]byte(secret))
|
|
block, err := aes.NewCipher(sum[:])
|
|
if err != nil {
|
|
return &SecretCipher{}
|
|
}
|
|
aead, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return &SecretCipher{}
|
|
}
|
|
return &SecretCipher{aead: aead}
|
|
}
|
|
|
|
// Ready 表示是否真的能加解密(密钥有效)。
|
|
func (c *SecretCipher) Ready() bool { return c != nil && c.aead != nil }
|
|
|
|
// Encrypt returns the prefixed base64 ciphertext for plain.
|
|
// Empty inputs and already-encrypted values round-trip unchanged.
|
|
func (c *SecretCipher) Encrypt(plain string) string {
|
|
if plain == "" || !c.Ready() || strings.HasPrefix(plain, EncPrefix) {
|
|
return plain
|
|
}
|
|
nonce := make([]byte, c.aead.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return plain
|
|
}
|
|
sealed := c.aead.Seal(nonce, nonce, []byte(plain), nil)
|
|
return EncPrefix + base64.StdEncoding.EncodeToString(sealed)
|
|
}
|
|
|
|
// Decrypt returns the plaintext for a stored value. Plaintext rows (no
|
|
// prefix) are returned unchanged, as are values that fail to authenticate.
|
|
func (c *SecretCipher) Decrypt(value string) string {
|
|
if value == "" || !c.Ready() || !strings.HasPrefix(value, EncPrefix) {
|
|
return value
|
|
}
|
|
data, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(value, EncPrefix))
|
|
if err != nil || len(data) < c.aead.NonceSize() {
|
|
return value
|
|
}
|
|
nonce, body := data[:c.aead.NonceSize()], data[c.aead.NonceSize():]
|
|
plain, err := c.aead.Open(nil, nonce, body, nil)
|
|
if err != nil {
|
|
return value
|
|
}
|
|
return string(plain)
|
|
}
|
|
|
|
// IsEncrypted reports whether value carries the encrypted prefix.
|
|
func (c *SecretCipher) IsEncrypted(value string) bool {
|
|
return strings.HasPrefix(value, EncPrefix)
|
|
}
|