Files
vaultik/internal/blobgen/reader.go
T
sneak 65d4bc572d
check / check (pull_request) Successful in 2m29s
Parse the age identity key once and accept every identity in it (closes #165)
Restore and verify --deep now parse the configured age secret key a
single time through a new internal helper that uses age.ParseIdentities
and hands every identity to age.Decrypt. A key file with several
identities (a whole age-keygen file) is fully accepted, so a blob
encrypted to any of its recipients decrypts, not just the first.

The helper is the first step of both commands, so a missing or
unparseable key now fails before anything is downloaded. Its error names
the configuration source (VAULTIK_AGE_SECRET_KEY or age_secret_key) and
never echoes the key value. config.extractAgeSecretKey and its silent
fallback are removed; the key is stored raw and parsed only where
decryption happens, so backup, list and prune are unaffected.

README, the restore help, and the missing-key error now show the key
read from a file with $(cat ...) rather than typed literally, keeping it
out of shell history, and say the variable may hold the whole key file.

Model: opus-4-8
2026-09-22 13:24:03 +00:00

80 lines
1.9 KiB
Go

package blobgen
import (
"crypto/sha256"
"fmt"
"hash"
"io"
"filippo.io/age"
"github.com/klauspost/compress/zstd"
)
// Reader wraps decompression and decryption with SHA256 verification
type Reader struct {
reader io.Reader
decompressor *zstd.Decoder
decryptor io.Reader
hasher hash.Hash
teeReader io.Reader
bytesRead int64
}
// NewReader creates a new Reader that decrypts, decompresses, and verifies
// data. Every supplied identity is offered to age.Decrypt, so a blob
// encrypted to any one of them can be read.
func NewReader(r io.Reader, identities ...age.Identity) (*Reader, error) {
// Create decryption reader
decReader, err := age.Decrypt(r, identities...)
if err != nil {
return nil, fmt.Errorf("creating decryption reader: %w", err)
}
// Create decompression reader
decompressor, err := zstd.NewReader(decReader)
if err != nil {
return nil, fmt.Errorf("creating decompression reader: %w", err)
}
// Create SHA256 hasher
hasher := sha256.New()
// Create tee reader that reads from decompressor and writes to hasher
teeReader := io.TeeReader(decompressor, hasher)
return &Reader{
reader: r,
decompressor: decompressor,
decryptor: decReader,
hasher: hasher,
teeReader: teeReader,
}, nil
}
// Read implements io.Reader
func (r *Reader) Read(p []byte) (int, error) {
n, err := r.teeReader.Read(p)
r.bytesRead += int64(n)
return n, err
}
// Close closes the decompressor
func (r *Reader) Close() error {
r.decompressor.Close()
return nil
}
// Sum256 returns the single SHA-256 of the plaintext read so far. This is the
// first hash only; the stored object name is its double hash, which callers
// obtain by passing this digest to DoubleSHA256.
func (r *Reader) Sum256() []byte {
return r.hasher.Sum(nil)
}
// BytesRead returns the number of uncompressed bytes read
func (r *Reader) BytesRead() int64 {
return r.bytesRead
}