check / check (pull_request) Successful in 2m29s
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
80 lines
1.9 KiB
Go
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
|
|
}
|