1// Package daemonidentity persists the stable authenticated identity of the
2// per-user daemon. Only daemon-owned startup may create it; readers never do.
3package daemonidentity
4
5import (
6 "crypto/rand"
7 "encoding/hex"
8 "errors"
9 "fmt"
10 "io"
11 "os"
12 "path/filepath"
13 "syscall"
14
15 "github.com/bnema/vev/internal/ports"
16 "github.com/bnema/vev/pkg/safedir"
17)
18
19const (
20 DirName = "daemon"
21 FileName = "identity"
22 encodedSize = 32
23)
24
25var ErrInvalid = errors.New("daemonidentity: invalid identity file")
26
27func Path(stateDir string) string { return filepath.Join(stateDir, DirName, FileName) }
28
29// Load reads an existing identity without creating any filesystem object.
30func Load(stateDir string) (ports.BrokerDaemonIdentity, error) {
31 path := Path(stateDir)
32 info, err := os.Lstat(path)
33 if err != nil {
34 return "", err
35 }
36 if !info.Mode().IsRegular() || info.Mode().Perm() != 0o600 {
37 return "", ErrInvalid
38 }
39 st, ok := info.Sys().(*syscall.Stat_t)
40 if !ok || int(st.Uid) != os.Getuid() {
41 return "", ErrInvalid
42 }
43 f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW, 0)
44 if err != nil {
45 return "", fmt.Errorf("%w: %v", ErrInvalid, err)
46 }
47 defer f.Close()
48 data, err := io.ReadAll(io.LimitReader(f, encodedSize+1))
49 if err != nil || len(data) != encodedSize {
50 return "", ErrInvalid
51 }
52 decoded := make([]byte, 16)
53 if _, err := hex.Decode(decoded, data); err != nil {
54 return "", ErrInvalid
55 }
56 id := ports.BrokerDaemonIdentity(string(data))
57 if err := id.Validate(); err != nil {
58 return "", ErrInvalid
59 }
60 return id, nil
61}
62
63// LoadOrCreate is daemon-owner-only creation. It writes a fresh random value
64// through an owner-only temporary file and atomically publishes it.
65func LoadOrCreate(stateDir string) (ports.BrokerDaemonIdentity, error) {
66 id, err := Load(stateDir)
67 if err == nil {
68 return id, nil
69 }
70 if !errors.Is(err, os.ErrNotExist) {
71 return "", err
72 }
73 dir := filepath.Dir(Path(stateDir))
74 if err := safedir.EnsurePrivate(dir); err != nil {
75 return "", err
76 }
77 var raw [16]byte
78 if _, err := io.ReadFull(rand.Reader, raw[:]); err != nil {
79 return "", err
80 }
81 data := []byte(hex.EncodeToString(raw[:]))
82 tmp, err := os.CreateTemp(dir, ".identity-")
83 if err != nil {
84 return "", err
85 }
86 tmpName := tmp.Name()
87 defer os.Remove(tmpName)
88 if err = tmp.Chmod(0o600); err == nil {
89 _, err = tmp.Write(data)
90 }
91 if err == nil {
92 err = tmp.Sync()
93 }
94 if closeErr := tmp.Close(); err == nil {
95 err = closeErr
96 }
97 if err != nil {
98 return "", err
99 }
100 if err := os.Rename(tmpName, Path(stateDir)); err != nil {
101 return "", err
102 }
103 d, err := os.Open(dir)
104 if err == nil {
105 err = d.Sync()
106 _ = d.Close()
107 }
108 if err != nil {
109 return "", err
110 }
111 return Load(stateDir)
112}
113
114func NewIncarnation() (ports.BrokerDaemonIncarnation, error) {
115 var id ports.BrokerDaemonIncarnation
116 _, err := io.ReadFull(rand.Reader, id[:])
117 if err == nil && id.IsZero() {
118 return NewIncarnation()
119 }
120 return id, err
121}