authenvelope.go raw

   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