- 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
399 lines
9.3 KiB
Go
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
|
|
}
|
|
}
|
|
}
|