Some checks failed
check / check (pull_request) Failing after 59s
Seven findings remain that cannot be fixed without either lying about the code or making a repo-wide naming decision, so each carries a per-site //nolint directive with its justification. gosec G115 (internal/log, internal/ui): term.IsTerminal takes an int and os.File.Fd() returns a uintptr, so the conversion is forced by the API. A file descriptor always fits in an int on every platform Go supports, and a closed file yields -1, which IsTerminal reports as not a terminal. gosec G703 (internal/vaultik/verify.go): the removed path comes from os.CreateTemp a few lines above and never from user input. G703's taint analysis treats every path derived from an *os.File as tainted, so there is no code shape that clears it. revive var-naming (internal/log, internal/crypto, internal/types): fixing these means renaming packages across the whole codebase, which is the repo owner's call, not a lint fix. Neither stdlib log nor stdlib crypto is imported anywhere in the repo, so nothing is actually shadowed today. The rename decision is tracked in issue #76. revive reports a package-name failure only once per package directory, on whichever file it happens to lint first, so every file of the affected packages carries the directive and lists nolintlint alongside revive so the ones that lose the race are not reported as unused. With this, make check exits 0 under the canonical .golangci.yml (sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb, unmodified), which also unblocks issue #59. TODO.md: record this work, correct the earlier entry that claimed make check was green when lint was still red, and move the next step on to the stale-branch triage.
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 //nolint:revive,nolintlint // stdlib crypto unused; see #76
|
|
|
|
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")
|