This commit represents a significant architectural overhaul of vaultik: Database Schema Changes: - Switch files table to use UUID primary keys instead of path-based keys - Add UUID primary keys to blobs table for immediate chunk association - Update all foreign key relationships to use UUIDs - Add comprehensive schema documentation in DATAMODEL.md - Add SQLite busy timeout handling for concurrent operations Streaming and Performance Improvements: - Implement true streaming blob packing without intermediate storage - Add streaming chunk processing to reduce memory usage - Improve progress reporting with real-time metrics - Add upload metrics tracking in new uploads table CLI Refactoring: - Restructure CLI to use subcommands: snapshot create/list/purge/verify - Add store info command for S3 configuration display - Add custom duration parser supporting days/weeks/months/years - Remove old backup.go in favor of enhanced snapshot.go - Add --cron flag for silent operation Configuration Changes: - Remove unused index_prefix configuration option - Add support for snapshot pruning retention policies - Improve configuration validation and error messages Testing Improvements: - Add comprehensive repository tests with edge cases - Add cascade delete debugging tests - Fix concurrent operation tests to use SQLite busy timeout - Remove tolerance for SQLITE_BUSY errors in tests Documentation: - Add MIT LICENSE file - Update README with new command structure - Add comprehensive DATAMODEL.md explaining database schema - Update DESIGN.md with UUID-based architecture Other Changes: - Add test-config.yml for testing - Update Makefile with better test output formatting - Fix various race conditions in concurrent operations - Improve error handling throughout
146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
"filippo.io/age"
|
|
)
|
|
|
|
// Encryptor provides thread-safe encryption using the age encryption library.
|
|
// It supports encrypting data for multiple recipients simultaneously, allowing
|
|
// any of the corresponding private keys to decrypt the data. This is useful
|
|
// for backup scenarios where multiple parties should be able to decrypt the data.
|
|
type Encryptor struct {
|
|
recipients []age.Recipient
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewEncryptor creates a new encryptor with the given age public keys.
|
|
// Each public key should be a valid age X25519 recipient string (e.g., "age1...")
|
|
// At least one recipient must be provided. Returns an error if any of the
|
|
// public keys are invalid or if no recipients are specified.
|
|
func NewEncryptor(publicKeys []string) (*Encryptor, error) {
|
|
if len(publicKeys) == 0 {
|
|
return nil, fmt.Errorf("at least one recipient is required")
|
|
}
|
|
|
|
recipients := make([]age.Recipient, 0, len(publicKeys))
|
|
for _, key := range publicKeys {
|
|
recipient, err := age.ParseX25519Recipient(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing age recipient %s: %w", key, err)
|
|
}
|
|
recipients = append(recipients, recipient)
|
|
}
|
|
|
|
return &Encryptor{
|
|
recipients: recipients,
|
|
}, nil
|
|
}
|
|
|
|
// Encrypt encrypts data using age encryption for all configured recipients.
|
|
// The encrypted data can be decrypted by any of the corresponding private keys.
|
|
// This method is suitable for small to medium amounts of data that fit in memory.
|
|
// For large data streams, use EncryptStream or EncryptWriter instead.
|
|
func (e *Encryptor) Encrypt(data []byte) ([]byte, error) {
|
|
e.mu.RLock()
|
|
recipients := e.recipients
|
|
e.mu.RUnlock()
|
|
|
|
var buf bytes.Buffer
|
|
|
|
// Create encrypted writer for all recipients
|
|
w, err := age.Encrypt(&buf, recipients...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating encrypted writer: %w", err)
|
|
}
|
|
|
|
// Write data
|
|
if _, err := w.Write(data); err != nil {
|
|
return nil, fmt.Errorf("writing encrypted data: %w", err)
|
|
}
|
|
|
|
// Close to flush
|
|
if err := w.Close(); err != nil {
|
|
return nil, fmt.Errorf("closing encrypted writer: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// EncryptStream encrypts data from reader to writer using age encryption.
|
|
// This method is suitable for encrypting large files or streams as it processes
|
|
// data in a streaming fashion without loading everything into memory.
|
|
// The encrypted data is written directly to the destination writer.
|
|
func (e *Encryptor) EncryptStream(dst io.Writer, src io.Reader) error {
|
|
e.mu.RLock()
|
|
recipients := e.recipients
|
|
e.mu.RUnlock()
|
|
|
|
// Create encrypted writer for all recipients
|
|
w, err := age.Encrypt(dst, recipients...)
|
|
if err != nil {
|
|
return fmt.Errorf("creating encrypted writer: %w", err)
|
|
}
|
|
|
|
// Copy data
|
|
if _, err := io.Copy(w, src); err != nil {
|
|
return fmt.Errorf("copying encrypted data: %w", err)
|
|
}
|
|
|
|
// Close to flush
|
|
if err := w.Close(); err != nil {
|
|
return fmt.Errorf("closing encrypted writer: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EncryptWriter creates a writer that encrypts data written to it.
|
|
// All data written to the returned WriteCloser will be encrypted and written
|
|
// to the destination writer. The caller must call Close() on the returned
|
|
// writer to ensure all encrypted data is properly flushed and finalized.
|
|
// This is useful for integrating encryption into existing writer-based pipelines.
|
|
func (e *Encryptor) EncryptWriter(dst io.Writer) (io.WriteCloser, error) {
|
|
e.mu.RLock()
|
|
recipients := e.recipients
|
|
e.mu.RUnlock()
|
|
|
|
// Create encrypted writer for all recipients
|
|
w, err := age.Encrypt(dst, recipients...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating encrypted writer: %w", err)
|
|
}
|
|
|
|
return w, nil
|
|
}
|
|
|
|
// UpdateRecipients updates the recipients for future encryption operations.
|
|
// This method is thread-safe and can be called while other encryption operations
|
|
// are in progress. Existing encryption operations will continue with the old
|
|
// recipients. At least one recipient must be provided. Returns an error if any
|
|
// of the public keys are invalid or if no recipients are specified.
|
|
func (e *Encryptor) UpdateRecipients(publicKeys []string) error {
|
|
if len(publicKeys) == 0 {
|
|
return fmt.Errorf("at least one recipient is required")
|
|
}
|
|
|
|
recipients := make([]age.Recipient, 0, len(publicKeys))
|
|
for _, key := range publicKeys {
|
|
recipient, err := age.ParseX25519Recipient(key)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing age recipient %s: %w", key, err)
|
|
}
|
|
recipients = append(recipients, recipient)
|
|
}
|
|
|
|
e.mu.Lock()
|
|
e.recipients = recipients
|
|
e.mu.Unlock()
|
|
|
|
return nil
|
|
}
|