Files
dbbackup/internal/encryption/encryption.go
A. Renz 914307ac8f ci: add golangci-lint config and fix formatting
- Add .golangci.yml with minimal linters (govet, ineffassign)
- Run gofmt -s and goimports on all files to fix formatting
- Disable fieldalignment and copylocks checks in govet
2025-12-11 17:53:28 +01:00

399 lines
9.3 KiB
Go

package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"golang.org/x/crypto/pbkdf2"
)
const (
// AES-256 requires 32-byte keys
KeySize = 32
// Nonce size for GCM
NonceSize = 12
// Salt size for key derivation
SaltSize = 32
// PBKDF2 iterations (100,000 is recommended minimum)
PBKDF2Iterations = 100000
// Magic header to identify encrypted files
EncryptedFileMagic = "DBBACKUP_ENCRYPTED_V1"
)
// EncryptionHeader stores metadata for encrypted files
type EncryptionHeader struct {
Magic [22]byte // "DBBACKUP_ENCRYPTED_V1" (21 bytes + null)
Version uint8 // Version number (1)
Algorithm uint8 // Algorithm ID (1 = AES-256-GCM)
Salt [32]byte // Salt for key derivation
Nonce [12]byte // GCM nonce
Reserved [32]byte // Reserved for future use
}
// EncryptionOptions configures encryption behavior
type EncryptionOptions struct {
// Key is the encryption key (32 bytes for AES-256)
Key []byte
// Passphrase for key derivation (alternative to direct key)
Passphrase string
// Salt for key derivation (if empty, will be generated)
Salt []byte
}
// DeriveKey derives an encryption key from a passphrase using PBKDF2
func DeriveKey(passphrase string, salt []byte) []byte {
return pbkdf2.Key([]byte(passphrase), salt, PBKDF2Iterations, KeySize, sha256.New)
}
// GenerateSalt creates a cryptographically secure random salt
func GenerateSalt() ([]byte, error) {
salt := make([]byte, SaltSize)
if _, err := rand.Read(salt); err != nil {
return nil, fmt.Errorf("failed to generate salt: %w", err)
}
return salt, nil
}
// GenerateKey creates a cryptographically secure random key
func GenerateKey() ([]byte, error) {
key := make([]byte, KeySize)
if _, err := rand.Read(key); err != nil {
return nil, fmt.Errorf("failed to generate key: %w", err)
}
return key, nil
}
// NewEncryptionWriter creates an encrypted writer that wraps an underlying writer
// Data written to this writer will be encrypted before being written to the underlying writer
func NewEncryptionWriter(w io.Writer, opts EncryptionOptions) (*EncryptionWriter, error) {
// Derive or validate key
var key []byte
var salt []byte
if opts.Passphrase != "" {
// Derive key from passphrase
if len(opts.Salt) == 0 {
var err error
salt, err = GenerateSalt()
if err != nil {
return nil, err
}
} else {
salt = opts.Salt
}
key = DeriveKey(opts.Passphrase, salt)
} else if len(opts.Key) > 0 {
if len(opts.Key) != KeySize {
return nil, fmt.Errorf("invalid key size: expected %d bytes, got %d", KeySize, len(opts.Key))
}
key = opts.Key
// Generate salt even when using direct key (for header)
var err error
salt, err = GenerateSalt()
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("either Key or Passphrase must be provided")
}
// Create AES cipher
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// Create GCM mode
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
// Generate nonce
nonce := make([]byte, NonceSize)
if _, err := rand.Read(nonce); err != nil {
return nil, fmt.Errorf("failed to generate nonce: %w", err)
}
// Write header
header := EncryptionHeader{
Version: 1,
Algorithm: 1, // AES-256-GCM
}
copy(header.Magic[:], []byte(EncryptedFileMagic))
copy(header.Salt[:], salt)
copy(header.Nonce[:], nonce)
if err := writeHeader(w, &header); err != nil {
return nil, fmt.Errorf("failed to write header: %w", err)
}
return &EncryptionWriter{
writer: w,
gcm: gcm,
nonce: nonce,
buffer: make([]byte, 0, 64*1024), // 64KB buffer
}, nil
}
// EncryptionWriter encrypts data written to it
type EncryptionWriter struct {
writer io.Writer
gcm cipher.AEAD
nonce []byte
buffer []byte
closed bool
}
// Write encrypts and writes data
func (ew *EncryptionWriter) Write(p []byte) (n int, err error) {
if ew.closed {
return 0, fmt.Errorf("writer is closed")
}
// Accumulate data in buffer
ew.buffer = append(ew.buffer, p...)
// If buffer is large enough, encrypt and write
const chunkSize = 64 * 1024 // 64KB chunks
for len(ew.buffer) >= chunkSize {
chunk := ew.buffer[:chunkSize]
encrypted := ew.gcm.Seal(nil, ew.nonce, chunk, nil)
// Write encrypted chunk size (4 bytes) then chunk
size := uint32(len(encrypted))
sizeBytes := []byte{
byte(size >> 24),
byte(size >> 16),
byte(size >> 8),
byte(size),
}
if _, err := ew.writer.Write(sizeBytes); err != nil {
return n, err
}
if _, err := ew.writer.Write(encrypted); err != nil {
return n, err
}
// Move remaining data to start of buffer
ew.buffer = ew.buffer[chunkSize:]
n += chunkSize
// Increment nonce for next chunk
incrementNonce(ew.nonce)
}
return len(p), nil
}
// Close flushes remaining data and finalizes encryption
func (ew *EncryptionWriter) Close() error {
if ew.closed {
return nil
}
ew.closed = true
// Encrypt and write remaining buffer
if len(ew.buffer) > 0 {
encrypted := ew.gcm.Seal(nil, ew.nonce, ew.buffer, nil)
size := uint32(len(encrypted))
sizeBytes := []byte{
byte(size >> 24),
byte(size >> 16),
byte(size >> 8),
byte(size),
}
if _, err := ew.writer.Write(sizeBytes); err != nil {
return err
}
if _, err := ew.writer.Write(encrypted); err != nil {
return err
}
}
// Write final zero-length chunk to signal end
if _, err := ew.writer.Write([]byte{0, 0, 0, 0}); err != nil {
return err
}
return nil
}
// NewDecryptionReader creates a decrypted reader from an encrypted stream
func NewDecryptionReader(r io.Reader, opts EncryptionOptions) (*DecryptionReader, error) {
// Read and parse header
header, err := readHeader(r)
if err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
// Verify magic
if string(header.Magic[:len(EncryptedFileMagic)]) != EncryptedFileMagic {
return nil, fmt.Errorf("not an encrypted backup file")
}
// Verify version
if header.Version != 1 {
return nil, fmt.Errorf("unsupported encryption version: %d", header.Version)
}
// Verify algorithm
if header.Algorithm != 1 {
return nil, fmt.Errorf("unsupported encryption algorithm: %d", header.Algorithm)
}
// Derive or validate key
var key []byte
if opts.Passphrase != "" {
key = DeriveKey(opts.Passphrase, header.Salt[:])
} else if len(opts.Key) > 0 {
if len(opts.Key) != KeySize {
return nil, fmt.Errorf("invalid key size: expected %d bytes, got %d", KeySize, len(opts.Key))
}
key = opts.Key
} else {
return nil, fmt.Errorf("either Key or Passphrase must be provided")
}
// Create AES cipher
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("failed to create cipher: %w", err)
}
// Create GCM mode
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("failed to create GCM: %w", err)
}
nonce := make([]byte, NonceSize)
copy(nonce, header.Nonce[:])
return &DecryptionReader{
reader: r,
gcm: gcm,
nonce: nonce,
buffer: make([]byte, 0),
}, nil
}
// DecryptionReader decrypts data from an encrypted stream
type DecryptionReader struct {
reader io.Reader
gcm cipher.AEAD
nonce []byte
buffer []byte
eof bool
}
// Read decrypts and returns data
func (dr *DecryptionReader) Read(p []byte) (n int, err error) {
// If we have buffered data, return it first
if len(dr.buffer) > 0 {
n = copy(p, dr.buffer)
dr.buffer = dr.buffer[n:]
return n, nil
}
// If EOF reached, return EOF
if dr.eof {
return 0, io.EOF
}
// Read next chunk size
sizeBytes := make([]byte, 4)
if _, err := io.ReadFull(dr.reader, sizeBytes); err != nil {
if err == io.EOF {
dr.eof = true
return 0, io.EOF
}
return 0, err
}
size := uint32(sizeBytes[0])<<24 | uint32(sizeBytes[1])<<16 | uint32(sizeBytes[2])<<8 | uint32(sizeBytes[3])
// Zero-length chunk signals end of stream
if size == 0 {
dr.eof = true
return 0, io.EOF
}
// Read encrypted chunk
encrypted := make([]byte, size)
if _, err := io.ReadFull(dr.reader, encrypted); err != nil {
return 0, err
}
// Decrypt chunk
decrypted, err := dr.gcm.Open(nil, dr.nonce, encrypted, nil)
if err != nil {
return 0, fmt.Errorf("decryption failed (wrong key?): %w", err)
}
// Increment nonce for next chunk
incrementNonce(dr.nonce)
// Return as much as fits in p, buffer the rest
n = copy(p, decrypted)
if n < len(decrypted) {
dr.buffer = decrypted[n:]
}
return n, nil
}
// Helper functions
func writeHeader(w io.Writer, h *EncryptionHeader) error {
data := make([]byte, 100) // Total header size
copy(data[0:22], h.Magic[:])
data[22] = h.Version
data[23] = h.Algorithm
copy(data[24:56], h.Salt[:])
copy(data[56:68], h.Nonce[:])
copy(data[68:100], h.Reserved[:])
_, err := w.Write(data)
return err
}
func readHeader(r io.Reader) (*EncryptionHeader, error) {
data := make([]byte, 100)
if _, err := io.ReadFull(r, data); err != nil {
return nil, err
}
header := &EncryptionHeader{
Version: data[22],
Algorithm: data[23],
}
copy(header.Magic[:], data[0:22])
copy(header.Salt[:], data[24:56])
copy(header.Nonce[:], data[56:68])
copy(header.Reserved[:], data[68:100])
return header, nil
}
func incrementNonce(nonce []byte) {
// Increment nonce as a big-endian counter
for i := len(nonce) - 1; i >= 0; i-- {
nonce[i]++
if nonce[i] != 0 {
break
}
}
}