crypto.go raw

   1  package marmot
   2  
   3  import (
   4  	"crypto/sha256"
   5  	"errors"
   6  	"sync"
   7  	"time"
   8  
   9  	"golang.org/x/crypto/hkdf"
  10  	"git.smesh.lol/orly/pkg/nostr/crypto/encryption"
  11  	"git.smesh.lol/orly/pkg/nostr/encoders/event"
  12  	"git.smesh.lol/orly/pkg/nostr/encoders/hex"
  13  	"git.smesh.lol/orly/pkg/nostr/interfaces/signer"
  14  )
  15  
  16  // CryptoProvider abstracts the crypto operations marmot needs.
  17  // LocalCrypto wraps signer.I for direct-key mode.
  18  // ProxyCrypto delegates to the browser extension for NIP-07 mode.
  19  type CryptoProvider interface {
  20  	Pub() []byte
  21  	SignEvent(ev *event.E) error
  22  	Nip44Encrypt(peerPub []byte, plaintext []byte) (string, error)
  23  	Nip44Decrypt(peerPub []byte, ciphertext string) (string, error)
  24  }
  25  
  26  // LocalCrypto wraps a signer.I for use as CryptoProvider.
  27  // Used by bridge, bridgebot, and nsec-authenticated sessions.
  28  type LocalCrypto struct{ Sign signer.I }
  29  
  30  func (c *LocalCrypto) Pub() []byte { return c.Sign.Pub() }
  31  
  32  func (c *LocalCrypto) SignEvent(ev *event.E) error { return ev.Sign(c.Sign) }
  33  
  34  func (c *LocalCrypto) convKey(peerPub []byte) ([]byte, error) {
  35  	shared, err := c.Sign.ECDHRaw(peerPub)
  36  	if err != nil {
  37  		return nil, err
  38  	}
  39  	return hkdf.Extract(sha256.New, shared, []byte("nip44-v2")), nil
  40  }
  41  
  42  func (c *LocalCrypto) Nip44Encrypt(peerPub []byte, plaintext []byte) (string, error) {
  43  	convKey, err := c.convKey(peerPub)
  44  	if err != nil {
  45  		return "", err
  46  	}
  47  	return encryption.Encrypt(convKey, plaintext, nil)
  48  }
  49  
  50  func (c *LocalCrypto) Nip44Decrypt(peerPub []byte, ciphertext string) (string, error) {
  51  	convKey, err := c.convKey(peerPub)
  52  	if err != nil {
  53  		return "", err
  54  	}
  55  	return encryption.Decrypt(convKey, ciphertext)
  56  }
  57  
  58  // ProxyCrypto delegates crypto operations to the browser extension via WebSocket.
  59  // Used for NIP-07 and pubkey+sig authenticated sessions.
  60  type ProxyCrypto struct {
  61  	pubkey  []byte
  62  	mu      sync.Mutex
  63  	nextID  int
  64  	pending map[int]chan proxyResult
  65  	sendFn  func(op, peerHex, data string, id int)
  66  }
  67  
  68  type proxyResult struct {
  69  	Result string
  70  	Err    string
  71  }
  72  
  73  func NewProxyCrypto(pubkey []byte, sendFn func(op, peerHex, data string, id int)) *ProxyCrypto {
  74  	return &ProxyCrypto{
  75  		pubkey:  pubkey,
  76  		pending: make(map[int]chan proxyResult),
  77  		sendFn:  sendFn,
  78  	}
  79  }
  80  
  81  func (c *ProxyCrypto) Pub() []byte { return c.pubkey }
  82  
  83  func (c *ProxyCrypto) SignEvent(ev *event.E) error {
  84  	// Build unsigned event JSON for the extension to sign.
  85  	ev.Pubkey = make([]byte, len(c.pubkey))
  86  	copy(ev.Pubkey, c.pubkey)
  87  	ev.ID = ev.GetIDBytes()
  88  	unsigned, err := ev.MarshalJSON()
  89  	if err != nil {
  90  		return err
  91  	}
  92  	signed, err := c.request("signEvent", "", string(unsigned))
  93  	if err != nil {
  94  		return err
  95  	}
  96  	// Parse the signed event to extract sig and verified ID.
  97  	return ev.UnmarshalJSON([]byte(signed))
  98  }
  99  
 100  func (c *ProxyCrypto) Nip44Encrypt(peerPub []byte, plaintext []byte) (string, error) {
 101  	return c.request("nip44Encrypt", hex.Enc(peerPub), string(plaintext))
 102  }
 103  
 104  func (c *ProxyCrypto) Nip44Decrypt(peerPub []byte, ciphertext string) (string, error) {
 105  	return c.request("nip44Decrypt", hex.Enc(peerPub), ciphertext)
 106  }
 107  
 108  func (c *ProxyCrypto) request(op, peerHex, data string) (string, error) {
 109  	c.mu.Lock()
 110  	id := c.nextID
 111  	c.nextID++
 112  	ch := make(chan proxyResult, 1)
 113  	c.pending[id] = ch
 114  	c.mu.Unlock()
 115  
 116  	c.sendFn(op, peerHex, data, id)
 117  
 118  	select {
 119  	case res := <-ch:
 120  		if res.Err != "" {
 121  			return "", errors.New(res.Err)
 122  		}
 123  		return res.Result, nil
 124  	case <-time.After(15 * time.Second):
 125  		c.mu.Lock()
 126  		delete(c.pending, id)
 127  		c.mu.Unlock()
 128  		return "", errors.New("crypto proxy timeout")
 129  	}
 130  }
 131  
 132  // Resolve routes a crypto_resp from the browser to the waiting goroutine.
 133  func (c *ProxyCrypto) Resolve(id int, result, errMsg string) {
 134  	c.mu.Lock()
 135  	ch, ok := c.pending[id]
 136  	if ok {
 137  		delete(c.pending, id)
 138  	}
 139  	c.mu.Unlock()
 140  	if ok {
 141  		ch <- proxyResult{result, errMsg}
 142  	}
 143  }
 144  
 145  // Close unblocks all pending requests with an error (called on WS disconnect).
 146  func (c *ProxyCrypto) Close() {
 147  	c.mu.Lock()
 148  	for id, ch := range c.pending {
 149  		ch <- proxyResult{Err: "connection closed"}
 150  		delete(c.pending, id)
 151  	}
 152  	c.mu.Unlock()
 153  }
 154