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 +20 -9
- package/package.json +1 -1
- package/src/index.ts +29 -12
package/dist/index.js
CHANGED
|
@@ -84,7 +84,8 @@ export class NCC05Resolver {
|
|
|
84
84
|
authors: [hexPubkey],
|
|
85
85
|
kinds: [10002]
|
|
86
86
|
});
|
|
87
|
-
|
|
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
|
-
//
|
|
116
|
-
|
|
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
|
|
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
package/src/index.ts
CHANGED
|
@@ -166,7 +166,8 @@ export class NCC05Resolver {
|
|
|
166
166
|
authors: [hexPubkey],
|
|
167
167
|
kinds: [10002]
|
|
168
168
|
});
|
|
169
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
+
}
|