arblant
firefox-vpn-client / core / internal / securefile / securefile.go
securefile.go94 lines · 2.4 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
// Package securefile writes files that hold secrets or personal data.
package securefile

import (
	"os"
	"path/filepath"
	"runtime"
	"time"
)

// WritePrivate replaces path with data through a temporary file in the same
// directory, readable and writable only by the owner (0600 on POSIX; Windows
// keeps the default ACL of the user's profile directory). The data reaches
// the disk before the rename (and the rename before it returns, on POSIX),
// so a crash or a power cut leaves either the old file or the new one, never
// an empty one. The old file is never deleted first: when Windows refuses
// the rename, the old file stays and an error is returned.
func WritePrivate(path string, data []byte) error {
	dir := filepath.Dir(path)
	tmp, err := os.CreateTemp(dir, filepath.Base(path)+".tmp-*")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer os.Remove(tmpPath)

	if err := tmp.Chmod(0600); err != nil {
		_ = tmp.Close()
		return err
	}
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		_ = tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}

	if err := renameReplacing(tmpPath, path); err != nil {
		return err
	}
	syncDir(dir)
	return os.Chmod(path, 0600)
}

// renameAttempts and renameFirstWait bound the retries of a refused rename.
const (
	renameAttempts  = 6
	renameFirstWait = 20 * time.Millisecond
)

// renameReplacing renames from over to. Windows refuses while another
// program, typically an antivirus or indexer scan of the new file, holds
// either file open; that lasts moments, so the rename is tried again with a
// growing wait (about 0.6 s in all).
func renameReplacing(from, to string) error {
	wait := renameFirstWait
	var err error
	for attempt := 0; attempt < renameAttempts; attempt++ {
		if err = os.Rename(from, to); err == nil {
			return nil
		}
		if runtime.GOOS != "windows" {
			return err
		}
		time.Sleep(wait)
		wait *= 2
	}
	return err
}

// syncDir makes a rename in dir durable on POSIX; Windows has no directory
// sync and needs none for NTFS's journaled renames.
func syncDir(dir string) {
	if runtime.GOOS == "windows" {
		return
	}
	if f, err := os.Open(dir); err == nil {
		_ = f.Sync()
		_ = f.Close()
	}
}

// EnsurePrivateDir creates dir and its parents with owner-only permissions.
func EnsurePrivateDir(dir string) error {
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return err
	}
	return os.Chmod(dir, 0o700)
}