implement auth, closed and close envelopes
This commit is contained in:
227
pkg/encoders/envelopes/authenvelope/authenvelope.go
Normal file
227
pkg/encoders/envelopes/authenvelope/authenvelope.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Package authenvelope defines the auth challenge (relay message) and response
|
||||
// (client message) of the NIP-42 authentication protocol.
|
||||
package authenvelope
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/errorf"
|
||||
"lol.mleku.dev/log"
|
||||
envs "next.orly.dev/pkg/encoders/envelopes"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
text2 "next.orly.dev/pkg/encoders/text"
|
||||
"next.orly.dev/pkg/interfaces/codec"
|
||||
)
|
||||
|
||||
// L is the label associated with this type of codec.Envelope.
|
||||
const L = "AUTH"
|
||||
|
||||
// Challenge is the relay-sent message containing a relay-chosen random string
|
||||
// to prevent replay attacks on NIP-42 authentication.
|
||||
type Challenge struct {
|
||||
Challenge []byte
|
||||
}
|
||||
|
||||
var _ codec.Envelope = (*Challenge)(nil)
|
||||
|
||||
// NewChallenge creates a new empty authenvelope.Challenge.
|
||||
func NewChallenge() *Challenge { return &Challenge{} }
|
||||
|
||||
// NewChallengeWith creates a new authenvelope.Challenge with provided bytes.
|
||||
func NewChallengeWith[V string | []byte](challenge V) *Challenge {
|
||||
return &Challenge{[]byte(challenge)}
|
||||
}
|
||||
|
||||
// Label returns the label of a authenvelope.Challenge.
|
||||
func (en *Challenge) Label() string { return L }
|
||||
|
||||
// Write encodes and writes the Challenge instance to the provided writer.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - w (io.Writer): The destination where the encoded data will be written.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - err (error): An error if writing to the writer fails.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// Encodes the Challenge instance into a byte slice using Marshal, logs the
|
||||
// encoded challenge, and writes it to the provided io.Writer.
|
||||
func (en *Challenge) Write(w io.Writer) (err error) {
|
||||
var b []byte
|
||||
b = en.Marshal(b)
|
||||
log.T.F("writing out challenge envelope: '%s'", b)
|
||||
_, err = w.Write(b)
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal encodes the Challenge instance into a byte slice, formatting it as
|
||||
// a JSON-like structure with a specific label and escaping rules applied to
|
||||
// its content.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - dst ([]byte): The destination buffer where the encoded data will be written.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - b ([]byte): The byte slice containing the encoded Challenge data.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// - Prepares the destination buffer and applies a label to it.
|
||||
//
|
||||
// - Escapes the challenge content according to Nostr-specific rules before
|
||||
// appending it to the output.
|
||||
//
|
||||
// - Returns the resulting byte slice with the complete encoded structure.
|
||||
func (en *Challenge) Marshal(dst []byte) (b []byte) {
|
||||
b = dst
|
||||
var err error
|
||||
b = envs.Marshal(
|
||||
b, L,
|
||||
func(bst []byte) (o []byte) {
|
||||
o = bst
|
||||
o = append(o, '"')
|
||||
o = text2.NostrEscape(o, en.Challenge)
|
||||
o = append(o, '"')
|
||||
return
|
||||
},
|
||||
)
|
||||
_ = err
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal parses the provided byte slice and extracts the challenge value,
|
||||
// leaving any remaining bytes after parsing.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - b ([]byte): The byte slice containing the encoded challenge data.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - r ([]byte): Any remaining bytes after parsing the challenge.
|
||||
//
|
||||
// - err (error): An error if parsing fails.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// - Extracts the quoted challenge string from the input byte slice.
|
||||
//
|
||||
// - Trims any trailing characters following the closing quote.
|
||||
func (en *Challenge) Unmarshal(b []byte) (r []byte, err error) {
|
||||
r = b
|
||||
if en.Challenge, r, err = text2.UnmarshalQuoted(r); chk.E(err) {
|
||||
return
|
||||
}
|
||||
for ; len(r) >= 0; r = r[1:] {
|
||||
if r[0] == ']' {
|
||||
r = r[:0]
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseChallenge parses the provided byte slice into a new Challenge instance,
|
||||
// extracting the challenge value and returning any remaining bytes after parsing.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - b ([]byte): The byte slice containing the encoded challenge data.
|
||||
//
|
||||
// # Return Values
|
||||
//
|
||||
// - t (*Challenge): A pointer to the newly created and populated Challenge
|
||||
// instance.
|
||||
//
|
||||
// - rem ([]byte): Any remaining bytes in the input slice after parsing.
|
||||
//
|
||||
// - err (error): An error if parsing fails.
|
||||
//
|
||||
// # Expected behaviour
|
||||
//
|
||||
// Parses the byte slice into a new Challenge instance using Unmarshal,
|
||||
// returning any remaining bytes and an error if parsing fails.
|
||||
func ParseChallenge(b []byte) (t *Challenge, rem []byte, err error) {
|
||||
t = NewChallenge()
|
||||
if rem, err = t.Unmarshal(b); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Response is a client-side envelope containing the signed event bearing the
|
||||
// relay's URL and Challenge string.
|
||||
type Response struct {
|
||||
Event *event.E
|
||||
}
|
||||
|
||||
var _ codec.Envelope = (*Response)(nil)
|
||||
|
||||
// NewResponse creates a new empty Response.
|
||||
func NewResponse() *Response { return &Response{} }
|
||||
|
||||
// NewResponseWith creates a new Response with a provided event.E.
|
||||
func NewResponseWith(event *event.E) *Response { return &Response{Event: event} }
|
||||
|
||||
// Label returns the label of a auth Response envelope.
|
||||
func (en *Response) Label() string { return L }
|
||||
|
||||
func (en *Response) Id() []byte { return en.Event.ID }
|
||||
|
||||
// Write the Response to a provided io.Writer.
|
||||
func (en *Response) Write(w io.Writer) (err error) {
|
||||
var b []byte
|
||||
b = en.Marshal(b)
|
||||
_, err = w.Write(b)
|
||||
return
|
||||
}
|
||||
|
||||
// Marshal a Response to minified JSON, appending to a provided destination
|
||||
// slice. Note that this ensures correct string escaping on the challenge field.
|
||||
func (en *Response) Marshal(dst []byte) (b []byte) {
|
||||
var err error
|
||||
if en == nil {
|
||||
err = errorf.E("nil response")
|
||||
return
|
||||
}
|
||||
if en.Event == nil {
|
||||
err = errorf.E("nil event in response")
|
||||
return
|
||||
}
|
||||
b = dst
|
||||
b = envs.Marshal(b, L, en.Event.Marshal)
|
||||
_ = err
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal a Response from minified JSON, returning the remainder after the en
|
||||
// of the envelope. Note that this ensures the challenge string was correctly
|
||||
// escaped by NIP-01 escaping rules.
|
||||
func (en *Response) Unmarshal(b []byte) (r []byte, err error) {
|
||||
r = b
|
||||
// literally just unmarshal the event
|
||||
en.Event = event.New()
|
||||
if r, err = en.Event.Unmarshal(r); chk.E(err) {
|
||||
return
|
||||
}
|
||||
if r, err = envs.SkipToTheEnd(r); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseResponse reads a Response encoded in minified JSON and unpacks it to
|
||||
// the runtime format.
|
||||
func ParseResponse(b []byte) (t *Response, rem []byte, err error) {
|
||||
t = NewResponse()
|
||||
if rem, err = t.Unmarshal(b); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
86
pkg/encoders/envelopes/authenvelope/authenvelope_test.go
Normal file
86
pkg/encoders/envelopes/authenvelope/authenvelope_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package authenvelope
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"lol.mleku.dev/chk"
|
||||
"next.orly.dev/pkg/crypto/p256k"
|
||||
"next.orly.dev/pkg/encoders/envelopes"
|
||||
"next.orly.dev/pkg/protocol/auth"
|
||||
"next.orly.dev/pkg/utils"
|
||||
"next.orly.dev/pkg/utils/bufpool"
|
||||
)
|
||||
|
||||
const relayURL = "wss://example.com"
|
||||
|
||||
func TestAuth(t *testing.T) {
|
||||
var err error
|
||||
signer := new(p256k.Signer)
|
||||
if err = signer.Generate(); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _ = range 1000 {
|
||||
b1, b2, b3, b4 := bufpool.Get(), bufpool.Get(), bufpool.Get(), bufpool.Get()
|
||||
ch := auth.GenerateChallenge()
|
||||
chal := Challenge{Challenge: ch}
|
||||
b1 = chal.Marshal(b1)
|
||||
oChal := make([]byte, len(b1))
|
||||
copy(oChal, b1)
|
||||
var rem []byte
|
||||
var l string
|
||||
if l, b1, err = envelopes.Identify(b1); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if l != L {
|
||||
t.Fatalf("invalid sentinel %s, expect %s", l, L)
|
||||
}
|
||||
c2 := NewChallenge()
|
||||
if rem, err = c2.Unmarshal(b1); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rem) != 0 {
|
||||
t.Fatalf("remainder should be empty\n%s", rem)
|
||||
}
|
||||
if !utils.FastEqual(chal.Challenge, c2.Challenge) {
|
||||
t.Fatalf(
|
||||
"challenge mismatch\n%s\n%s",
|
||||
chal.Challenge, c2.Challenge,
|
||||
)
|
||||
}
|
||||
b2 = c2.Marshal(b2)
|
||||
if !utils.FastEqual(oChal, b2) {
|
||||
t.Fatalf("challenge mismatch\n%s\n%s", oChal, b2)
|
||||
}
|
||||
resp := Response{
|
||||
Event: auth.CreateUnsigned(
|
||||
signer.Pub(), ch,
|
||||
relayURL,
|
||||
),
|
||||
}
|
||||
if err = resp.Event.Sign(signer); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b3 = resp.Marshal(b3)
|
||||
oResp := make([]byte, len(b3))
|
||||
copy(oResp, b3)
|
||||
if l, b3, err = envelopes.Identify(b3); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if l != L {
|
||||
t.Fatalf("invalid sentinel %s, expect %s", l, L)
|
||||
}
|
||||
r2 := NewResponse()
|
||||
if _, err = r2.Unmarshal(b3); chk.E(err) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b4 = r2.Marshal(b4)
|
||||
if !utils.FastEqual(oResp, b4) {
|
||||
t.Fatalf("challenge mismatch\n%s\n%s", oResp, b4)
|
||||
}
|
||||
bufpool.Put(b1)
|
||||
bufpool.Put(b2)
|
||||
bufpool.Put(b3)
|
||||
bufpool.Put(b4)
|
||||
oChal, oResp = oChal[:0], oResp[:0]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user