All checks were successful
check / check (push) Successful in 5s
Updates golangci-lint to v2.12.2 everywhere it is pinned and installs the canonical `.golangci.yml`, then remediates every finding the new linter/config surfaces so `make check` is green. ## Version bump - `Dockerfile` lint stage: `golangci/golangci-lint:v2.11.3-alpine` -> `v2.12.2-alpine` (digest-pinned, date comment updated) - `Makefile` `deps` target: `go install` moved from the old v1 module path at `@latest` to the pinned `github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2` - `.golangci.yml` replaced with the canonical config (v2 schema; settings under `linters.settings` so the thresholds actually apply; `default: all` with the standard six disables) - `script/bootstrap` installs golangci-lint via the system package manager and carries no version pin, so it is unchanged - CI (`.gitea/workflows/check.yml`) only runs `script/cibuild`, so it needed no change ## Lint remediation The canonical config surfaced ~3,300 findings across 56k lines. All are fixed, behavior-preserving; incorporates and supersedes the per-package mechanical passes already merged to `main` (refs #61). Highlights: - `err113`: dynamic errors replaced with package sentinels + `%w` wrapping; comparisons via `errors.Is` - `goprintffuncname`: printf-style helpers renamed with an `f` suffix (`ui.Writer` message methods, `cli.ReportErrorf`, `database.Fatalf`) and all call sites updated - `revive` stutter renames: `blob.Handler`, `blob.WithReader`, `blob.ChunkPosition`, `storage.URL`, `storage.Info`; missing doc comments added - `contextcheck`/`noctx`: `context.Context` threaded through `blob.Packer` and the scanner call sites; context-aware `exec`/`sql` variants - `funlen`/`cyclop`/`gocognit`/`dupl`: oversized and duplicated functions split into focused helpers (production and test code) - tests: `t.Parallel()` added where safe (global logger init kept in the serial phase for `-race`), `t.TempDir()`/`t.Helper()` adopted, several suites converted to external test packages - `gosec`: bounded integer conversions, `ReadHeaderTimeout` on the test HTTP server; remaining warnings suppressed per-site with justifications - remaining `nolint` directives are rare, targeted, and each carries a reason (e.g. `nilnil` not-found contract in the repository layer, fx module globals, on-disk snake_case struct tags) - removed the deprecated `log.LogOptions` alias (callers migrated to `log.Options`) `make check` (tests with `-race`, lint, fmt-check) passes. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #62 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
225 lines
6.7 KiB
Go
225 lines
6.7 KiB
Go
// Package crypto provides thread-safe age encryption and decryption
|
|
// helpers used to protect blob and metadata content.
|
|
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
|
|
"filippo.io/age"
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// ErrNoRecipients is returned when an encryptor is created or updated
|
|
// without any recipient public keys.
|
|
var ErrNoRecipients = errors.New("at least one recipient is required")
|
|
|
|
// 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, ErrNoRecipients
|
|
}
|
|
|
|
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
|
|
_, err = w.Write(data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("writing encrypted data: %w", err)
|
|
}
|
|
|
|
// Close to flush
|
|
err = w.Close()
|
|
if 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
|
|
_, err = io.Copy(w, src)
|
|
if err != nil {
|
|
return fmt.Errorf("copying encrypted data: %w", err)
|
|
}
|
|
|
|
// Close to flush
|
|
err = w.Close()
|
|
if 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 ErrNoRecipients
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Decryptor provides thread-safe decryption using the age encryption library.
|
|
// It uses a private key to decrypt data that was encrypted for the corresponding
|
|
// public key.
|
|
type Decryptor struct {
|
|
identity age.Identity
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// NewDecryptor creates a new decryptor with the given age private key.
|
|
// The private key should be a valid age X25519 identity string.
|
|
// Returns an error if the private key is invalid.
|
|
func NewDecryptor(privateKey string) (*Decryptor, error) {
|
|
identity, err := age.ParseX25519Identity(privateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing age identity: %w", err)
|
|
}
|
|
|
|
return &Decryptor{
|
|
identity: identity,
|
|
}, nil
|
|
}
|
|
|
|
// Decrypt decrypts data using age decryption.
|
|
// This method is suitable for small to medium amounts of data that fit in memory.
|
|
// For large data streams, use DecryptStream instead.
|
|
func (d *Decryptor) Decrypt(data []byte) ([]byte, error) {
|
|
d.mu.RLock()
|
|
identity := d.identity
|
|
d.mu.RUnlock()
|
|
|
|
r, err := age.Decrypt(bytes.NewReader(data), identity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating decrypted reader: %w", err)
|
|
}
|
|
|
|
decrypted, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading decrypted data: %w", err)
|
|
}
|
|
|
|
return decrypted, nil
|
|
}
|
|
|
|
// DecryptStream returns a reader that decrypts data from the provided reader.
|
|
// This method is suitable for decrypting large files or streams as it processes
|
|
// data in a streaming fashion without loading everything into memory.
|
|
// The caller should close the input reader when done.
|
|
func (d *Decryptor) DecryptStream(src io.Reader) (io.Reader, error) {
|
|
d.mu.RLock()
|
|
identity := d.identity
|
|
d.mu.RUnlock()
|
|
|
|
r, err := age.Decrypt(src, identity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating decrypted reader: %w", err)
|
|
}
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// Module exports the crypto module for fx dependency injection.
|
|
//
|
|
//nolint:gochecknoglobals // fx module definitions are package globals
|
|
var Module = fx.Module("crypto")
|