1// Package visualproc launches the separate visual process and speaks the tiny
2// private daemon<->visual pipe protocol. It never renders and never owns or
3// releases the session lock; the visual does, and reports the outcome.
4//
5// Wire format, both directions (fd 3: daemon->visual, fd 4: visual->daemon):
6//
7// "NAV1" | kind u8 | length u32 BE | payload
8//
9// Commands: 1 Fade {black u8, duration i64 ns BE}; 2 Lock {generation u64,
10// then three u32-length-prefixed byte strings: PINSource, PINReference,
11// EnvPIN}. Events: 3 LockConfirmed {generation u64}; 4 LockReleased
12// {generation u64}. Process exit is observed by the launcher, not sent.
13package visualproc
14
15import (
16 "encoding/binary"
17 "errors"
18 "io"
19 "time"
20
21 "github.com/bnema/neferafk/internal/ports"
22)
23
24var ErrProtocol = errors.New("visualproc: invalid frame")
25
26const (
27 kindFade byte = iota + 1
28 kindLock
29 kindConfirmed
30 kindReleased
31 maxPayload = 8 + 3*4 + 2*ports.AuthMaxMetadata + ports.AuthMaxSecret
32)
33
34func writeFrame(w io.Writer, kind byte, payload []byte) error {
35 buf := make([]byte, 9+len(payload))
36 defer clear(buf)
37 copy(buf, "NAV1")
38 buf[4] = kind
39 binary.BigEndian.PutUint32(buf[5:], uint32(len(payload)))
40 copy(buf[9:], payload)
41 for b := buf; len(b) > 0; {
42 n, err := w.Write(b)
43 if err != nil {
44 return err
45 }
46 b = b[n:]
47 }
48 return nil
49}
50
51func readFrame(r io.Reader) (byte, []byte, error) {
52 var h [9]byte
53 if _, err := io.ReadFull(r, h[:]); err != nil {
54 return 0, nil, err
55 }
56 n := binary.BigEndian.Uint32(h[5:])
57 if string(h[:4]) != "NAV1" || n > maxPayload {
58 return 0, nil, ErrProtocol
59 }
60 p := make([]byte, n)
61 if _, err := io.ReadFull(r, p); err != nil {
62 clear(p)
63 return 0, nil, err
64 }
65 return h[4], p, nil
66}
67
68// WriteCommand encodes a daemon->visual command. The caller wipes any secret
69// it owns; the encoded copy is wiped here.
70func WriteCommand(w io.Writer, cmd ports.VisualCommand) error {
71 switch c := cmd.(type) {
72 case ports.VisualFade:
73 p := make([]byte, 9)
74 if c.Black {
75 p[0] = 1
76 }
77 binary.BigEndian.PutUint64(p[1:], uint64(c.Duration))
78 return writeFrame(w, kindFade, p)
79 case ports.VisualLock:
80 a := c.Auth
81 fields := [][]byte{[]byte(a.PINSource), []byte(a.PINReference), a.EnvPIN}
82 if c.Generation == 0 || uint64(c.Generation) != a.Generation || len(fields[1]) > ports.AuthMaxMetadata || len(fields[2]) > ports.AuthMaxSecret || len(fields[0]) > 16 {
83 return ErrProtocol
84 }
85 p := binary.BigEndian.AppendUint64(nil, uint64(c.Generation))
86 for _, f := range fields {
87 p = binary.BigEndian.AppendUint32(p, uint32(len(f)))
88 p = append(p, f...)
89 }
90 defer clear(p)
91 return writeFrame(w, kindLock, p)
92 }
93 return ErrProtocol
94}
95
96// ReadCommand decodes a daemon->visual command (used by the visual process).
97// The returned VisualLock owns EnvPIN and its receiver must wipe it.
98func ReadCommand(r io.Reader) (ports.VisualCommand, error) {
99 kind, p, err := readFrame(r)
100 if err != nil {
101 return nil, err
102 }
103 defer clear(p)
104 switch kind {
105 case kindFade:
106 if len(p) != 9 || p[0] > 1 {
107 return nil, ErrProtocol
108 }
109 d := time.Duration(binary.BigEndian.Uint64(p[1:]))
110 if d < 0 || d > ports.MaxDuration {
111 return nil, ErrProtocol
112 }
113 return ports.VisualFade{Black: p[0] == 1, Duration: d}, nil
114 case kindLock:
115 if len(p) < 8 {
116 return nil, ErrProtocol
117 }
118 gen := binary.BigEndian.Uint64(p)
119 rest := p[8:]
120 var f [3][]byte
121 for i := range f {
122 if len(rest) < 4 {
123 return nil, ErrProtocol
124 }
125 n := binary.BigEndian.Uint32(rest)
126 if uint64(n) > uint64(len(rest)-4) {
127 return nil, ErrProtocol
128 }
129 f[i] = rest[4 : 4+n]
130 rest = rest[4+n:]
131 }
132 if gen == 0 || len(rest) != 0 || len(f[0]) > 16 || len(f[1]) > ports.AuthMaxMetadata || len(f[2]) > ports.AuthMaxSecret {
133 return nil, ErrProtocol
134 }
135 return ports.VisualLock{Generation: ports.Generation(gen), Auth: ports.AuthBootstrap{Generation: gen, PINSource: ports.PINSource(f[0]), PINReference: string(f[1]), EnvPIN: append([]byte(nil), f[2]...)}}, nil
136 }
137 return nil, ErrProtocol
138}
139
140// WriteEvent encodes a visual->daemon lifecycle event (used by the visual).
141func WriteEvent(w io.Writer, ev ports.VisualEvent) error {
142 switch e := ev.(type) {
143 case ports.LockConfirmed:
144 return writeFrame(w, kindConfirmed, binary.BigEndian.AppendUint64(nil, uint64(e.Generation)))
145 case ports.LockReleased:
146 return writeFrame(w, kindReleased, binary.BigEndian.AppendUint64(nil, uint64(e.Generation)))
147 }
148 return ErrProtocol
149}
150
151// ReadEvent decodes a visual->daemon event.
152func ReadEvent(r io.Reader) (ports.VisualEvent, error) {
153 kind, p, err := readFrame(r)
154 if err != nil {
155 return nil, err
156 }
157 if len(p) != 8 || binary.BigEndian.Uint64(p) == 0 {
158 return nil, ErrProtocol
159 }
160 gen := ports.Generation(binary.BigEndian.Uint64(p))
161 switch kind {
162 case kindConfirmed:
163 return ports.LockConfirmed{Generation: gen}, nil
164 case kindReleased:
165 return ports.LockReleased{Generation: gen}, nil
166 }
167 return nil, ErrProtocol
168}