refactor nip-44 and server side of bunker

This commit is contained in:
2025-04-20 11:27:56 -01:06
parent 07d91d2356
commit 3a79179436
11 changed files with 868 additions and 583 deletions

69
bunker/main.go Normal file
View File

@@ -0,0 +1,69 @@
package bunker
import (
"encoding/json"
"net/url"
"strings"
"relay.mleku.dev/chk"
"relay.mleku.dev/context"
"relay.mleku.dev/event"
"relay.mleku.dev/keys"
)
type Request struct {
ID string `json:"id"`
Method string `json:"method"`
Params [][]byte `json:"params"`
}
func (r *Request) String() (s string) {
var j []byte
var err error
if j, err = json.Marshal(r); chk.E(err) {
return
}
return string(j)
}
type Response struct {
ID string `json:"id"`
Error string `json:"error,omitempty"`
Result string `json:"result,omitempty"`
}
func (r *Response) String() (s string) {
var j []byte
var err error
if j, err = json.Marshal(r); chk.E(err) {
return
}
return string(j)
}
type Signer interface {
GetSession(clientPubkey string) (Session, bool)
HandleRequest(context.T, *event.T) (req Request, resp Response,
eventResponse *event.T, err error)
}
type RelayReadWrite struct {
Read, Write bool
}
func IsValidBunkerURL(input string) bool {
p, err := url.Parse(input)
if err != nil {
return false
}
if p.Scheme != "bunker" {
return false
}
if !keys.IsValidPublicKey(p.Host) {
return false
}
if !strings.Contains(p.RawQuery, "relay=") {
return false
}
return true
}

63
bunker/session.go Normal file
View File

@@ -0,0 +1,63 @@
package bunker
import (
"encoding/json"
"relay.mleku.dev/chk"
"relay.mleku.dev/encryption"
"relay.mleku.dev/event"
"relay.mleku.dev/kind"
"relay.mleku.dev/tag"
"relay.mleku.dev/tags"
"relay.mleku.dev/timestamp"
)
type Session struct {
Pubkey string
SharedKey []byte
ConversationKey []byte
}
func (s *Session) ParseRequest(ev *event.T) (req *Request, err error) {
var b []byte
if b, err = encryption.Decrypt(ev.Content, s.ConversationKey); chk.E(err) {
if b, err = encryption.DecryptNip4(ev.Content, s.SharedKey); chk.E(err) {
return
}
}
if err = json.Unmarshal(b, &req); chk.E(err) {
return
}
return
}
func (s *Session) MakeResponse(id, requester, result string,
inErr error) (resp *Response,
ev *event.T, err error) {
if inErr != nil {
resp = &Response{
ID: string(id),
Result: inErr.Error(),
}
} else if len(result) > 0 {
resp = &Response{
ID: string(id),
Result: string(result),
}
}
var j []byte
if j, err = json.Marshal(resp); chk.E(err) {
return
}
var ciphertext []byte
if ciphertext, err = encryption.Encrypt(j, s.ConversationKey); chk.E(err) {
return
}
ev = &event.T{
Content: ciphertext,
CreatedAt: timestamp.Now(),
Kind: kind.NostrConnect,
Tags: tags.New(tag.New("p", requester)),
}
return
}

199
bunker/static.go Normal file
View File

@@ -0,0 +1,199 @@
package bunker
import (
"encoding/json"
"fmt"
"sync"
"relay.mleku.dev/chk"
"relay.mleku.dev/context"
"relay.mleku.dev/ec/schnorr"
"relay.mleku.dev/encryption"
"relay.mleku.dev/errorf"
"relay.mleku.dev/event"
"relay.mleku.dev/hex"
"relay.mleku.dev/keys"
"relay.mleku.dev/kind"
"relay.mleku.dev/signer"
)
type StaticKeySigner struct {
sync.Mutex
secretKey signer.I
sessions map[string]*Session
RelaysToAdvertise map[string]RelayReadWrite
AuthorizeRequest func(harmless bool, from, secret []byte) bool
}
func NewStaticKeySigner(secretKey signer.I) *StaticKeySigner {
return &StaticKeySigner{secretKey: secretKey,
RelaysToAdvertise: make(map[string]RelayReadWrite)}
}
func (p *StaticKeySigner) GetSession(clientPubkey string) (s *Session, exists bool) {
s, exists = p.sessions[clientPubkey]
return
}
func (p *StaticKeySigner) getOrCreateSession(clientPubkey []byte) (s *Session, err error) {
p.Lock()
defer p.Unlock()
s = new(Session)
var exists bool
if s, exists = p.sessions[string(clientPubkey)]; exists {
return
}
if s.SharedKey, err = encryption.ComputeSharedSecret(clientPubkey,
p.secretKey.Sec()); chk.E(err) {
return
}
s.ConversationKey = p.secretKey.Pub()
s.Pubkey = hex.Enc(p.secretKey.Pub())
// add to pool
p.sessions[string(clientPubkey)] = s
return
}
func (p *StaticKeySigner) HandleRequest(_ context.T, ev *event.T) (req *Request, res *Response,
eventResponse *event.T, err error) {
if !ev.Kind.Equal(kind.NostrConnect) {
err = errorf.E("event kind is %s, but we expected %s",
ev.Kind.Name(), kind.NostrConnect.Name())
return
}
var session *Session
if session, err = p.getOrCreateSession(ev.Pubkey); chk.E(err) {
return
}
if req, err = session.ParseRequest(ev); chk.E(err) {
return
}
var secret, result []byte
var harmless bool
var rErr error
switch req.Method {
case "connect":
if len(req.Params) >= 2 {
secret = req.Params[1]
}
result = []byte("ack")
harmless = true
case "get_public_key":
result = []byte(session.Pubkey)
harmless = true
case "sign_event":
if len(req.Params) != 1 {
rErr = errorf.E("wrong number of arguments to 'sign_event'")
break
}
evt := &event.T{}
if rErr = json.Unmarshal(req.Params[0], evt); chk.E(rErr) {
break
}
if rErr = evt.Sign(p.secretKey); chk.E(rErr) {
break
}
result = evt.Serialize()
case "get_relays":
if result, rErr = json.Marshal(p.RelaysToAdvertise); chk.E(rErr) {
break
}
harmless = true
case "nip44_encrypt":
var pk, sharedSecret []byte
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
break
}
if sharedSecret, rErr = p.GetConversationKey(pk); chk.E(err) {
break
}
if result, rErr = encryption.Encrypt(req.Params[1], sharedSecret); chk.E(err) {
break
}
case "nip44_decrypt":
var pk, sharedSecret []byte
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
break
}
if sharedSecret, rErr = p.GetConversationKey(pk); chk.E(err) {
break
}
if result, err = encryption.Decrypt(req.Params[1], sharedSecret); chk.E(err) {
break
}
case "nip04_encrypt":
var pk, sharedSecret []byte
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
break
}
if sharedSecret, rErr = p.ComputeSharedSecret(pk); chk.E(err) {
break
}
if result, rErr = encryption.EncryptNip4(req.Params[1],
sharedSecret); chk.E(err) {
break
}
case "nip04_decrypt":
var pk, sharedSecret []byte
if pk, rErr = CheckParamsAndKey(req); chk.E(err) {
break
}
if sharedSecret, rErr = p.ComputeSharedSecret(pk); chk.E(err) {
break
}
if result, rErr = encryption.DecryptNip4(req.Params[1],
sharedSecret); chk.E(err) {
break
}
case "ping":
result = []byte("pong")
harmless = true
default:
rErr = errorf.E("unknown method '%s'", req.Method)
}
if rErr == nil && p.AuthorizeRequest != nil {
if !p.AuthorizeRequest(harmless, ev.Pubkey, secret) {
rErr = fmt.Errorf("unauthorized")
}
}
if res, eventResponse, err = session.MakeResponse(req.ID, hex.Enc(ev.Pubkey),
string(result), rErr); chk.E(err) {
return
}
if err = eventResponse.Sign(p.secretKey); chk.E(err) {
return
}
return
}
func (p *StaticKeySigner) GetConversationKey(pk []byte) (sharedSecret []byte, rErr error) {
if sharedSecret, rErr = encryption.GenerateConversationKey(pk,
p.secretKey.Sec()); chk.E(rErr) {
return
}
return
}
func (p *StaticKeySigner) ComputeSharedSecret(pk []byte) (sharedSecret []byte, rErr error) {
if sharedSecret, rErr = encryption.ComputeSharedSecret(pk,
p.secretKey.Sec()); chk.E(rErr) {
return
}
return
}
func CheckParamsAndKey(req *Request) (pk []byte, rErr error) {
if len(req.Params) != 2 {
rErr = errorf.E("wrong number of arguments to 'nip04_decrypt'")
return
}
if !keys.IsValidPublicKey(req.Params[0]) {
rErr = errorf.E("first argument to 'nip04_decrypt' is not a pubkey string")
return
}
pk = make([]byte, schnorr.PubKeyBytesLen)
if _, rErr = hex.DecBytes(pk, req.Params[0]); chk.E(rErr) {
return
}
return
}

View File

@@ -5,32 +5,22 @@ import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"strings"
"lukechampine.com/frand"
"relay.mleku.dev/hex"
"relay.mleku.dev/p256k"
"relay.mleku.dev/chk"
"relay.mleku.dev/errorf"
"relay.mleku.dev/p256k"
)
// 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(pkh, skh string) (sharedSecret []byte, err error) {
var skb, pkb []byte
if skb, err = hex.Dec(skh); chk.E(err) {
return
}
if pkb, err = hex.Dec(pkh); chk.E(err) {
return
}
func ComputeSharedSecret(pk, sk []byte) (sharedSecret []byte, err error) {
signer := new(p256k.Signer)
if err = signer.InitSec(skb); chk.E(err) {
if err = signer.InitSec(sk); chk.E(err) {
return
}
if sharedSecret, err = signer.ECDH(pkb); chk.E(err) {
if sharedSecret, err = signer.ECDH(pk); chk.E(err) {
return
}
return
@@ -42,7 +32,7 @@ func ComputeSharedSecret(pkh, skh string) (sharedSecret []byte, err error) {
// Returns: base64(encrypted_bytes) + "?iv=" + base64(initialization_vector).
//
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
func EncryptNip4(msg []byte, key []byte) (ct []byte, err error) {
// block size is 16 bytes
iv := make([]byte, 16)
if _, err = frand.Read(iv); chk.E(err) {
@@ -56,7 +46,7 @@ func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
return
}
mode := cipher.NewCBCEncrypter(block, iv)
plaintext := []byte(msg)
plaintext := msg
// add padding
base := len(plaintext)
// this will be a number between 1 and 16 (inclusive), never 0
@@ -75,19 +65,19 @@ func EncryptNip4(msg string, key []byte) (ct []byte, err error) {
// EncryptNip4(message, key).
//
// Deprecated: upgrade to using Decrypt with the NIP-44 algorithm.
func DecryptNip4(content string, key []byte) (msg []byte, err error) {
parts := strings.Split(content, "?iv=")
func DecryptNip4(content, key []byte) (msg []byte, err error) {
parts := bytes.Split(content, []byte("?iv="))
if len(parts) < 2 {
return nil, errorf.E(
"error parsing encrypted message: no initialization vector")
}
var ciphertext []byte
if ciphertext, err = base64.StdEncoding.DecodeString(parts[0]); chk.E(err) {
ciphertext := make([]byte, base64.StdEncoding.EncodedLen(len(parts[0])))
iv := make([]byte, base64.StdEncoding.EncodedLen(len(parts[1])))
if _, err = base64.StdEncoding.Decode(ciphertext, parts[0]); chk.E(err) {
err = errorf.E("error decoding ciphertext from base64: %w", err)
return
}
var iv []byte
if iv, err = base64.StdEncoding.DecodeString(parts[1]); chk.E(err) {
if _, err = base64.StdEncoding.Decode(iv, parts[1]); chk.E(err) {
err = errorf.E("error decoding iv from base64: %w", err)
return
}

View File

@@ -45,8 +45,8 @@ func WithCustomNonce(salt []byte) func(opts *Opts) {
// Encrypt data using a provided symmetric conversation key using NIP-44 encryption (chacha20
// cipher stream and sha256 HMAC).
func Encrypt(plaintext string, conversationKey []byte,
applyOptions ...func(opts *Opts)) (cipherString string,
func Encrypt(plaintext, conversationKey []byte,
applyOptions ...func(opts *Opts)) (cipherString []byte,
err error) {
var o Opts
@@ -67,7 +67,7 @@ func Encrypt(plaintext string, conversationKey []byte,
if enc, cc20nonce, auth, err = getKeys(conversationKey, o.nonce); chk.E(err) {
return
}
plain := []byte(plaintext)
plain := plaintext
size := len(plain)
if size < MinPlaintextSize || size > MaxPlaintextSize {
err = errorf.E("plaintext should be between 1b and 64kB")
@@ -90,25 +90,26 @@ func Encrypt(plaintext string, conversationKey []byte,
ct = append(ct, o.nonce...)
ct = append(ct, cipher...)
ct = append(ct, mac...)
cipherString = base64.StdEncoding.EncodeToString(ct)
cipherString = make([]byte, base64.StdEncoding.EncodedLen(len(ct)))
base64.StdEncoding.Encode(cipherString, ct)
return
}
// Decrypt data that has been encoded using a provided symmetric conversation key using NIP-44
// encryption (chacha20 cipher stream and sha256 HMAC).
func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (plaintext string,
func Decrypt(b64ciphertextWrapped []byte, conversationKey []byte) (plaintext []byte,
err error) {
cLen := len(b64ciphertextWrapped)
if cLen < 132 || cLen > 87472 {
err = errorf.E("invalid payload length: %d", cLen)
return
}
if b64ciphertextWrapped[:1] == "#" {
if b64ciphertextWrapped[0] == '#' {
err = errorf.E("unknown version")
return
}
var decoded []byte
if decoded, err = base64.StdEncoding.DecodeString(b64ciphertextWrapped); chk.E(err) {
if decoded, err = base64.StdEncoding.DecodeString(string(b64ciphertextWrapped)); chk.E(err) {
return
}
if decoded[0] != version {
@@ -143,25 +144,18 @@ func Decrypt(b64ciphertextWrapped string, conversationKey []byte) (plaintext str
err = errorf.E("invalid padding")
return
}
unpadded := padded[2:][:unpaddedLen]
if len(unpadded) == 0 || len(unpadded) != int(unpaddedLen) {
plaintext = padded[2:][:unpaddedLen]
if len(plaintext) == 0 || len(plaintext) != int(unpaddedLen) {
err = errorf.E("invalid padding")
return
}
plaintext = string(unpadded)
return
}
// GenerateConversationKey performs an ECDH key generation hashed with the nip-44-v2 using hkdf.
func GenerateConversationKey(pkh, skh string) (ck []byte, err error) {
if skh >= "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141" ||
skh == "0000000000000000000000000000000000000000000000000000000000000000" {
err = errorf.E("invalid private key: x coordinate %s is not on the secp256k1 curve",
skh)
return
}
func GenerateConversationKey(pk, sk []byte) (ck []byte, err error) {
var shared []byte
if shared, err = ComputeSharedSecret(pkh, skh); chk.E(err) {
if shared, err = ComputeSharedSecret(pk, sk); chk.E(err) {
return
}
ck = hkdf.Extract(sha256.New, shared, []byte("nip44-v2"))

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,7 @@ func GenerateSecretKeyHex() (sks string) {
return hex.Enc(skb)
}
// GetPublicKeyHex generates a public key from a hex encoded secret key.
// GetPublicKeyHex generates a hex encoded public key from a hex encoded secret key.
func GetPublicKeyHex(sk string) (pk string, err error) {
var b []byte
if b, err = hex.Dec(sk); chk.E(err) {
@@ -48,6 +48,20 @@ func GetPublicKeyHex(sk string) (pk string, err error) {
return hex.Enc(signer.Pub()), nil
}
// GetPublicKey generates a public key from a hex encoded secret key.
func GetPublicKey(sk string) (pk []byte, err error) {
var b []byte
if b, err = hex.Dec(sk); chk.E(err) {
return
}
signer := &p256k.Signer{}
if err = signer.InitSec(b); chk.E(err) {
return
}
pk = signer.Pub()
return
}
// SecretBytesToPubKeyHex generates a public key from secret key bytes.
func SecretBytesToPubKeyHex(skb []byte) (pk string, err error) {
signer := &p256k.Signer{}

View File

@@ -55,3 +55,8 @@ func (b *Backend) QueryEvents(c context.T, f *filter.T) (ch event.Ts, err error)
func (b *Backend) SaveEvent(c context.T, ev *event.T) (err error) {
return b.Backend.SaveEvent(c, ev)
}
func (b *Backend) SetLogLevel(level string) {
b.L2.SetLogLevel(level)
b.L1.SetLogLevel(level)
}

View File

@@ -267,3 +267,8 @@ func (b *Backend) Sync() (err error) {
err = errors.Join(err1, err2)
return
}
func (b *Backend) SetLogLevel(level string) {
b.L2.SetLogLevel(level)
b.L1.SetLogLevel(level)
}

View File

@@ -61,8 +61,6 @@ func (s *Signer) InitSec(skb []byte) (err error) {
s.skb = skb
s.SecretKey = &cs.Key
s.PublicKey = cx.Key
// s.ECPublicKey = cp.Key
// needed for ecdh
s.BTCECSec, _ = btcec.PrivKeyFromBytes(s.skb)
return
}

View File

@@ -14,7 +14,7 @@ import (
"relay.mleku.dev/tags"
)
const RELAY = "wss://mleku.realy.lol"
const RELAY = "wss://realy.mleku.dev"
// // test if we can fetch a couple of random events
// func TestSubscribeBasic(t *testing.T) {