diff --git a/event.go b/event.go index 0a5e981..33055a4 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,79 @@ 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 + // so the order is kept. See NIP-01 + ser := make([]byte, 0) - arr := arena.NewArray() + // 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, ',') + } + 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{']', ','}...) - // version: 0 - arr.SetArrayItem(0, arena.NewNumberInt(0)) + // content needs to be escaped in general as it is user generated. + ser = quoteEscapeString(ser, evt.Content) + ser = append(ser, ']') - // pubkey - arr.SetArrayItem(1, arena.NewString(evt.PubKey)) - - // created_at - arr.SetArrayItem(2, arena.NewNumberInt(int(evt.CreatedAt.Unix()))) - - // kind - arr.SetArrayItem(3, arena.NewNumberInt(evt.Kind)) - - // tags - arr.SetArrayItem(4, tagsToFastjsonArray(&arena, evt.Tags)) - - // content - arr.SetArrayItem(5, arena.NewString(evt.Content)) - - return arr.MarshalTo(nil) + return ser } // CheckSignature checks if the signature is valid for the id 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 { 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