ncc-05 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -84,7 +84,8 @@ export class NCC05Resolver {
84
84
  authors: [hexPubkey],
85
85
  kinds: [10002]
86
86
  });
87
- if (relayListEvent) {
87
+ // Security: Verify NIP-65 event signature and author
88
+ if (relayListEvent && verifyEvent(relayListEvent) && relayListEvent.pubkey === hexPubkey) {
88
89
  const discoveredRelays = relayListEvent.tags
89
90
  .filter(t => t[0] === 'r')
90
91
  .map(t => t[1]);
@@ -104,46 +105,56 @@ export class NCC05Resolver {
104
105
  const result = await Promise.race([queryPromise, timeoutPromise]);
105
106
  if (!result || (Array.isArray(result) && result.length === 0))
106
107
  return null;
108
+ // 2. Filter for valid signatures, correct author, and sort by created_at
107
109
  const validEvents = result
108
- .filter(e => verifyEvent(e))
110
+ .filter(e => e.pubkey === hexPubkey && verifyEvent(e))
109
111
  .sort((a, b) => b.created_at - a.created_at);
110
112
  if (validEvents.length === 0)
111
113
  return null;
112
114
  const latestEvent = validEvents[0];
113
115
  try {
114
116
  let content = latestEvent.content;
115
- // Handle "Wrapped" multi-recipient content
116
- if (content.includes('"wraps"') && secretKey) {
117
+ // Security: Robust multi-recipient detection
118
+ const isWrapped = content.includes('"wraps"') &&
119
+ content.includes('"ciphertext"') &&
120
+ content.startsWith('{');
121
+ if (isWrapped && secretKey) {
117
122
  const wrapped = JSON.parse(content);
118
123
  const myPk = getPublicKey(secretKey);
119
124
  const myWrap = wrapped.wraps[myPk];
120
125
  if (myWrap) {
121
126
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
122
127
  const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
128
+ // Convert hex symmetric key back to Uint8Array for NIP-44 decryption
123
129
  const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
124
130
  const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
125
131
  content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
126
132
  }
133
+ else {
134
+ return null; // Not intended for us
135
+ }
127
136
  }
128
- else if (secretKey) {
129
- // Standard NIP-44
137
+ else if (secretKey && !content.startsWith('{')) {
138
+ // Standard NIP-44 (likely encrypted if not starting with {)
130
139
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
131
140
  content = nip44.decrypt(latestEvent.content, conversationKey);
132
141
  }
142
+ // Security: Safe JSON parsing
133
143
  const payload = JSON.parse(content);
134
- if (!payload.endpoints)
144
+ if (!payload || !payload.endpoints || !Array.isArray(payload.endpoints)) {
135
145
  return null;
146
+ }
136
147
  // Freshness validation
137
148
  const now = Math.floor(Date.now() / 1000);
138
149
  if (now > payload.updated_at + payload.ttl) {
139
150
  if (options.strict)
140
151
  return null;
141
- console.warn('NCC-05 record has expired');
152
+ console.warn('NCC-05 record expired');
142
153
  }
143
154
  return payload;
144
155
  }
145
156
  catch (e) {
146
- return null;
157
+ return null; // Decryption or parsing failed
147
158
  }
148
159
  }
149
160
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncc-05",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Nostr Community Convention 05 - Identity-Bound Service Locator Resolution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -166,7 +166,8 @@ export class NCC05Resolver {
166
166
  authors: [hexPubkey],
167
167
  kinds: [10002]
168
168
  });
169
- if (relayListEvent) {
169
+ // Security: Verify NIP-65 event signature and author
170
+ if (relayListEvent && verifyEvent(relayListEvent) && relayListEvent.pubkey === hexPubkey) {
170
171
  const discoveredRelays = relayListEvent.tags
171
172
  .filter(t => t[0] === 'r')
172
173
  .map(t => t[1]);
@@ -189,8 +190,9 @@ export class NCC05Resolver {
189
190
 
190
191
  if (!result || (Array.isArray(result) && result.length === 0)) return null;
191
192
 
193
+ // 2. Filter for valid signatures, correct author, and sort by created_at
192
194
  const validEvents = (result as Event[])
193
- .filter(e => verifyEvent(e))
195
+ .filter(e => e.pubkey === hexPubkey && verifyEvent(e))
194
196
  .sort((a, b) => b.created_at - a.created_at);
195
197
 
196
198
  if (validEvents.length === 0) return null;
@@ -199,8 +201,12 @@ export class NCC05Resolver {
199
201
  try {
200
202
  let content = latestEvent.content;
201
203
 
202
- // Handle "Wrapped" multi-recipient content
203
- if (content.includes('"wraps"') && secretKey) {
204
+ // Security: Robust multi-recipient detection
205
+ const isWrapped = content.includes('"wraps"') &&
206
+ content.includes('"ciphertext"') &&
207
+ content.startsWith('{');
208
+
209
+ if (isWrapped && secretKey) {
204
210
  const wrapped = JSON.parse(content) as WrappedContent;
205
211
  const myPk = getPublicKey(secretKey);
206
212
  const myWrap = wrapped.wraps[myPk];
@@ -208,30 +214,41 @@ export class NCC05Resolver {
208
214
  if (myWrap) {
209
215
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
210
216
  const symmetricKeyHex = nip44.decrypt(myWrap, conversationKey);
211
- const symmetricKey = new Uint8Array(symmetricKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
212
217
 
213
- const sessionConversationKey = nip44.getConversationKey(symmetricKey, getPublicKey(symmetricKey));
218
+ // Convert hex symmetric key back to Uint8Array for NIP-44 decryption
219
+ const symmetricKey = new Uint8Array(
220
+ symmetricKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16))
221
+ );
222
+
223
+ const sessionConversationKey = nip44.getConversationKey(
224
+ symmetricKey, getPublicKey(symmetricKey)
225
+ );
214
226
  content = nip44.decrypt(wrapped.ciphertext, sessionConversationKey);
227
+ } else {
228
+ return null; // Not intended for us
215
229
  }
216
- } else if (secretKey) {
217
- // Standard NIP-44
230
+ } else if (secretKey && !content.startsWith('{')) {
231
+ // Standard NIP-44 (likely encrypted if not starting with {)
218
232
  const conversationKey = nip44.getConversationKey(secretKey, hexPubkey);
219
233
  content = nip44.decrypt(latestEvent.content, conversationKey);
220
234
  }
221
235
 
236
+ // Security: Safe JSON parsing
222
237
  const payload = JSON.parse(content) as NCC05Payload;
223
- if (!payload.endpoints) return null;
238
+ if (!payload || !payload.endpoints || !Array.isArray(payload.endpoints)) {
239
+ return null;
240
+ }
224
241
 
225
242
  // Freshness validation
226
243
  const now = Math.floor(Date.now() / 1000);
227
244
  if (now > payload.updated_at + payload.ttl) {
228
245
  if (options.strict) return null;
229
- console.warn('NCC-05 record has expired');
246
+ console.warn('NCC-05 record expired');
230
247
  }
231
248
 
232
249
  return payload;
233
250
  } catch (e) {
234
- return null;
251
+ return null; // Decryption or parsing failed
235
252
  }
236
253
  }
237
254
 
@@ -348,4 +365,4 @@ export class NCC05Publisher {
348
365
  close(relays: string[]) {
349
366
  this.pool.close(relays);
350
367
  }
351
- }
368
+ }