mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
* 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
141 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|