Restore files at 0600 and make the blob hash check unskippable (closes #163)
check / check (pull_request) Successful in 1m21s
check / check (push) Successful in 2m42s

Regular files are now created with O_EXCL at mode 0600 and given their stored mode only after the content is written and closed, so a file whose stored mode is restrictive is never briefly readable by other local users mid-restore. A file whose write or close fails is removed rather than left partial, and a chmod failure is a user-visible warning instead of a debug line.

hashVerifyReader.Close now errors when closed before EOF, so a short read or early close can never obtain a blob whose hash was not verified; downloadBlobToCache drops the cache entry on any such failure.

verifyFile (--verify) now rejects a restored file with bytes past its last chunk. Tests cover each behaviour under umask 022.

Model: opus-4-8
This commit was merged in pull request #182.
This commit is contained in:
clawbot committed 2026-09-22 11:45:52 +02:00
1 parent 4c83e82543
commit d9f0220f94
4 files changed
+462 -23

No files matched your search

+48
View File
@@ -133,3 +133,51 @@ func TestFetchAndDecryptBlobVerifiesHash(t *testing.T) {
}
})
}
// TestFetchAndDecryptBlobCloseBeforeEOFFails verifies the hash check
// cannot be skipped: a caller that reads only part of the blob and then
// closes gets an error rather than silently unverified bytes.
func TestFetchAndDecryptBlobCloseBeforeEOFFails(t *testing.T) {
t.Parallel()
identity, err := age.GenerateX25519Identity()
if err != nil {
t.Fatalf("generating identity: %v", err)
}
plaintext := []byte("hello world test data for blob hash verification")
encryptedData, correctHash := buildHashTestBlob(t, identity, plaintext)
mockStorage := NewMockStorer()
blobPath := "blobs/" + correctHash[:2] + "/" +
correctHash[2:4] + "/" + correctHash
mockStorage.mu.Lock()
mockStorage.data[blobPath] = encryptedData
mockStorage.mu.Unlock()
tv := vaultik.NewForTesting(mockStorage)
rc, err := tv.FetchAndDecryptBlob(
context.Background(), correctHash, int64(len(encryptedData)), identity)
if err != nil {
t.Fatalf("unexpected error opening stream: %v", err)
}
// Read one byte, far short of the plaintext length, then close.
buf := make([]byte, 1)
_, err = rc.Read(buf)
if err != nil {
t.Fatalf("reading first byte: %v", err)
}
err = rc.Close()
if err == nil {
t.Fatal("expected error closing before EOF, got nil")
}
if !strings.Contains(err.Error(), "hash not verified") {
t.Fatalf("expected not-verified error, got: %v", err)
}
}