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