23 changed files with 1186 additions and 44 deletions
@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
|
||||
package app |
||||
|
||||
func (s *Server) HandleMessage() { |
||||
import ( |
||||
"lol.mleku.dev/log" |
||||
) |
||||
|
||||
func (s *Server) HandleMessage(msg []byte) { |
||||
log.I.F("received message:\n%s\n", msg) |
||||
} |
||||
|
||||
@ -0,0 +1,227 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,86 @@
@@ -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] |
||||
} |
||||
} |
||||
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
// Package closedenvelope defines the nostr message type CLOSED which is sent
|
||||
// from a relay to indicate the relay-side termination of a subscription or the
|
||||
// demand for authentication associated with a subscription.
|
||||
package closedenvelope |
||||
|
||||
import ( |
||||
"io" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"next.orly.dev/pkg/encoders/envelopes" |
||||
"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 = "CLOSED" |
||||
|
||||
// T is a CLOSED envelope, which is a signal that a subscription has been
|
||||
// stopped on the relay side for some reason. Primarily this is for auth and can
|
||||
// be for other things like rate limiting.
|
||||
type T struct { |
||||
Subscription []byte |
||||
Reason []byte |
||||
} |
||||
|
||||
var _ codec.Envelope = (*T)(nil) |
||||
|
||||
// New creates an empty new T.
|
||||
func New() *T { |
||||
return new(T) |
||||
} |
||||
|
||||
// NewFrom creates a new closedenvelope.T populated with subscription ID and Reason.
|
||||
func NewFrom(id, msg []byte) *T { |
||||
return &T{ |
||||
Subscription: id, Reason: msg, |
||||
} |
||||
} |
||||
|
||||
// Label returns the label of a closedenvelope.T.
|
||||
func (en *T) Label() string { return L } |
||||
|
||||
// ReasonString returns the Reason in the form of a string.
|
||||
func (en *T) ReasonString() string { return string(en.Reason) } |
||||
|
||||
// Write the closedenvelope.T to a provided io.Writer.
|
||||
func (en *T) Write(w io.Writer) (err error) { |
||||
var b []byte |
||||
b = en.Marshal(b) |
||||
_, err = w.Write(b) |
||||
return |
||||
} |
||||
|
||||
// Marshal a closedenvelope.T envelope in minified JSON, appending to a provided
|
||||
// destination slice. Note that this ensures correct string escaping on the
|
||||
// Reason field.
|
||||
func (en *T) Marshal(dst []byte) (b []byte) { |
||||
b = dst |
||||
b = envelopes.Marshal( |
||||
b, L, |
||||
func(bst []byte) (o []byte) { |
||||
o = bst |
||||
o = append(o, '"') |
||||
o = append(o, en.Subscription...) |
||||
o = append(o, '"') |
||||
o = append(o, ',') |
||||
o = append(o, '"') |
||||
o = text.NostrEscape(o, en.Reason) |
||||
o = append(o, '"') |
||||
return |
||||
}, |
||||
) |
||||
return |
||||
} |
||||
|
||||
// Unmarshal a closedenvelope.T from minified JSON, returning the remainder after the end
|
||||
// of the envelope. Note that this ensures the Reason string is correctly
|
||||
// unescaped by NIP-01 escaping rules.
|
||||
func (en *T) Unmarshal(b []byte) (r []byte, err error) { |
||||
r = b |
||||
if en.Subscription, r, err = text.UnmarshalQuoted(r); chk.E(err) { |
||||
return |
||||
} |
||||
if en.Reason, r, err = text.UnmarshalQuoted(r); chk.E(err) { |
||||
return |
||||
} |
||||
if r, err = envelopes.SkipToTheEnd(r); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Parse reads a closedenvelope.T from minified JSON into a newly allocated closedenvelope.T.
|
||||
func Parse(b []byte) (t *T, rem []byte, err error) { |
||||
t = New() |
||||
if rem, err = t.Unmarshal(b); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
package closedenvelope |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
"testing" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"next.orly.dev/pkg/encoders/envelopes" |
||||
"next.orly.dev/pkg/utils" |
||||
"next.orly.dev/pkg/utils/bufpool" |
||||
|
||||
"lukechampine.com/frand" |
||||
) |
||||
|
||||
var messages = [][]byte{ |
||||
[]byte(""), |
||||
[]byte("pow: difficulty 25>=24"), |
||||
[]byte("duplicate: already have this event"), |
||||
[]byte("blocked: you are banned from posting here"), |
||||
[]byte("blocked: please register your pubkey at https://my-expensive-realy.example.com"), |
||||
[]byte("rate-limited: slow down there chief"), |
||||
[]byte("invalid: event creation date is too far off from the current time"), |
||||
[]byte("pow: difficulty 26 is less than 30"), |
||||
[]byte("error: could not connect to the database"), |
||||
} |
||||
|
||||
func RandomMessage() []byte { |
||||
return messages[frand.Intn(len(messages)-1)] |
||||
} |
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) { |
||||
var err error |
||||
for _ = range 1000 { |
||||
rb, rb1, rb2 := bufpool.Get(), bufpool.Get(), bufpool.Get() |
||||
s := []byte(fmt.Sprintf("sub:%d", frand.Intn(math.MaxInt64))) |
||||
req := NewFrom(s, RandomMessage()) |
||||
rb = req.Marshal(rb) |
||||
rb1 = append(rb1, rb...) |
||||
var rem []byte |
||||
var l string |
||||
if l, rb, err = envelopes.Identify(rb); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
if l != L { |
||||
t.Fatalf("invalid sentinel %s, expect %s", l, L) |
||||
} |
||||
req2 := New() |
||||
if rem, err = req2.Unmarshal(rb); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
// log.I.Ln(req2.ID)
|
||||
if len(rem) > 0 { |
||||
t.Fatalf( |
||||
"unmarshal failed, remainder\n%d %s", |
||||
len(rem), rem, |
||||
) |
||||
} |
||||
rb2 = req2.Marshal(rb2) |
||||
if !utils.FastEqual(rb1, rb2) { |
||||
if len(rb1) != len(rb2) { |
||||
t.Fatalf( |
||||
"unmarshal failed, different lengths\n%d %s\n%d %s\n", |
||||
len(rb1), rb1, len(rb2), rb2, |
||||
) |
||||
} |
||||
for i := range rb1 { |
||||
if rb1[i] != rb2[i] { |
||||
t.Fatalf( |
||||
"unmarshal failed, difference at position %d\n%d %s\n%s\n%d %s\n%s\n", |
||||
i, len(rb1), rb1[:i], rb1[i:], len(rb2), rb2[:i], |
||||
rb2[i:], |
||||
) |
||||
} |
||||
} |
||||
t.Fatalf( |
||||
"unmarshal failed\n%d %s\n%d %s\n", |
||||
len(rb1), rb1, len(rb2), rb2, |
||||
) |
||||
} |
||||
bufpool.Put(rb1) |
||||
bufpool.Put(rb2) |
||||
bufpool.Put(rb) |
||||
} |
||||
} |
||||
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
// Package closeenvelope provides the encoder for the client message CLOSE which
|
||||
// is a request to terminate a subscription.
|
||||
package closeenvelope |
||||
|
||||
import ( |
||||
"io" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"next.orly.dev/pkg/encoders/envelopes" |
||||
"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 = "CLOSE" |
||||
|
||||
// T is a CLOSE envelope, which is a signal from client to relay to stop a
|
||||
// specified subscription.
|
||||
type T struct { |
||||
ID []byte |
||||
} |
||||
|
||||
var _ codec.Envelope = (*T)(nil) |
||||
|
||||
// New creates an empty new standard formatted closeenvelope.T.
|
||||
func New() *T { return new(T) } |
||||
|
||||
// NewFrom creates a new closeenvelope.T populated with subscription ID.
|
||||
func NewFrom(id []byte) *T { return &T{ID: id} } |
||||
|
||||
// Label returns the label of a closeenvelope.T.
|
||||
func (en *T) Label() string { return L } |
||||
|
||||
// Write the closeenvelope.T to a provided io.Writer.
|
||||
func (en *T) Write(w io.Writer) (err error) { |
||||
_, err = w.Write(en.Marshal(nil)) |
||||
return |
||||
} |
||||
|
||||
// Marshal a closeenvelope.T envelope in minified JSON, appending to a provided
|
||||
// destination slice.
|
||||
func (en *T) Marshal(dst []byte) (b []byte) { |
||||
b = dst |
||||
b = envelopes.Marshal( |
||||
b, L, |
||||
func(bst []byte) (o []byte) { |
||||
o = bst |
||||
o = append(o, '"') |
||||
o = append(o, en.ID...) |
||||
o = append(o, '"') |
||||
return |
||||
}, |
||||
) |
||||
return |
||||
} |
||||
|
||||
// Unmarshal a closeenvelope.T from minified JSON, returning the remainder after
|
||||
// the end of the envelope.
|
||||
func (en *T) Unmarshal(b []byte) (r []byte, err error) { |
||||
r = b |
||||
if en.ID, r, err = text.UnmarshalQuoted(r); chk.E(err) { |
||||
return |
||||
} |
||||
if r, err = envelopes.SkipToTheEnd(r); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Parse reads a CLOSE envelope from minified JSON into a newly allocated
|
||||
// closeenvelope.T.
|
||||
func Parse(b []byte) (t *T, rem []byte, err error) { |
||||
t = New() |
||||
if rem, err = t.Unmarshal(b); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,68 @@
@@ -0,0 +1,68 @@
|
||||
package closeenvelope |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
"testing" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"lukechampine.com/frand" |
||||
"next.orly.dev/pkg/encoders/envelopes" |
||||
"next.orly.dev/pkg/utils" |
||||
"next.orly.dev/pkg/utils/bufpool" |
||||
) |
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) { |
||||
var err error |
||||
for _ = range 1000 { |
||||
rb, rb1, rb2 := bufpool.Get(), bufpool.Get(), bufpool.Get() |
||||
s := []byte(fmt.Sprintf("sub:%d", frand.Intn(math.MaxInt64))) |
||||
req := NewFrom(s) |
||||
rb = req.Marshal(rb) |
||||
rb1 = append(rb1, rb...) |
||||
var rem []byte |
||||
var l string |
||||
if l, rb, err = envelopes.Identify(rb); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
if l != L { |
||||
t.Fatalf("invalid sentinel %s, expect %s", l, L) |
||||
} |
||||
req2 := New() |
||||
if rem, err = req2.Unmarshal(rb); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
// log.I.Ln(req2.ID)
|
||||
if len(rem) > 0 { |
||||
t.Fatalf( |
||||
"unmarshal failed, remainder\n%d %s", |
||||
len(rem), rem, |
||||
) |
||||
} |
||||
rb2 = req2.Marshal(rb2) |
||||
if !utils.FastEqual(rb1, rb2) { |
||||
if len(rb1) != len(rb2) { |
||||
t.Fatalf( |
||||
"unmarshal failed, different lengths\n%d %s\n%d %s\n", |
||||
len(rb1), rb1, len(rb2), rb2, |
||||
) |
||||
} |
||||
for i := range rb1 { |
||||
if rb1[i] != rb2[i] { |
||||
t.Fatalf( |
||||
"unmarshal failed, difference at position %d\n%d %s\n%s\n%d %s\n%s\n", |
||||
i, len(rb1), rb1[:i], rb1[i:], len(rb2), rb2[:i], |
||||
rb2[i:], |
||||
) |
||||
} |
||||
} |
||||
t.Fatalf( |
||||
"unmarshal failed\n%d %s\n%d %s\n", |
||||
len(rb1), rb1, len(rb2), rb2, |
||||
) |
||||
bufpool.Put(rb1) |
||||
bufpool.Put(rb2) |
||||
bufpool.Put(rb) |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
// Package envelopes provides common functions for marshaling and identifying
|
||||
// nostr envelopes (JSON arrays containing protocol messages).
|
||||
package envelopes |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
package envelopes |
||||
|
||||
// Identify handles determining what kind of codec.Envelope is, by the Label,
|
||||
// the first step in identifying the structure of the message. This first step
|
||||
// is not sufficient because the same labels are used on several codec.Envelope
|
||||
// types in the nostr specification. The rest of the context is in whether this
|
||||
// is a client or a relay receiving it.
|
||||
func Identify(b []byte) (t string, rem []byte, err error) { |
||||
var openBrackets, openQuotes, afterQuotes bool |
||||
var label []byte |
||||
rem = b |
||||
for ; len(rem) > 0; rem = rem[1:] { |
||||
if !openBrackets && rem[0] == '[' { |
||||
openBrackets = true |
||||
} else if openBrackets { |
||||
if !openQuotes && rem[0] == '"' { |
||||
openQuotes = true |
||||
} else if afterQuotes { |
||||
// return the remainder after the comma
|
||||
if rem[0] == ',' { |
||||
rem = rem[1:] |
||||
return |
||||
} |
||||
} else if openQuotes { |
||||
for i := range rem { |
||||
if rem[i] == '"' { |
||||
label = rem[:i] |
||||
rem = rem[i:] |
||||
t = string(label) |
||||
afterQuotes = true |
||||
break |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
package envelopes |
||||
|
||||
import ( |
||||
"io" |
||||
) |
||||
|
||||
// Marshaller is a function signature the same as the codec.JSON Marshal but
|
||||
// without the requirement of there being a full implementation or declared
|
||||
// receiver variable of this interface. Used here to encapsulate one or more
|
||||
// other data structures into an envelope.
|
||||
type Marshaller func(dst []byte) (b []byte) |
||||
|
||||
// Marshal is a parser for dynamic typed arrays like nosttr codec.Envelope
|
||||
// types.
|
||||
func Marshal(dst []byte, label string, m Marshaller) (b []byte) { |
||||
b = dst |
||||
b = append(b, '[', '"') |
||||
b = append(b, label...) |
||||
b = append(b, '"', ',') |
||||
b = m(b) |
||||
b = append(b, ']') |
||||
return |
||||
} |
||||
|
||||
// SkipToTheEnd scans forward after all fields in an envelope have been read to
|
||||
// find the closing bracket.
|
||||
func SkipToTheEnd(dst []byte) (rem []byte, err error) { |
||||
if len(dst) == 0 { |
||||
return |
||||
} |
||||
rem = dst |
||||
// we have everything, just need to snip the end
|
||||
for ; len(rem) > 0; rem = rem[1:] { |
||||
if rem[0] == ']' { |
||||
rem = rem[:0] |
||||
return |
||||
} |
||||
} |
||||
err = io.EOF |
||||
return |
||||
} |
||||
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
package event |
||||
|
||||
import ( |
||||
"next.orly.dev/pkg/crypto/sha256" |
||||
"next.orly.dev/pkg/encoders/hex" |
||||
"next.orly.dev/pkg/encoders/ints" |
||||
"next.orly.dev/pkg/encoders/text" |
||||
) |
||||
|
||||
// ToCanonical converts the event to the canonical encoding used to derive the
|
||||
// event ID.
|
||||
func (ev *E) ToCanonical(dst []byte) (b []byte) { |
||||
b = dst |
||||
b = append(b, "[0,\""...) |
||||
b = hex.EncAppend(b, ev.Pubkey) |
||||
b = append(b, "\","...) |
||||
b = ints.New(ev.CreatedAt).Marshal(nil) |
||||
b = append(b, ',') |
||||
b = ints.New(ev.Kind).Marshal(nil) |
||||
b = append(b, ',') |
||||
b = ev.Tags.Marshal(b) |
||||
b = append(b, ',') |
||||
b = text.AppendQuote(b, ev.Content, text.NostrEscape) |
||||
b = append(b, ']') |
||||
return |
||||
} |
||||
|
||||
// GetIDBytes returns the raw SHA256 hash of the canonical form of an event.E.
|
||||
func (ev *E) GetIDBytes() []byte { return Hash(ev.ToCanonical(nil)) } |
||||
|
||||
// Hash is a little helper generate a hash and return a slice instead of an
|
||||
// array.
|
||||
func Hash(in []byte) (out []byte) { |
||||
h := sha256.Sum256(in) |
||||
return h[:] |
||||
} |
||||
@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
|
||||
package event |
||||
|
||||
import ( |
||||
"lol.mleku.dev/chk" |
||||
"lol.mleku.dev/errorf" |
||||
"lol.mleku.dev/log" |
||||
"next.orly.dev/pkg/crypto/p256k" |
||||
"next.orly.dev/pkg/interfaces/signer" |
||||
"next.orly.dev/pkg/utils" |
||||
) |
||||
|
||||
// Sign the event using the signer.I. Uses github.com/bitcoin-core/secp256k1 if
|
||||
// available for much faster signatures.
|
||||
//
|
||||
// Note that this only populates the Pubkey, ID and Sig. The caller must
|
||||
// set the CreatedAt timestamp as intended.
|
||||
func (ev *E) Sign(keys signer.I) (err error) { |
||||
ev.Pubkey = keys.Pub() |
||||
ev.ID = ev.GetIDBytes() |
||||
if ev.Sig, err = keys.Sign(ev.ID); chk.E(err) { |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Verify an event is signed by the pubkey it contains. Uses
|
||||
// github.com/bitcoin-core/secp256k1 if available for faster verification.
|
||||
func (ev *E) Verify() (valid bool, err error) { |
||||
keys := p256k.Signer{} |
||||
if err = keys.InitPub(ev.Pubkey); chk.E(err) { |
||||
return |
||||
} |
||||
if valid, err = keys.Verify(ev.ID, ev.Sig); chk.T(err) { |
||||
// check that this isn't because of a bogus ID
|
||||
id := ev.GetIDBytes() |
||||
if !utils.FastEqual(id, ev.ID) { |
||||
log.E.Ln("event ID incorrect") |
||||
ev.ID = id |
||||
err = nil |
||||
if valid, err = keys.Verify(ev.ID, ev.Sig); chk.E(err) { |
||||
return |
||||
} |
||||
err = errorf.W("event ID incorrect but signature is valid on correct ID") |
||||
} |
||||
return |
||||
} |
||||
return |
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
package codec |
||||
|
||||
import ( |
||||
"io" |
||||
) |
||||
|
||||
type I interface { |
||||
MarshalWrite(w io.Writer) (err error) |
||||
UnmarshalRead(r io.Reader) (err error) |
||||
} |
||||
|
||||
// Envelope is an interface for the nostr "envelope" message formats, a JSON
|
||||
// array with the first field an upper case string that provides type
|
||||
// information, in combination with the context of the side sending it (relay or
|
||||
// client).
|
||||
type Envelope interface { |
||||
// Label returns the (uppercase) string that signifies the type of message.
|
||||
Label() string |
||||
// Write outputs the envelope to an io.Writer
|
||||
Write(w io.Writer) (err error) |
||||
// JSON is a somewhat simplified version of the
|
||||
// json.Marshaler/json.Unmarshaler that has no error for the Marshal side of
|
||||
// the operation.
|
||||
JSON |
||||
} |
||||
|
||||
// JSON is a somewhat simplified version of the json.Marshaler/json.Unmarshaler
|
||||
// that has no error for the Marshal side of the operation.
|
||||
type JSON interface { |
||||
// Marshal converts the data of the type into JSON, appending it to the provided
|
||||
// slice and returning the extended slice.
|
||||
Marshal(dst []byte) (b []byte) |
||||
// Unmarshal decodes a JSON form of a type back into the runtime form, and
|
||||
// returns whatever remains after the type has been decoded out.
|
||||
Unmarshal(b []byte) (r []byte, err error) |
||||
} |
||||
|
||||
// Binary is a similarly simplified form of the stdlib binary Marshal/Unmarshal
|
||||
// server. Same as JSON it does not have an error for the MarshalBinary.
|
||||
type Binary interface { |
||||
// MarshalBinary converts the data of the type into binary form, appending
|
||||
// it to the provided slice.
|
||||
MarshalBinary(dst []byte) (b []byte) |
||||
// UnmarshalBinary decodes a binary form of a type back into the runtime
|
||||
// form, and returns whatever remains after the type has been decoded out.
|
||||
UnmarshalBinary(b []byte) (r []byte, err error) |
||||
} |
||||
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
package auth |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"encoding/base64" |
||||
"net/url" |
||||
"strings" |
||||
"time" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"lol.mleku.dev/errorf" |
||||
"next.orly.dev/pkg/encoders/event" |
||||
"next.orly.dev/pkg/encoders/kind" |
||||
"next.orly.dev/pkg/encoders/tag" |
||||
"next.orly.dev/pkg/utils" |
||||
) |
||||
|
||||
// GenerateChallenge creates a reasonable, 16-byte base64 challenge string
|
||||
func GenerateChallenge() (b []byte) { |
||||
bb := make([]byte, 12) |
||||
b = make([]byte, 16) |
||||
_, _ = rand.Read(bb) |
||||
base64.URLEncoding.Encode(b, bb) |
||||
return |
||||
} |
||||
|
||||
// CreateUnsigned creates an event which should be sent via an "AUTH" command.
|
||||
// If the authentication succeeds, the user will be authenticated as a pubkey.
|
||||
func CreateUnsigned(pubkey, challenge []byte, relayURL string) (ev *event.E) { |
||||
return &event.E{ |
||||
Pubkey: pubkey, |
||||
CreatedAt: time.Now().Unix(), |
||||
Kind: kind.ClientAuthentication.K, |
||||
Tags: tag.NewS( |
||||
tag.New("relay", relayURL), |
||||
tag.New("challenge", string(challenge)), |
||||
), |
||||
} |
||||
} |
||||
|
||||
// helper function for ValidateAuthEvent.
|
||||
func parseURL(input string) (*url.URL, error) { |
||||
return url.Parse( |
||||
strings.ToLower( |
||||
strings.TrimSuffix(input, "/"), |
||||
), |
||||
) |
||||
} |
||||
|
||||
var ( |
||||
// ChallengeTag is the tag for the challenge in a NIP-42 auth event
|
||||
// (prevents relay attacks).
|
||||
ChallengeTag = []byte("challenge") |
||||
// RelayTag is the relay tag for a NIP-42 auth event (prevents cross-server
|
||||
// attacks).
|
||||
RelayTag = []byte("relay") |
||||
) |
||||
|
||||
// Validate checks whether an event is a valid NIP-42 event for a given
|
||||
// challenge and relayURL. The result of the validation is encoded in the ok
|
||||
// bool.
|
||||
func Validate(evt *event.E, challenge []byte, relayURL string) ( |
||||
ok bool, err error, |
||||
) { |
||||
if evt.Kind != kind.ClientAuthentication.K { |
||||
err = errorf.E( |
||||
"event incorrect kind for auth: %d %s", |
||||
evt.Kind, kind.GetString(evt.Kind), |
||||
) |
||||
return |
||||
} |
||||
if evt.Tags.GetFirst(ChallengeTag) == nil { |
||||
err = errorf.E("challenge tag missing from auth response") |
||||
return |
||||
} |
||||
if !utils.FastEqual(challenge, evt.Tags.GetFirst(ChallengeTag).Value()) { |
||||
err = errorf.E("challenge tag incorrect from auth response") |
||||
return |
||||
} |
||||
var expected, found *url.URL |
||||
if expected, err = parseURL(relayURL); chk.D(err) { |
||||
return |
||||
} |
||||
r := evt.Tags. |
||||
GetFirst(RelayTag).Value() |
||||
if len(r) == 0 { |
||||
err = errorf.E("relay tag missing from auth response") |
||||
return |
||||
} |
||||
if found, err = parseURL(string(r)); chk.D(err) { |
||||
err = errorf.E("error parsing relay url: %s", err) |
||||
return |
||||
} |
||||
if expected.Scheme != found.Scheme { |
||||
err = errorf.E( |
||||
"HTTP Scheme incorrect: expected '%s' got '%s", |
||||
expected.Scheme, found.Scheme, |
||||
) |
||||
return |
||||
} |
||||
if expected.Host != found.Host { |
||||
err = errorf.E( |
||||
"HTTP Host incorrect: expected '%s' got '%s", |
||||
expected.Host, found.Host, |
||||
) |
||||
return |
||||
} |
||||
if expected.Path != found.Path { |
||||
err = errorf.E( |
||||
"HTTP Path incorrect: expected '%s' got '%s", |
||||
expected.Path, found.Path, |
||||
) |
||||
return |
||||
} |
||||
|
||||
now := time.Now().Unix() |
||||
ca := evt.CreatedAt |
||||
if ca > now+10*60 || ca < now-10*60 { |
||||
err = errorf.E( |
||||
"auth event more than 10 minutes before or after current time", |
||||
) |
||||
return |
||||
} |
||||
// save for last, as it is the most expensive operation
|
||||
return evt.Verify() |
||||
} |
||||
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
package auth |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"lol.mleku.dev/chk" |
||||
"lol.mleku.dev/log" |
||||
"next.orly.dev/pkg/crypto/p256k" |
||||
) |
||||
|
||||
func TestCreateUnsigned(t *testing.T) { |
||||
var err error |
||||
signer := new(p256k.Signer) |
||||
if err = signer.Generate(); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
var ok bool |
||||
const relayURL = "wss://example.com" |
||||
for range 100 { |
||||
challenge := GenerateChallenge() |
||||
ev := CreateUnsigned(signer.Pub(), challenge, relayURL) |
||||
if err = ev.Sign(signer); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
log.I.S(ev) |
||||
if ok, err = Validate(ev, challenge, relayURL); chk.E(err) { |
||||
t.Fatal(err) |
||||
} |
||||
if !ok { |
||||
bb := ev.Marshal(nil) |
||||
t.Fatalf("failed to validate auth event\n%s", bb) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue