115 lines
4.3 KiB
Go
115 lines
4.3 KiB
Go
|
|
// Package crypt — шифрование чувствительных полей перед записью в БД:
|
|||
|
|
// сейчас это токены собственных ботов Telegram (миграция 0012).
|
|||
|
|
//
|
|||
|
|
// Токен бота — полный контроль над ботом, поэтому в базе он лежит
|
|||
|
|
// зашифрованным AES-256-GCM (authenticated encryption): утечка дампа
|
|||
|
|
// не даёт читать токены, а подмена байтов в колонке ловится при
|
|||
|
|
// расшифровке. Ключ — секрет сервера DATA_ENCRYPTION_KEY (32 байта,
|
|||
|
|
// base64 или hex), сам он в коде и в логах не появляется.
|
|||
|
|
//
|
|||
|
|
// Без ключа фича выключается на уровне хендлеров (Cipher == nil):
|
|||
|
|
// ввод токена отвечает 400, сохранённое расшифровать некому — ключ
|
|||
|
|
// меняется только вместе с явным планом миграции.
|
|||
|
|
package crypt
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"crypto/aes"
|
|||
|
|
"crypto/cipher"
|
|||
|
|
"crypto/rand"
|
|||
|
|
"encoding/base64"
|
|||
|
|
"encoding/hex"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"strings"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
// keyLen — AES-256: 32 байта.
|
|||
|
|
keyLen = 32
|
|||
|
|
// nonceLen — размер nonce для GCM: 96 бит, стандарт из RFC 5116.
|
|||
|
|
nonceLen = 12
|
|||
|
|
// tagLen — аутентифицирующая метка GCM (не считается в nonce).
|
|||
|
|
tagLen = 16
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
// ErrKeySize — ключ не 32 байта (неверный base64/hex).
|
|||
|
|
ErrKeySize = errors.New("encryption key must be 32 bytes")
|
|||
|
|
// ErrCorrupt — данные не читаются: не наш формат или биты подделаны.
|
|||
|
|
ErrCorrupt = errors.New("ciphertext is corrupt")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Cipher — AES-256-GCM поверх одного ключа. Потокобезопасен:
|
|||
|
|
// stateless-шум в nonce, общих структур нет.
|
|||
|
|
type Cipher struct {
|
|||
|
|
aead cipher.AEAD
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// New создаёт шифровальщик из ключа в base64 (стандартное кодирование,
|
|||
|
|
// допускается и без паддинга) либо в hex. Сам ключ генерируется так:
|
|||
|
|
// `openssl rand -base64 32` либо `openssl rand -hex 32`.
|
|||
|
|
func New(key string) (*Cipher, error) {
|
|||
|
|
raw, err := decodeKey(strings.TrimSpace(key))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
block, err := aes.NewCipher(raw)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
aead, err := cipher.NewGCM(block)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &Cipher{aead: aead}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Seal шифрует строку: к возвращаемому значению приклеивается случайный
|
|||
|
|
// nonce (12 байт), весь результат — base64. Результат не содержит исходного
|
|||
|
|
// текста и не выглядит как base64 от него.
|
|||
|
|
func (c *Cipher) Seal(plain string) (string, error) {
|
|||
|
|
nonce := make([]byte, nonceLen)
|
|||
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|||
|
|
return "", fmt.Errorf("nonce: %w", err)
|
|||
|
|
}
|
|||
|
|
out := c.aead.Seal(nonce, nonce, []byte(plain), nil)
|
|||
|
|
return base64.StdEncoding.EncodeToString(out), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Open расшифровывает то, что вернул Seal. Ошибка — либо чужие данные,
|
|||
|
|
// либо другой ключ: причина наружу не раскрывается (ErrCorrupt единый),
|
|||
|
|
// детали логирует вызывающий.
|
|||
|
|
func (c *Cipher) Open(enc string) (string, error) {
|
|||
|
|
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(enc))
|
|||
|
|
if err != nil {
|
|||
|
|
return "", ErrCorrupt
|
|||
|
|
}
|
|||
|
|
if len(raw) < nonceLen+tagLen {
|
|||
|
|
return "", ErrCorrupt
|
|||
|
|
}
|
|||
|
|
nonce, sealed := raw[:nonceLen], raw[nonceLen:]
|
|||
|
|
plain, err := c.aead.Open(nil, nonce, sealed, nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", ErrCorrupt
|
|||
|
|
}
|
|||
|
|
return string(plain), nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// decodeKey — base64 или hex, ровно 32 байта.
|
|||
|
|
func decodeKey(key string) ([]byte, error) {
|
|||
|
|
if key == "" {
|
|||
|
|
return nil, ErrKeySize
|
|||
|
|
}
|
|||
|
|
for _, dec := range []func(string) ([]byte, error){
|
|||
|
|
base64.StdEncoding.DecodeString,
|
|||
|
|
base64.RawStdEncoding.DecodeString,
|
|||
|
|
hex.DecodeString,
|
|||
|
|
} {
|
|||
|
|
if raw, err := dec(key); err == nil && len(raw) == keyLen {
|
|||
|
|
return raw, nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil, ErrKeySize
|
|||
|
|
}
|