1package auth
2
3import (
4 "encoding/binary"
5 "errors"
6 "io"
7
8 "github.com/bnema/neferafk/internal/ports"
9)
10
11var ErrProtocol = errors.New("invalid authentication frame")
12
13const frameHeader = 22 // magic, kind, generic code, generation, attempt
14
15func payloadLimit(kind ports.AuthKind) int {
16 if kind == ports.AuthAttempt || kind == ports.AuthSecret {
17 return ports.AuthMaxSecret
18 }
19 return ports.AuthMaxMetadata
20}
21
22func validRole(f ports.AuthFrame, n int) bool {
23 if f.Generation == 0 || n < 0 || n > payloadLimit(f.Kind) {
24 return false
25 }
26 if f.Kind == ports.AuthReady {
27 if f.Attempt != 0 {
28 return false
29 }
30 switch f.Code {
31 case ports.AuthPassword, ports.AuthPIN, ports.AuthFallbackPassword:
32 return n == 0
33 case ports.AuthUnavailable:
34 return n == 0 || n == 1
35 default:
36 return false
37 }
38 }
39 if f.Attempt == 0 {
40 return false
41 }
42 switch f.Kind {
43 case ports.AuthAttempt, ports.AuthSecret:
44 return f.Code == 0
45 case ports.AuthPrompt, ports.AuthStatus:
46 return f.Code == ports.AuthPassword && n == 0
47 case ports.AuthResult:
48 return n == 0 && (f.Code == ports.AuthSuccess || f.Code == ports.AuthDenied || f.Code == ports.AuthUnavailable || f.Code == ports.AuthInvalid)
49 case ports.AuthCancel:
50 return f.Code == ports.AuthInvalid && n == 0
51 default:
52 return false
53 }
54}
55
56func validFrame(f ports.AuthFrame) bool {
57 if !validRole(f, len(f.Payload)) {
58 return false
59 }
60 return len(f.Payload) != 1 || f.Kind != ports.AuthReady || f.Payload[0] == byte(ports.AuthFallbackToPassword)
61}
62
63// ReadFrame validates the fixed header before allocating any bounded payload.
64// Partial reads wipe the allocated payload; no error includes input bytes.
65func ReadFrame(r io.Reader) (ports.AuthFrame, error) {
66 var prefix [4]byte
67 if _, err := io.ReadFull(r, prefix[:]); err != nil {
68 return ports.AuthFrame{}, err
69 }
70 n := int(binary.BigEndian.Uint32(prefix[:]))
71 if n < frameHeader || n > frameHeader+ports.AuthMaxMetadata {
72 return ports.AuthFrame{}, ErrProtocol
73 }
74 var h [frameHeader]byte
75 if _, err := io.ReadFull(r, h[:]); err != nil {
76 return ports.AuthFrame{}, err
77 }
78 f := ports.AuthFrame{Kind: ports.AuthKind(h[4]), Code: ports.AuthCode(h[5]), Generation: binary.BigEndian.Uint64(h[6:14]), Attempt: binary.BigEndian.Uint64(h[14:22])}
79 if string(h[:4]) != "NAW1" || !validRole(f, n-frameHeader) {
80 return ports.AuthFrame{}, ErrProtocol
81 }
82 f.Payload = make([]byte, n-frameHeader)
83 if _, err := io.ReadFull(r, f.Payload); err != nil {
84 clear(f.Payload)
85 return ports.AuthFrame{}, err
86 }
87 if !validFrame(f) {
88 clear(f.Payload)
89 return ports.AuthFrame{}, ErrProtocol
90 }
91 return f, nil
92}
93
94func WriteFrame(w io.Writer, f ports.AuthFrame) error {
95 if !validFrame(f) {
96 return ErrProtocol
97 }
98 var h [4 + frameHeader]byte
99 binary.BigEndian.PutUint32(h[:4], uint32(frameHeader+len(f.Payload)))
100 copy(h[4:8], "NAW1")
101 h[8], h[9] = byte(f.Kind), byte(f.Code)
102 binary.BigEndian.PutUint64(h[10:18], f.Generation)
103 binary.BigEndian.PutUint64(h[18:26], f.Attempt)
104 if err := writeAll(w, h[:]); err != nil {
105 return err
106 }
107 return writeAll(w, f.Payload)
108}
109
110func writeAll(w io.Writer, b []byte) error {
111 for len(b) != 0 {
112 n, err := w.Write(b)
113 if n < 0 || n > len(b) {
114 return ErrProtocol
115 }
116 b = b[n:]
117 if err != nil {
118 return err
119 }
120 if n == 0 {
121 return io.ErrShortWrite
122 }
123 }
124 return nil
125}