1 // Package authenvelope defines the auth challenge (relay message) and response
2 // (client message) of the NIP-42 authentication protocol.
3 package authenvelope
4 5 import (
6 "io"
7 8 "next.orly.dev/pkg/nostr/encoders/envelopes"
9 "next.orly.dev/pkg/nostr/encoders/event"
10 "next.orly.dev/pkg/nostr/encoders/text"
11 "next.orly.dev/pkg/nostr/interfaces/codec"
12 "next.orly.dev/pkg/nostr/utils/constraints"
13 "next.orly.dev/pkg/nostr/utils/units"
14 "next.orly.dev/pkg/lol/chk"
15 "next.orly.dev/pkg/lol/errorf"
16 )
17 18 // L is the label associated with this type of codec.Envelope.
19 const L = "AUTH"
20 21 // Challenge is the relay-sent message containing a relay-chosen random string
22 // to prevent replay attacks on NIP-42 authentication.
23 type Challenge struct {
24 Challenge []byte
25 }
26 27 var _ codec.Envelope = (*Challenge)(nil)
28 29 // NewChallenge creates a new empty authenvelope.Challenge.
30 func NewChallenge() *Challenge { return &Challenge{} }
31 32 // NewChallengeWith creates a new authenvelope.Challenge with provided bytes.
33 func NewChallengeWith[V constraints.Bytes](challenge V) *Challenge {
34 return &Challenge{[]byte(challenge)}
35 }
36 37 // Label returns the label of a authenvelope.Challenge.
38 func (en *Challenge) Label() string { return L }
39 40 // Write encodes and writes the Challenge instance to the provided writer.
41 //
42 // # Parameters
43 //
44 // - w (io.Writer): The destination where the encoded data will be written.
45 //
46 // # Return Values
47 //
48 // - err (error): An error if writing to the writer fails.
49 //
50 // # Expected behaviour
51 //
52 // Encodes the Challenge instance into a byte slice using Marshal, logs the
53 // encoded challenge, and writes it to the provided io.Writer.
54 func (en *Challenge) Write(w io.Writer) (err error) {
55 var b []byte
56 b = en.Marshal(b)
57 // log.D.F("writing out challenge envelope: '%s'", b)
58 _, err = w.Write(b)
59 return
60 }
61 62 // Marshal encodes the Challenge instance into a byte slice, formatting it as
63 // a JSON-like structure with a specific label and escaping rules applied to
64 // its content.
65 //
66 // # Parameters
67 //
68 // - dst ([]byte): The destination buffer where the encoded data will be written.
69 //
70 // # Return Values
71 //
72 // - b ([]byte): The byte slice containing the encoded Challenge data.
73 //
74 // # Expected behaviour
75 //
76 // - Prepares the destination buffer and applies a label to it.
77 //
78 // - Escapes the challenge content according to Nostr-specific rules before
79 // appending it to the output.
80 //
81 // - Returns the resulting byte slice with the complete encoded structure.
82 func (en *Challenge) Marshal(dst []byte) (b []byte) {
83 b = dst
84 var err error
85 b = envelopes.Marshal(
86 b, L,
87 func(bst []byte) (o []byte) {
88 o = bst
89 o = append(o, '"')
90 o = text.NostrEscape(o, en.Challenge)
91 o = append(o, '"')
92 return
93 },
94 )
95 _ = err
96 return
97 }
98 99 // Unmarshal parses the provided byte slice and extracts the challenge value,
100 // leaving any remaining bytes after parsing.
101 //
102 // # Parameters
103 //
104 // - b ([]byte): The byte slice containing the encoded challenge data.
105 //
106 // # Return Values
107 //
108 // - r ([]byte): Any remaining bytes after parsing the challenge.
109 //
110 // - err (error): An error if parsing fails.
111 //
112 // # Expected behaviour
113 //
114 // - Extracts the quoted challenge string from the input byte slice.
115 //
116 // - Trims any trailing characters following the closing quote.
117 func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
118 r = b
119 if en.Challenge, r, err = text.UnmarshalQuoted(r); chk.E(err) {
120 return
121 }
122 for ; len(r) >= 0; r = r[1:] {
123 if r[0] == ']' {
124 r = r[:0]
125 return
126 }
127 }
128 return
129 }
130 131 // ParseChallenge parses the provided byte slice into a new Challenge instance,
132 // extracting the challenge value and returning any remaining bytes after parsing.
133 //
134 // # Parameters
135 //
136 // - b ([]byte): The byte slice containing the encoded challenge data.
137 //
138 // # Return Values
139 //
140 // - t (*Challenge): A pointer to the newly created and populated Challenge
141 // instance.
142 //
143 // - rem ([]byte): Any remaining bytes in the input slice after parsing.
144 //
145 // - err (error): An error if parsing fails.
146 //
147 // # Expected behaviour
148 //
149 // Parses the byte slice into a new Challenge instance using Unmarshal,
150 // returning any remaining bytes and an error if parsing fails.
151 func ParseChallenge(b []byte) (t *Challenge, rem []byte, err error) {
152 t = NewChallenge()
153 if rem, err = t.Unmarshal(b); chk.E(err) {
154 return
155 }
156 return
157 }
158 159 // Response is a client-side envelope containing the signed event bearing the
160 // relay's URL and Challenge string.
161 type Response struct {
162 Event *event.E
163 }
164 165 var _ codec.Envelope = (*Response)(nil)
166 167 // NewResponse creates a new empty Response.
168 func NewResponse() *Response { return &Response{} }
169 170 // NewResponseWith creates a new Response with a provided event.E.
171 func NewResponseWith(event *event.E) *Response { return &Response{Event: event} }
172 173 // Label returns the label of a auth Response envelope.
174 func (en *Response) Label() string { return L }
175 176 func (en *Response) Id() []byte { return en.Event.ID }
177 178 // Write the Response to a provided io.Writer.
179 func (en *Response) Write(w io.Writer) (err error) {
180 var b []byte
181 b = en.Marshal(b)
182 _, err = w.Write(b)
183 return
184 }
185 186 // Marshal a Response to minified JSON, appending to a provided destination
187 // slice. Note that this ensures correct string escaping on the challenge field.
188 func (en *Response) Marshal(dst []byte) (b []byte) {
189 var err error
190 if en == nil {
191 err = errorf.E("nil response")
192 return
193 }
194 if en.Event == nil {
195 err = errorf.E("nil event in response")
196 return
197 }
198 // if the destination capacity is not large enough, allocate a new
199 // destination slice.
200 if en.Event.EstimateSize() >= cap(dst) {
201 dst = make([]byte, 0, en.Event.EstimateSize()+units.Kb)
202 }
203 b = dst
204 b = envelopes.Marshal(b, L, en.Event.Marshal)
205 _ = err
206 return
207 }
208 209 // Unmarshal a Response from minified JSON, returning the remainder after the en
210 // of the envelope. Note that this ensures the challenge string was correctly
211 // escaped by NIP-01 escaping rules.
212 func (en *Response) Unmarshal(b []byte) (r []byte, err error) {
213 r = b
214 // literally just unmarshal the event
215 en.Event = event.New()
216 if r, err = en.Event.Unmarshal(r); chk.E(err) {
217 return
218 }
219 if r, err = envelopes.SkipToTheEnd(r); chk.E(err) {
220 return
221 }
222 return
223 }
224 225 // ParseResponse reads a Response encoded in minified JSON and unpacks it to
226 // the runtime format.
227 func ParseResponse(b []byte) (t *Response, rem []byte, err error) {
228 t = NewResponse()
229 if rem, err = t.Unmarshal(b); chk.E(err) {
230 return
231 }
232 return
233 }
234