Files
zero/internal/securefile/securefile_test.go
euxaristia 8536cc87f7 fix(securefile): reclaim stale lock files to prevent permanent DOS (#615)
* fix(securefile): reclaim stale lock files to prevent permanent DOS

When createSecretFile fails to acquire the O_EXCL lock because it already exists, check if the file is stale (older than 10 seconds). If so, atomically reclaim it via a rename-aside check, preventing a permanent DOS after abrupt process crashes. Update the lock file to write and verify process-specific tokens, and add TestCreateSecretFileReclaimsStaleLock to cover the stale reclaim flow.

* style(securefile): gofmt const block alignment
2026-07-10 10:07:22 +08:00

141 lines
3.9 KiB
Go

package securefile
import (
"os"
"path/filepath"
"runtime"
"testing"
"time"
)
func TestCreateSecretFileRetriesOnPermissionContention(t *testing.T) {
secretPath := filepath.Join(t.TempDir(), "k.secret")
attempts := 0
origOpen := openSecretLockFile
openSecretLockFile = func(path string, flag int, perm os.FileMode) (*os.File, error) {
attempts++
if attempts == 1 {
return nil, os.ErrPermission
}
return os.OpenFile(path, flag, perm)
}
t.Cleanup(func() {
openSecretLockFile = origOpen
})
secret, err := createSecretFile(secretPath)
if err != nil {
t.Fatalf("createSecretFile should retry on permission contention: %v", err)
}
if len(secret) != secretBytes {
t.Fatalf("secret length = %d, want %d", len(secret), secretBytes)
}
if attempts < 2 {
t.Fatalf("expected at least 2 lock attempts, got %d", attempts)
}
}
func TestSealOpenRoundTrip(t *testing.T) {
secret := filepath.Join(t.TempDir(), "k.secret")
c := NewCrypter(secret)
plain := []byte(`{"openai":{"type":"api","key":"sk-test-123"}}`)
blob, err := c.Seal(plain)
if err != nil {
t.Fatalf("seal: %v", err)
}
if string(blob) == string(plain) {
t.Fatal("blob must not equal plaintext")
}
// The secret file is created 0600.
info, err := os.Stat(secret)
if err != nil {
t.Fatalf("secret not created: %v", err)
}
// Windows doesn't honor Unix permission bits (Chmod only toggles read-only), so
// the 0600 guarantee only holds on Unix.
if runtime.GOOS != "windows" {
if perm := info.Mode().Perm(); perm != 0o600 {
t.Fatalf("secret perm = %o, want 600", perm)
}
}
got, err := c.Open(blob)
if err != nil {
t.Fatalf("open: %v", err)
}
if string(got) != string(plain) {
t.Fatalf("round-trip = %q, want %q", got, plain)
}
}
func TestOpenTamperedFailsClosed(t *testing.T) {
c := NewCrypter(filepath.Join(t.TempDir(), "k.secret"))
blob, err := c.Seal([]byte("secret-data"))
if err != nil {
t.Fatal(err)
}
blob[len(blob)-1] ^= 0xff // flip a ciphertext bit
if _, err := c.Open(blob); err == nil {
t.Fatal("expected tampered blob to fail authentication")
}
}
func TestOpenMissingSecretFailsClosed(t *testing.T) {
c := NewCrypter(filepath.Join(t.TempDir(), "absent.secret"))
if _, err := c.Open([]byte("0123456789012")); err == nil {
t.Fatal("expected open with no secret to fail, not mint a new key")
}
}
func TestOpenShortBlobFails(t *testing.T) {
c := NewCrypter(filepath.Join(t.TempDir(), "k.secret"))
if _, err := c.Seal([]byte("x")); err != nil { // create the secret
t.Fatal(err)
}
if _, err := c.Open([]byte("short")); err == nil {
t.Fatal("expected too-short blob to fail")
}
}
func TestWrongSecretCannotDecrypt(t *testing.T) {
dir := t.TempDir()
a := NewCrypter(filepath.Join(dir, "a.secret"))
blob, err := a.Seal([]byte("data"))
if err != nil {
t.Fatal(err)
}
b := NewCrypter(filepath.Join(dir, "b.secret")) // different secret
if _, err := b.Open(blob); err == nil {
t.Fatal("expected decryption under a different secret to fail")
}
}
func TestCreateSecretFileReclaimsStaleLock(t *testing.T) {
secretPath := filepath.Join(t.TempDir(), "k.secret")
lockPath := secretPath + ".lock"
// Create a stale lock file manually
if err := os.WriteFile(lockPath, []byte("99999-12345-1"), 0o600); err != nil {
t.Fatal(err)
}
// Backdate the lock file's modification time so it is considered stale
staleTime := time.Now().Add(-2 * secureLockStaleAfter)
if err := os.Chtimes(lockPath, staleTime, staleTime); err != nil {
t.Fatal(err)
}
// Attempt to create the secret file; it should detect the stale lock, reclaim it, and succeed
secret, err := createSecretFile(secretPath)
if err != nil {
t.Fatalf("createSecretFile failed to reclaim stale lock: %v", err)
}
if len(secret) != secretBytes {
t.Fatalf("secret length = %d, want %d", len(secret), secretBytes)
}
// The lock file should be removed after completion
if _, err := os.Stat(lockPath); err == nil {
t.Fatal("expected lock file to be cleaned up after successful acquisition")
}
}