1package auth
2
3import (
4 "bytes"
5 "errors"
6 "io"
7 "testing"
8 "time"
9
10 "github.com/bnema/neferafk/internal/ports"
11 pam "github.com/bnema/purego-pam"
12)
13
14func TestFallbackReadyIsExplicitEvenWithoutPAM(t *testing.T) {
15 for _, available := range []bool{false, true} {
16 var output bytes.Buffer
17 var backend pamBackend
18 if available {
19 backend = newMockpamBackend(t)
20 }
21 if err := runScoped(bytes.NewReader(nil), &output, 3, nil, backend, "user", true); !errors.Is(err, io.EOF) {
22 t.Fatal(err)
23 }
24 f, err := ReadFrame(&output)
25 if err != nil {
26 t.Fatal(err)
27 }
28 if available {
29 if f.Code != ports.AuthFallbackPassword || len(f.Payload) != 0 {
30 t.Fatalf("fallback ready %+v", f)
31 }
32 } else if f.Code != ports.AuthUnavailable || !bytes.Equal(f.Payload, []byte{byte(ports.AuthFallbackToPassword)}) {
33 t.Fatalf("fallback unavailable %+v", f)
34 }
35 }
36}
37
38func TestEchoOnNeverReceivesPresubmittedCredential(t *testing.T) {
39 var input, output bytes.Buffer
40 if err := WriteFrame(&input, ports.AuthFrame{Kind: ports.AuthSecret, Generation: 3, Attempt: 9, Payload: []byte("explicit-response")}); err != nil {
41 t.Fatal(err)
42 }
43 w := &worker{r: &input, w: &output, generation: 3, attempt: 9, deadline: time.Now().Add(time.Second), first: true, secretLen: 8, secret: make([]byte, 512)}
44 copy(w.secret, "password")
45 answer := make([]byte, 512)
46 n, err := w.Respond(pam.Message{Style: pam.PromptEchoOn}, answer)
47 if err != nil || string(answer[:n]) != "explicit-response" || !w.first {
48 t.Fatal("echo-on leaked or consumed initial password")
49 }
50 clear(answer)
51 n, err = w.Respond(pam.Message{Style: pam.PromptEchoOff}, answer)
52 if err != nil || string(answer[:n]) != "password" || w.first || !bytes.Equal(w.secret, make([]byte, 512)) {
53 t.Fatal("echo-off did not consume and wipe initial credential")
54 }
55 f, err := ReadFrame(&output)
56 if err != nil || f.Kind != ports.AuthPrompt || len(f.Payload) != 0 {
57 t.Fatal("echo-on did not emit generic prompt")
58 }
59}
60
61func TestCodecRejectsContradictoryRoles(t *testing.T) {
62 frames := []ports.AuthFrame{
63 {Kind: ports.AuthReady, Generation: 1},
64 {Kind: ports.AuthResult, Generation: 1, Attempt: 1},
65 {Kind: ports.AuthStatus, Generation: 1, Attempt: 1},
66 {Kind: ports.AuthResult, Code: ports.AuthPIN, Generation: 1, Attempt: 1},
67 {Kind: ports.AuthResult, Code: ports.AuthSuccess, Generation: 1, Attempt: 1, Payload: []byte("raw")},
68 {Kind: ports.AuthReady, Code: ports.AuthUnavailable, Generation: 1, Payload: []byte{99}},
69 {Kind: ports.AuthReady, Code: ports.AuthPassword, Generation: 1, Attempt: 1},
70 {Kind: ports.AuthAttempt, Code: ports.AuthSuccess, Generation: 1, Attempt: 1},
71 {Kind: ports.AuthSecret, Generation: 1},
72 }
73 for _, f := range frames {
74 if err := WriteFrame(io.Discard, f); !errors.Is(err, ErrProtocol) {
75 t.Fatalf("contradictory role accepted: kind=%d code=%d", f.Kind, f.Code)
76 }
77 }
78}
79
80func TestStaleGenerationSecretNeverProducesResult(t *testing.T) {
81 var input, output bytes.Buffer
82 f := ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 2, Attempt: 1, Payload: []byte("synthetic")}
83 if err := WriteFrame(&input, f); err != nil {
84 t.Fatal(err)
85 }
86 if err := run(&input, &output, 3, nil, nil, ""); !errors.Is(err, ErrProtocol) {
87 t.Fatalf("stale generation accepted: %v", err)
88 }
89 ready, err := ReadFrame(&output)
90 if err != nil || ready.Kind != ports.AuthReady {
91 t.Fatal("missing readiness")
92 }
93 if _, err := ReadFrame(&output); !errors.Is(err, io.EOF) {
94 t.Fatal("stale secret produced result")
95 }
96}
97
98func TestDedicatedSecretPageCapacityAndRetirement(t *testing.T) {
99 for _, size := range []int{32, 512} {
100 b, err := newSecret(size)
101 if err != nil {
102 t.Fatal(err)
103 }
104 if len(b.data) != size || cap(b.data) != size || len(b.page) < size {
105 t.Fatal("secret capacity not bounded")
106 }
107 copy(b.data, "synthetic")
108 b.close()
109 if b.data != nil || b.page != nil {
110 t.Fatal("retired native memory still accessible")
111 }
112 b.close()
113 }
114}