Reject a decrypted snapshot database that is not the requested one (closes #156)
Restore and deep verify downloaded and decrypted metadata/<key>/db.zst.age by object name alone. age decryption proves the database is readable, not that it is the snapshot that was asked for: an attacker who swaps in another valid db.zst.age could redirect the operation, and deep verify with a swapped database plus an empty manifest reported success with zero blobs verified. After the database is opened, both paths now confirm its identity: an exported per-snapshot database holds one snapshot row, and a snapshot remote key derives from that row ID, so the database is the requested one exactly when its sole snapshot hashes back to the remote key fetched. The shared check lives in verifySnapshotDBIdentity, backed by a new SnapshotRepository.GetOnlySnapshot. Model: opus-4-8
This commit was merged in pull request #194.
This commit is contained in:
+41
-17
@@ -191,24 +191,9 @@ func (v *Vaultik) loadVerificationData(
|
||||
v.stdoutf("Downloading and decrypting database...\n")
|
||||
}
|
||||
|
||||
// Download and decrypt database
|
||||
dbPath := fmt.Sprintf("metadata/%s/db.zst.age", remoteKey)
|
||||
log.Info("Downloading encrypted database", "path", dbPath)
|
||||
|
||||
dbReader, err := v.Storage.Get(v.ctx, dbPath)
|
||||
tdb, err := v.downloadVerifiedSnapshotDB(snapshotID, remoteKey, opts, result, identity)
|
||||
if err != nil {
|
||||
return nil, nil, nil, v.deepVerifyFailure(result, opts,
|
||||
fmt.Sprintf("failed to download database: %v", err),
|
||||
fmt.Errorf("failed to download database: %w", err))
|
||||
}
|
||||
|
||||
defer func() { _ = dbReader.Close() }()
|
||||
|
||||
tdb, err := v.decryptAndLoadDatabase(dbReader, identity)
|
||||
if err != nil {
|
||||
return nil, nil, nil, v.deepVerifyFailure(result, opts,
|
||||
fmt.Sprintf("failed to decrypt database: %v", err),
|
||||
fmt.Errorf("failed to decrypt database: %w", err))
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
dbBlobs, err := v.getBlobsFromDatabase(tdb.db.Conn())
|
||||
@@ -237,6 +222,45 @@ func (v *Vaultik) loadVerificationData(
|
||||
return manifest, tdb, dbBlobs, nil
|
||||
}
|
||||
|
||||
// downloadVerifiedSnapshotDB downloads and decrypts the snapshot metadata
|
||||
// database and confirms it really is the snapshot named by remoteKey
|
||||
// before any of its rows are trusted (see verifySnapshotDBIdentity). On
|
||||
// any failure it records the failure in result and returns the error the
|
||||
// caller should propagate; the temp database is closed on a rejected
|
||||
// identity so nothing is left on disk.
|
||||
func (v *Vaultik) downloadVerifiedSnapshotDB(
|
||||
snapshotID, remoteKey string, opts *VerifyOptions, result *VerifyResult,
|
||||
identity age.Identity,
|
||||
) (*tempDB, error) {
|
||||
dbPath := fmt.Sprintf("metadata/%s/db.zst.age", remoteKey)
|
||||
log.Info("Downloading encrypted database", "path", dbPath)
|
||||
|
||||
dbReader, err := v.Storage.Get(v.ctx, dbPath)
|
||||
if err != nil {
|
||||
return nil, v.deepVerifyFailure(result, opts,
|
||||
fmt.Sprintf("failed to download database: %v", err),
|
||||
fmt.Errorf("failed to download database: %w", err))
|
||||
}
|
||||
|
||||
defer func() { _ = dbReader.Close() }()
|
||||
|
||||
tdb, err := v.decryptAndLoadDatabase(dbReader, identity)
|
||||
if err != nil {
|
||||
return nil, v.deepVerifyFailure(result, opts,
|
||||
fmt.Sprintf("failed to decrypt database: %v", err),
|
||||
fmt.Errorf("failed to decrypt database: %w", err))
|
||||
}
|
||||
|
||||
err = v.verifySnapshotDBIdentity(tdb.db, snapshotID, remoteKey)
|
||||
if err != nil {
|
||||
_ = tdb.Close()
|
||||
|
||||
return nil, v.deepVerifyFailure(result, opts, err.Error(), err)
|
||||
}
|
||||
|
||||
return tdb, nil
|
||||
}
|
||||
|
||||
// runVerificationSteps executes manifest verification, blob existence
|
||||
// check, and deep content verification.
|
||||
func (v *Vaultik) runVerificationSteps(
|
||||
|
||||
Reference in New Issue
Block a user