Open the downloaded snapshot database read-only, on a private temp dir (closes #162)
Restore and deep verify used to open the decrypted snapshot database read-write through the local-index constructor, which applied migrations against whatever the file carried, and left the decrypted file in the shared temp directory. A forged file could redefine what restore queries return, and an interrupted open left decrypted metadata on disk. Add database.OpenReadOnly: opens the file read-only (mode=ro) with query_only and trusted_schema=OFF, never applies schema files, and refuses a file whose schema carries a trigger, view or virtual table or lacks an expected table. Restore and deep verify now both use it, each inside its own private (0700) temp directory removed on every return path. pickNextDownload returns (FileID, bool) so a genuine nil-UUID file is not mistaken for "nothing left". Model: opus-4-8
This commit was merged in pull request #186.
This commit is contained in:
+41
-24
@@ -9,12 +9,12 @@ import (
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
|
||||
// Blank import registers the pure-Go sqlite driver for database/sql.
|
||||
_ "modernc.org/sqlite"
|
||||
"sneak.berlin/go/vaultik/internal/database"
|
||||
"sneak.berlin/go/vaultik/internal/log"
|
||||
"sneak.berlin/go/vaultik/internal/snapshot"
|
||||
)
|
||||
@@ -195,7 +195,7 @@ func (v *Vaultik) loadVerificationData(
|
||||
fmt.Errorf("failed to decrypt database: %w", err))
|
||||
}
|
||||
|
||||
dbBlobs, err := v.getBlobsFromDatabase(tdb.DB)
|
||||
dbBlobs, err := v.getBlobsFromDatabase(tdb.db.Conn())
|
||||
if err != nil {
|
||||
_ = tdb.Close()
|
||||
|
||||
@@ -256,7 +256,7 @@ func (v *Vaultik) runVerificationSteps(
|
||||
len(dbBlobs), ubytes(totalSize))
|
||||
}
|
||||
|
||||
err = v.performDeepVerificationFromDB(dbBlobs, tdb.DB, opts)
|
||||
err = v.performDeepVerificationFromDB(dbBlobs, tdb.db.Conn(), opts)
|
||||
if err != nil {
|
||||
return v.deepVerifyFailure(result, opts, err.Error(), err)
|
||||
}
|
||||
@@ -264,16 +264,18 @@ func (v *Vaultik) runVerificationSteps(
|
||||
return nil
|
||||
}
|
||||
|
||||
// tempDB wraps sql.DB with cleanup
|
||||
// tempDB is the downloaded snapshot database opened read-only for deep
|
||||
// verify, held in a private temp directory removed in full on Close.
|
||||
type tempDB struct {
|
||||
*sql.DB
|
||||
|
||||
tempPath string
|
||||
db *database.DB
|
||||
tempDir string
|
||||
}
|
||||
|
||||
func (t *tempDB) Close() error {
|
||||
err := t.DB.Close()
|
||||
_ = os.Remove(t.tempPath)
|
||||
err := t.db.Close()
|
||||
// Remove the whole private directory so the decrypted database and
|
||||
// any SQLite side files are gone on every path.
|
||||
_ = os.RemoveAll(t.tempDir)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -300,41 +302,56 @@ func (v *Vaultik) decryptAndLoadDatabase(reader io.ReadCloser) (*tempDB, error)
|
||||
}
|
||||
defer decompressor.Close()
|
||||
|
||||
// Create temporary file for the database
|
||||
tempFile, err := os.CreateTemp("", "vaultik-verify-*.db")
|
||||
// Materialize the decrypted database inside a private (0700) temp
|
||||
// directory so it is never world-readable, and remove the whole
|
||||
// directory on any failure below.
|
||||
tempDir, err := os.MkdirTemp("", "vaultik-verify-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
success := false
|
||||
|
||||
defer func() {
|
||||
if !success {
|
||||
_ = os.RemoveAll(tempDir)
|
||||
}
|
||||
}()
|
||||
|
||||
dbPath := filepath.Join(tempDir, snapshotDBFilename)
|
||||
|
||||
//nolint:gosec // G304: dbPath is our MkdirTemp dir plus a constant filename
|
||||
tempFile, err := os.OpenFile(
|
||||
dbPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, restoreFileMode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
|
||||
tempPath := tempFile.Name()
|
||||
|
||||
// Stream decompress directly to file
|
||||
log.Info("Decompressing database...")
|
||||
|
||||
written, err := io.Copy(tempFile, decompressor)
|
||||
if err != nil {
|
||||
_ = tempFile.Close()
|
||||
_ = os.Remove(tempPath)
|
||||
|
||||
return nil, fmt.Errorf("failed to decompress database: %w", err)
|
||||
}
|
||||
|
||||
_ = tempFile.Close()
|
||||
err = tempFile.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to close temp database file: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Database decompressed", "size", ubytes(written))
|
||||
|
||||
// Open the database
|
||||
db, err := sql.Open("sqlite", tempPath)
|
||||
db, err := database.OpenReadOnly(v.ctx, dbPath)
|
||||
if err != nil {
|
||||
_ = os.Remove(tempPath)
|
||||
|
||||
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
return &tempDB{
|
||||
DB: db,
|
||||
tempPath: tempPath,
|
||||
}, nil
|
||||
success = true
|
||||
|
||||
return &tempDB{db: db, tempDir: tempDir}, nil
|
||||
}
|
||||
|
||||
// verifyBlob downloads and verifies a single blob
|
||||
|
||||
Reference in New Issue
Block a user