From 67813257dfc9be4f2c2442718eb20a2c03b528ef Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Mon, 16 Jan 2023 11:32:00 -0500 Subject: [PATCH 1/5] stopping data races with sync.mutex to Publish() in relay.go --- relay.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/relay.go b/relay.go index 1bc5e33..dd8ca41 100644 --- a/relay.go +++ b/relay.go @@ -199,6 +199,9 @@ func (r *Relay) Connect(ctx context.Context) error { func (r *Relay) Publish(ctx context.Context, event Event) Status { status := PublishStatusFailed + // data races on status variable without this mutex + var mu sync.Mutex + if _, ok := ctx.Deadline(); !ok { // if no timeout is set, force it to 3 seconds var cancel context.CancelFunc @@ -213,6 +216,8 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status { // listen for an OK callback okCallback := func(ok bool) { + mu.Lock() + defer mu.Unlock() if ok { status = PublishStatusSucceeded } else { @@ -224,20 +229,23 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status { defer r.okCallbacks.Delete(event.ID) // publish event - err := r.Connection.WriteJSON([]interface{}{"EVENT", event}) - if err != nil { + if err := r.Connection.WriteJSON([]interface{}{"EVENT", event}); err != nil { return status } // update status (this will be returned later) + mu.Lock() status = PublishStatusSent + mu.Unlock() sub := r.Subscribe(ctx, Filters{Filter{IDs: []string{event.ID}}}) + defer mu.Unlock() for { select { case receivedEvent := <-sub.Events: if receivedEvent.ID == event.ID { // we got a success, so update our status and proceed to return + mu.Lock() status = PublishStatusSucceeded return status } @@ -246,6 +254,8 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status { // will proceed to return status as it is // e.g. if this happens because of the timeout then status will probably be "failed" // but if it happens because okCallback was called then it might be "succeeded" + // do not return if okCallback is in process + mu.Lock() return status } } @@ -289,12 +299,12 @@ func (r *Relay) Auth(ctx context.Context, event Event) Status { if err := r.Connection.WriteJSON([]interface{}{"AUTH", event}); err != nil { // status will be "failed" return status - } else { - // use mu.Lock() just in case the okCallback got called, extremely unlikely. - mu.Lock() - status = PublishStatusSent - mu.Unlock() } + // use mu.Lock() just in case the okCallback got called, extremely unlikely. + mu.Lock() + status = PublishStatusSent + mu.Unlock() + // the context either times out, and the status is "sent" // or the okCallback is called and the status is set to "succeeded" or "failed" // NIP-42 does not mandate an "OK" reply to an "AUTH" message From ebe3d6148490d5a32d3c53f7c0c233ecd23cd490 Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Mon, 16 Jan 2023 21:55:34 -0500 Subject: [PATCH 2/5] added nip04.go docstrings changed variable names in ComputeSharedSecret to emphasize that it is agnostic about who is sender and who is receiver. --- nip04/nip04.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nip04/nip04.go b/nip04/nip04.go index 1923cd6..fc4cca5 100644 --- a/nip04/nip04.go +++ b/nip04/nip04.go @@ -13,16 +13,18 @@ import ( "github.com/btcsuite/btcd/btcec/v2" ) -// ECDH -func ComputeSharedSecret(senderPrivKey string, receiverPubKey string) (sharedSecret []byte, err error) { - privKeyBytes, err := hex.DecodeString(senderPrivKey) +// ComputeSharedSecret returns a shared secret key used to encrypt messages. +// The private and public keys should be hex encoded. +// Uses the Diffie-Hellman key exchange (ECDH) (RFC 4753). +func ComputeSharedSecret(pub string, sk string) (sharedSecret []byte, err error) { + privKeyBytes, err := hex.DecodeString(sk) if err != nil { return nil, fmt.Errorf("Error decoding sender private key: %s. \n", err) } privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes) // adding 02 to signal that this is a compressed public key (33 bytes) - pubKeyBytes, err := hex.DecodeString("02" + receiverPubKey) + pubKeyBytes, err := hex.DecodeString("02" + pub) if err != nil { return nil, fmt.Errorf("Error decoding hex string of receiver public key: %s. \n", err) } @@ -34,7 +36,9 @@ func ComputeSharedSecret(senderPrivKey string, receiverPubKey string) (sharedSec return btcec.GenerateSharedSecret(privKey, pubKey), nil } -// aes-256-cbc +// Encrypt encrypts message with key using aes-256-cbc. +// key should be the shared secret generated by ComputeSharedSecret. +// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector). func Encrypt(message string, key []byte) (string, error) { // block size is 16 bytes iv := make([]byte, 16) @@ -70,7 +74,8 @@ func Encrypt(message string, key []byte) (string, error) { return base64.StdEncoding.EncodeToString(ciphertext) + "?iv=" + base64.StdEncoding.EncodeToString(iv), nil } -// aes-256-cbc +// Decrypt decrypts a content string using the shared secret key. +// The inverse operation to message -> Encrypt(message, key). func Decrypt(content string, key []byte) (string, error) { parts := strings.Split(content, "?iv=") if len(parts) < 2 { From be46f7abac1e672e0d2f1bcb295a922a6759a980 Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Mon, 16 Jan 2023 23:13:19 -0500 Subject: [PATCH 3/5] fixing JSON escaping (to RFC4627) --- event.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 13 deletions(-) diff --git a/event.go b/event.go index 0a5e981..a7a2ece 100644 --- a/event.go +++ b/event.go @@ -8,7 +8,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/valyala/fastjson" ) type Event struct { @@ -46,33 +45,86 @@ func (evt *Event) GetID() string { return hex.EncodeToString(h[:]) } -// Serialize outputs a byte array that can be hashed/signed to identify/authenticate +// Escaping strings for JSON encoding according to RFC4627. +// Also encloses result in quotation marks "". +func quoteEscapeString(dst []byte, s string) []byte { + dst = append(dst, '"') + for i := 0; i < len(s); i++ { + c := s[i] + switch { + case c == '"': + // quotation mark + dst = append(dst, []byte{'\\', '"'}...) + case c == '\\': + // reverse solidus + dst = append(dst, []byte{'\\', '\\'}...) + case c >= 0x20: + // default, rest below are control chars + dst = append(dst, c) + case c < 0x09: + dst = append(dst, []byte{'\\', 'u', '0', '0', '0', '0' + c}...) + case c == 0x09: + dst = append(dst, []byte{'\\', 't'}...) + case c == 0x0a: + dst = append(dst, []byte{'\\', 'n'}...) + case c == 0x0d: + dst = append(dst, []byte{'\\', 'r'}...) + case c < 0x10: + dst = append(dst, []byte{'\\', 'u', '0', '0', '0', 0x57 + c}...) + case c < 0x1a: + dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x20 + c}...) + case c < 0x20: + dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x47 + c}...) + } + } + dst = append(dst, '"') + return dst +} + +// Serialize outputs a byte array that can be hashed/signed to identify/authenticate. +// JSON encoding as defined in RFC4627. func (evt *Event) Serialize() []byte { // the serialization process is just putting everything into a JSON array - // so the order is kept - var arena fastjson.Arena - - arr := arena.NewArray() + // so the order is kept. See NIP-01 + ser := make([]byte, 0) // version: 0 - arr.SetArrayItem(0, arena.NewNumberInt(0)) + ser = append(ser, []byte{'[', '0', ','}...) // pubkey - arr.SetArrayItem(1, arena.NewString(evt.PubKey)) + ser = append(ser, '"') + ser = append(ser, []byte(evt.PubKey)...) + ser = append(ser, []byte{'"', ','}...) // created_at - arr.SetArrayItem(2, arena.NewNumberInt(int(evt.CreatedAt.Unix()))) + ser = append(ser, []byte(fmt.Sprintf("%d", int(evt.CreatedAt.Unix())))...) + ser = append(ser, ',') // kind - arr.SetArrayItem(3, arena.NewNumberInt(evt.Kind)) + ser = append(ser, []byte(fmt.Sprintf("%d,", int(evt.Kind)))...) // tags - arr.SetArrayItem(4, tagsToFastjsonArray(&arena, evt.Tags)) + ser = append(ser, '[') + for i, tag := range evt.Tags { + if i > 0 { + ser = append(ser, ',') + } + ser = append(ser, '[') + for i, s := range tag { + if i > 0 { + ser = append(ser, ',') + } + ser = quoteEscapeString(ser, s) + } + ser = append(ser, ']') + } + ser = append(ser, []byte{']', ','}...) // content - arr.SetArrayItem(5, arena.NewString(evt.Content)) + ser = quoteEscapeString(ser, evt.Content) + ser = append(ser, ']') - return arr.MarshalTo(nil) + return ser } // CheckSignature checks if the signature is valid for the id From 771a2b621829772d091c86c2b9822329b4576367 Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Mon, 16 Jan 2023 23:22:45 -0500 Subject: [PATCH 4/5] remove int64 -> int conversion as it is not necessary for Sprinting --- event.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/event.go b/event.go index a7a2ece..0a27977 100644 --- a/event.go +++ b/event.go @@ -97,11 +97,11 @@ func (evt *Event) Serialize() []byte { ser = append(ser, []byte{'"', ','}...) // created_at - ser = append(ser, []byte(fmt.Sprintf("%d", int(evt.CreatedAt.Unix())))...) + ser = append(ser, []byte(fmt.Sprintf("%d", evt.CreatedAt.Unix()))...) ser = append(ser, ',') // kind - ser = append(ser, []byte(fmt.Sprintf("%d,", int(evt.Kind)))...) + ser = append(ser, []byte(fmt.Sprintf("%d,", evt.Kind))...) // tags ser = append(ser, '[') From c6b4867dd4073459f61d08aef4bfd98e9e97d435 Mon Sep 17 00:00:00 2001 From: Dylan Cant Date: Mon, 16 Jan 2023 23:31:22 -0500 Subject: [PATCH 5/5] simplifications in number of lines of code for serialization --- event.go | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/event.go b/event.go index 0a27977..33055a4 100644 --- a/event.go +++ b/event.go @@ -88,23 +88,16 @@ func (evt *Event) Serialize() []byte { // so the order is kept. See NIP-01 ser := make([]byte, 0) - // version: 0 - ser = append(ser, []byte{'[', '0', ','}...) - - // pubkey - ser = append(ser, '"') - ser = append(ser, []byte(evt.PubKey)...) - ser = append(ser, []byte{'"', ','}...) - - // created_at - ser = append(ser, []byte(fmt.Sprintf("%d", evt.CreatedAt.Unix()))...) - ser = append(ser, ',') - - // kind - ser = append(ser, []byte(fmt.Sprintf("%d,", evt.Kind))...) - - // tags - ser = append(ser, '[') + // the header portion is easy to serialize + // [0,"pubkey",created_at,kind,[ + ser = append(ser, []byte( + fmt.Sprintf( + "[0,\"%s\",%d,%d,[", + evt.PubKey, + evt.CreatedAt.Unix(), + evt.Kind, + ))...) + // tags need to be escaped in general. for i, tag := range evt.Tags { if i > 0 { ser = append(ser, ',') @@ -120,7 +113,7 @@ func (evt *Event) Serialize() []byte { } ser = append(ser, []byte{']', ','}...) - // content + // content needs to be escaped in general as it is user generated. ser = quoteEscapeString(ser, evt.Content) ser = append(ser, ']')