Merge pull request #39 from barkyq/master

This commit is contained in:
fiatjaf
2023-01-17 08:12:32 -03:00
committed by GitHub
3 changed files with 96 additions and 36 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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