Merge pull request #39 from barkyq/master
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
|||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/valyala/fastjson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
@@ -46,33 +45,79 @@ func (evt *Event) GetID() string {
|
|||||||
return hex.EncodeToString(h[:])
|
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 {
|
func (evt *Event) Serialize() []byte {
|
||||||
// the serialization process is just putting everything into a JSON array
|
// the serialization process is just putting everything into a JSON array
|
||||||
// so the order is kept
|
// so the order is kept. See NIP-01
|
||||||
var arena fastjson.Arena
|
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
|
// content needs to be escaped in general as it is user generated.
|
||||||
arr.SetArrayItem(0, arena.NewNumberInt(0))
|
ser = quoteEscapeString(ser, evt.Content)
|
||||||
|
ser = append(ser, ']')
|
||||||
|
|
||||||
// pubkey
|
return ser
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckSignature checks if the signature is valid for the id
|
// CheckSignature checks if the signature is valid for the id
|
||||||
|
|||||||
+11
-6
@@ -13,16 +13,18 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ECDH
|
// ComputeSharedSecret returns a shared secret key used to encrypt messages.
|
||||||
func ComputeSharedSecret(senderPrivKey string, receiverPubKey string) (sharedSecret []byte, err error) {
|
// The private and public keys should be hex encoded.
|
||||||
privKeyBytes, err := hex.DecodeString(senderPrivKey)
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error decoding sender private key: %s. \n", err)
|
return nil, fmt.Errorf("Error decoding sender private key: %s. \n", err)
|
||||||
}
|
}
|
||||||
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
privKey, _ := btcec.PrivKeyFromBytes(privKeyBytes)
|
||||||
|
|
||||||
// adding 02 to signal that this is a compressed public key (33 bytes)
|
// 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error decoding hex string of receiver public key: %s. \n", err)
|
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
|
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) {
|
func Encrypt(message string, key []byte) (string, error) {
|
||||||
// block size is 16 bytes
|
// block size is 16 bytes
|
||||||
iv := make([]byte, 16)
|
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
|
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) {
|
func Decrypt(content string, key []byte) (string, error) {
|
||||||
parts := strings.Split(content, "?iv=")
|
parts := strings.Split(content, "?iv=")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ func (r *Relay) Connect(ctx context.Context) error {
|
|||||||
func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
||||||
status := PublishStatusFailed
|
status := PublishStatusFailed
|
||||||
|
|
||||||
|
// data races on status variable without this mutex
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
if _, ok := ctx.Deadline(); !ok {
|
if _, ok := ctx.Deadline(); !ok {
|
||||||
// if no timeout is set, force it to 3 seconds
|
// if no timeout is set, force it to 3 seconds
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
@@ -213,6 +216,8 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
|||||||
|
|
||||||
// listen for an OK callback
|
// listen for an OK callback
|
||||||
okCallback := func(ok bool) {
|
okCallback := func(ok bool) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
if ok {
|
if ok {
|
||||||
status = PublishStatusSucceeded
|
status = PublishStatusSucceeded
|
||||||
} else {
|
} else {
|
||||||
@@ -224,20 +229,23 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
|||||||
defer r.okCallbacks.Delete(event.ID)
|
defer r.okCallbacks.Delete(event.ID)
|
||||||
|
|
||||||
// publish event
|
// publish event
|
||||||
err := r.Connection.WriteJSON([]interface{}{"EVENT", event})
|
if err := r.Connection.WriteJSON([]interface{}{"EVENT", event}); err != nil {
|
||||||
if err != nil {
|
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
|
|
||||||
// update status (this will be returned later)
|
// update status (this will be returned later)
|
||||||
|
mu.Lock()
|
||||||
status = PublishStatusSent
|
status = PublishStatusSent
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
sub := r.Subscribe(ctx, Filters{Filter{IDs: []string{event.ID}}})
|
sub := r.Subscribe(ctx, Filters{Filter{IDs: []string{event.ID}}})
|
||||||
|
defer mu.Unlock()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case receivedEvent := <-sub.Events:
|
case receivedEvent := <-sub.Events:
|
||||||
if receivedEvent.ID == event.ID {
|
if receivedEvent.ID == event.ID {
|
||||||
// we got a success, so update our status and proceed to return
|
// we got a success, so update our status and proceed to return
|
||||||
|
mu.Lock()
|
||||||
status = PublishStatusSucceeded
|
status = PublishStatusSucceeded
|
||||||
return status
|
return status
|
||||||
}
|
}
|
||||||
@@ -246,6 +254,8 @@ func (r *Relay) Publish(ctx context.Context, event Event) Status {
|
|||||||
// will proceed to return status as it is
|
// will proceed to return status as it is
|
||||||
// e.g. if this happens because of the timeout then status will probably be "failed"
|
// 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"
|
// 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
|
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 {
|
if err := r.Connection.WriteJSON([]interface{}{"AUTH", event}); err != nil {
|
||||||
// status will be "failed"
|
// status will be "failed"
|
||||||
return status
|
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"
|
// the context either times out, and the status is "sent"
|
||||||
// or the okCallback is called and the status is set to "succeeded" or "failed"
|
// 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
|
// NIP-42 does not mandate an "OK" reply to an "AUTH" message
|
||||||
|
|||||||
Reference in New Issue
Block a user