Bound download expansion and escape control chars on the terminal (closes #164)
Objects fetched from the store are untrusted; several decode paths let one expand or print without limit. - blobgen.LimitReader errors past a byte cap (not io.LimitReader silent EOF). DecodeManifest reads through caps on both compressed input and decompressed output, far above any real manifest, so json.Decode cannot buffer a compressible bomb. FetchAndDecryptBlob bounds decompression to the blob recorded uncompressed_size (not the restoring host blob_size_limit). - downloadSnapshotDB streams straight from storage to its temp file with io.Copy, replacing two ReadAll calls that held the whole database twice. - FetchBlob drops the per-blob Stat round-trip, its expectedSize parameter and returned size, all of which only fed a debug log. - TTYHandler and ui.Writer escape control characters in messages, attribute keys/values, and rendered identifiers/paths before colour codes are applied, so a crafted value cannot drive the terminal. Model: opus-4-8
This commit was merged in pull request #197.
This commit is contained in:
+33
-34
@@ -1,7 +1,6 @@
|
||||
package vaultik
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@@ -428,7 +427,7 @@ func (s *restoreSession) downloadNextBlobSet(plan *restorePlan) (bool, error) {
|
||||
return false, fmt.Errorf("%w: %s", errBlobMissingFromIndex, shortHash(hash))
|
||||
}
|
||||
|
||||
err := s.downloadBlobToCache(hash, blob.CompressedSize)
|
||||
err := s.downloadBlobToCache(hash, blob.CompressedSize, blob.UncompressedSize)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("downloading blob %s: %w", shortHash(hash), err)
|
||||
}
|
||||
@@ -655,32 +654,18 @@ func (v *Vaultik) downloadSnapshotDB(
|
||||
|
||||
defer func() { _ = reader.Close() }()
|
||||
|
||||
// Read all data
|
||||
encryptedData, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("reading encrypted data: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Downloaded encrypted database",
|
||||
"size", ubytes(int64(len(encryptedData))))
|
||||
|
||||
// Decrypt and decompress using blobgen.Reader
|
||||
blobReader, err := blobgen.NewReader(bytes.NewReader(encryptedData), identities...)
|
||||
// Decrypt and decompress straight from the storage stream, then stream
|
||||
// the plaintext to a temp file. Neither the encrypted bytes nor the
|
||||
// decrypted database is ever held whole in memory; a snapshot database
|
||||
// can be large.
|
||||
blobReader, err := blobgen.NewReader(reader, identities...)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("creating decryption reader: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = blobReader.Close() }()
|
||||
|
||||
// Read the binary SQLite database
|
||||
dbData, err := io.ReadAll(blobReader)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("decrypting and decompressing: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Decrypted database", "size", ubytes(int64(len(dbData))))
|
||||
|
||||
db, tempDir, err := v.materializeSnapshotDB(dbData)
|
||||
db, tempDir, err := v.materializeSnapshotDB(blobReader)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -729,13 +714,15 @@ func (v *Vaultik) verifySnapshotDBIdentity(
|
||||
return nil
|
||||
}
|
||||
|
||||
// materializeSnapshotDB writes the decrypted snapshot database bytes into
|
||||
// a fresh private (0700) temp directory and opens the file read-only. On
|
||||
// any failure it removes the directory before returning, so no decrypted
|
||||
// metadata is left on disk when the open is interrupted or the payload is
|
||||
// damaged. On success the returned directory is the caller's to remove.
|
||||
// materializeSnapshotDB streams the decrypted snapshot database into a
|
||||
// fresh private (0700) temp directory and opens the file read-only. The
|
||||
// database is copied through an io.Copy buffer rather than read whole into
|
||||
// memory. On any failure it removes the directory before returning, so no
|
||||
// decrypted metadata is left on disk when the copy is interrupted or the
|
||||
// payload is damaged. On success the returned directory is the caller's to
|
||||
// remove.
|
||||
func (v *Vaultik) materializeSnapshotDB(
|
||||
dbData []byte,
|
||||
dbReader io.Reader,
|
||||
) (*database.DB, string, error) {
|
||||
tempDir, err := afero.TempDir(v.Fs, "", "vaultik-restore-")
|
||||
if err != nil {
|
||||
@@ -752,12 +739,24 @@ func (v *Vaultik) materializeSnapshotDB(
|
||||
|
||||
dbPath := filepath.Join(tempDir, snapshotDBFilename)
|
||||
|
||||
err = afero.WriteFile(v.Fs, dbPath, dbData, restoreFileMode)
|
||||
dbFile, err := v.Fs.OpenFile(
|
||||
dbPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, restoreFileMode)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("writing database file: %w", err)
|
||||
return nil, "", fmt.Errorf("creating database file: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Created restore database", "path", dbPath)
|
||||
written, copyErr := io.Copy(dbFile, dbReader)
|
||||
closeErr := dbFile.Close()
|
||||
|
||||
if copyErr != nil {
|
||||
return nil, "", fmt.Errorf("writing database file: %w", copyErr)
|
||||
}
|
||||
|
||||
if closeErr != nil {
|
||||
return nil, "", fmt.Errorf("closing database file: %w", closeErr)
|
||||
}
|
||||
|
||||
log.Debug("Created restore database", "path", dbPath, "size", ubytes(written))
|
||||
|
||||
db, err := database.OpenReadOnly(v.ctx, dbPath)
|
||||
if err != nil {
|
||||
@@ -1212,12 +1211,12 @@ func (s *restoreSession) writeFileChunks(
|
||||
// size, which is what makes multi-GB blobs tractable on machines with
|
||||
// less RAM than the blob.
|
||||
func (s *restoreSession) downloadBlobToCache(
|
||||
blobHash string, expectedSize int64,
|
||||
blobHash string, compressedSize, uncompressedSize int64,
|
||||
) error {
|
||||
start := time.Now()
|
||||
|
||||
t0 := time.Now()
|
||||
rc, err := s.v.FetchAndDecryptBlob(s.ctx, blobHash, expectedSize, s.identities...)
|
||||
rc, err := s.v.FetchAndDecryptBlob(s.ctx, blobHash, uncompressedSize, s.identities...)
|
||||
fetchSetupDur := time.Since(t0)
|
||||
|
||||
if err != nil {
|
||||
@@ -1247,7 +1246,7 @@ func (s *restoreSession) downloadBlobToCache(
|
||||
|
||||
log.Debug("Streamed blob into disk cache",
|
||||
"hash", blobHash[:16],
|
||||
"compressed_bytes", expectedSize,
|
||||
"compressed_bytes", compressedSize,
|
||||
"plaintext_bytes", written,
|
||||
"ms_total", time.Since(start).Milliseconds(),
|
||||
"ms_fetch_setup", fetchSetupDur.Milliseconds(),
|
||||
|
||||
Reference in New Issue
Block a user