package auth import ( "crypto/rand" "encoding/base64" "net/url" "strings" "time" "realy.lol/chk" "realy.lol/event" "realy.lol/kind" "realy.lol/log" "realy.lol/tag" "realy.lol/tags" "realy.lol/timestamp" ) // GenerateChallenge creates a reasonable, 96 byte base64 challenge string func GenerateChallenge() (b []byte) { bb := make([]byte, 12) b = make([]byte, 16) _, _ = rand.Read(bb) base64.StdEncoding.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 pubkey. func CreateUnsigned(pubkey, challenge []byte, relayURL string) (ev *event.T) { return &event.T{ Pubkey: pubkey, CreatedAt: timestamp.Now(), Kind: kind.ClientAuthentication, Tags: tags.New(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 is the relay tag for a NIP-42 auth event (prevents cross-server attacks). RelayTag = []byte("relay") ) // Validate checks whether event is a valid NIP-42 event for given challenge and relayURL. // The result of the validation is encoded in the ok bool. func Validate(ev *event.T, challenge []byte, relayURL string) (ok bool, err error) { // log.T.ToSliceOfBytes("relayURL '%s'", relayURL) // log.I.S(ev) if ev.Kind.K != kind.ClientAuthentication.K { err = log.E.Err("event incorrect kind for auth: %d %s", ev.Kind.K, kind.GetString(ev.Kind)) log.D.Ln(err) return } if ev.Tags.GetFirst(tag.New(ChallengeTag, challenge)) == nil { err = log.E.Err("challenge tag missing from auth response") log.D.Ln(err) return } // log.I.Ln(relayURL) var expected, found *url.URL if expected, err = parseURL(relayURL); chk.D(err) { log.D.Ln(err) return } r := ev.Tags. GetFirst(tag.New(RelayTag, nil)).Value() if len(r) == 0 { err = log.E.Err("relay tag missing from auth response") log.D.Ln(err) return } if found, err = parseURL(string(r)); chk.D(err) { err = log.E.Err("error parsing relay url: %s", err) log.D.Ln(err) return } if expected.Scheme != found.Scheme { err = log.E.Err("HTTP Scheme incorrect: expected '%s' got '%s", expected.Scheme, found.Scheme) log.D.Ln(err) return } if expected.Host != found.Host { err = log.E.Err("HTTP Host incorrect: expected '%s' got '%s", expected.Host, found.Host) log.D.Ln(err) return } if expected.Path != found.Path { err = log.E.Err("HTTP Path incorrect: expected '%s' got '%s", expected.Path, found.Path) log.D.Ln(err) return } now := time.Now() if ev.CreatedAt.Time().After(now.Add(10*time.Minute)) || ev.CreatedAt.Time().Before(now.Add(-10*time.Minute)) { err = log.E.Err( "auth event more than 10 minutes before or after current time") log.D.Ln(err) return } // save for last, as it is the most expensive operation return ev.Verify() }