mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-09-27 20:38:41 +02:00
Merge pull request #39 from barkyq/master
This commit is contained in:
91
event.go
91
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
|
||||
|
@@ -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 {
|
||||
|
24
relay.go
24
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
|
||||
|
Reference in New Issue
Block a user