@neuraiproject/neurai-depin-msg 1.0.0
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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/neurai-depin-msg.js +4709 -0
- package/dist/neurai-depin-msg.min.js +24 -0
- package/package.json +39 -0
- package/src/index.js +597 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @neuraiproject/neurai-depin-msg
|
|
3
|
+
*
|
|
4
|
+
* Build encrypted DePIN messages for Neurai blockchain
|
|
5
|
+
* Self-contained library with bundled secp256k1
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as secp256k1 from '@bitcoinerlab/secp256k1';
|
|
9
|
+
|
|
10
|
+
// ============================================
|
|
11
|
+
// SERIALIZATION UTILITIES
|
|
12
|
+
// ============================================
|
|
13
|
+
|
|
14
|
+
function writeCompactSize(value) {
|
|
15
|
+
if (value < 0) throw new Error('CompactSize cannot be negative');
|
|
16
|
+
|
|
17
|
+
if (value < 253) {
|
|
18
|
+
return new Uint8Array([value]);
|
|
19
|
+
} else if (value <= 0xffff) {
|
|
20
|
+
const buf = new Uint8Array(3);
|
|
21
|
+
buf[0] = 253;
|
|
22
|
+
buf[1] = value & 0xff;
|
|
23
|
+
buf[2] = (value >> 8) & 0xff;
|
|
24
|
+
return buf;
|
|
25
|
+
} else if (value <= 0xffffffff) {
|
|
26
|
+
const buf = new Uint8Array(5);
|
|
27
|
+
buf[0] = 254;
|
|
28
|
+
buf[1] = value & 0xff;
|
|
29
|
+
buf[2] = (value >> 8) & 0xff;
|
|
30
|
+
buf[3] = (value >> 16) & 0xff;
|
|
31
|
+
buf[4] = (value >> 24) & 0xff;
|
|
32
|
+
return buf;
|
|
33
|
+
} else {
|
|
34
|
+
const buf = new Uint8Array(9);
|
|
35
|
+
buf[0] = 255;
|
|
36
|
+
const low = value >>> 0;
|
|
37
|
+
const high = Math.floor(value / 0x100000000) >>> 0;
|
|
38
|
+
buf[1] = low & 0xff;
|
|
39
|
+
buf[2] = (low >> 8) & 0xff;
|
|
40
|
+
buf[3] = (low >> 16) & 0xff;
|
|
41
|
+
buf[4] = (low >> 24) & 0xff;
|
|
42
|
+
buf[5] = high & 0xff;
|
|
43
|
+
buf[6] = (high >> 8) & 0xff;
|
|
44
|
+
buf[7] = (high >> 16) & 0xff;
|
|
45
|
+
buf[8] = (high >> 24) & 0xff;
|
|
46
|
+
return buf;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function serializeString(str) {
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
const strBytes = encoder.encode(str);
|
|
53
|
+
return concatBytes(writeCompactSize(strBytes.length), strBytes);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function serializeVector(data) {
|
|
57
|
+
return concatBytes(writeCompactSize(data.length), data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function serializeInt64(value) {
|
|
61
|
+
const buf = new Uint8Array(8);
|
|
62
|
+
const low = value >>> 0;
|
|
63
|
+
const high = Math.floor(value / 0x100000000) >>> 0;
|
|
64
|
+
buf[0] = low & 0xff;
|
|
65
|
+
buf[1] = (low >> 8) & 0xff;
|
|
66
|
+
buf[2] = (low >> 16) & 0xff;
|
|
67
|
+
buf[3] = (low >> 24) & 0xff;
|
|
68
|
+
buf[4] = high & 0xff;
|
|
69
|
+
buf[5] = (high >> 8) & 0xff;
|
|
70
|
+
buf[6] = (high >> 16) & 0xff;
|
|
71
|
+
buf[7] = (high >> 24) & 0xff;
|
|
72
|
+
return buf;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function concatBytes(...arrays) {
|
|
76
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
77
|
+
const result = new Uint8Array(totalLength);
|
|
78
|
+
let offset = 0;
|
|
79
|
+
for (const arr of arrays) {
|
|
80
|
+
result.set(arr, offset);
|
|
81
|
+
offset += arr.length;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hexToBytes(hex) {
|
|
87
|
+
if (hex.length % 2 !== 0) throw new Error('Hex must have even length');
|
|
88
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
89
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
90
|
+
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
91
|
+
}
|
|
92
|
+
return bytes;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function bytesToHex(bytes) {
|
|
96
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Encode ECDSA signature to DER format
|
|
101
|
+
* Takes 64-byte raw signature (32 bytes r + 32 bytes s) and converts to DER
|
|
102
|
+
*/
|
|
103
|
+
function encodeDER(signature) {
|
|
104
|
+
if (signature.length !== 64) {
|
|
105
|
+
throw new Error('Raw signature must be 64 bytes');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const r = signature.slice(0, 32);
|
|
109
|
+
const s = signature.slice(32, 64);
|
|
110
|
+
|
|
111
|
+
// Helper to encode an integer in DER format
|
|
112
|
+
function encodeInteger(value) {
|
|
113
|
+
// Remove leading zeros (but keep one if needed for sign bit)
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < value.length - 1 && value[i] === 0 && (value[i + 1] & 0x80) === 0) {
|
|
116
|
+
i++;
|
|
117
|
+
}
|
|
118
|
+
const trimmed = value.slice(i);
|
|
119
|
+
|
|
120
|
+
// If high bit is set, prepend 0x00 to indicate positive number
|
|
121
|
+
const needsPadding = (trimmed[0] & 0x80) !== 0;
|
|
122
|
+
const paddedValue = needsPadding
|
|
123
|
+
? concatBytes(new Uint8Array([0x00]), trimmed)
|
|
124
|
+
: trimmed;
|
|
125
|
+
|
|
126
|
+
// DER integer: 0x02 (INTEGER tag) + length + value
|
|
127
|
+
return concatBytes(
|
|
128
|
+
new Uint8Array([0x02, paddedValue.length]),
|
|
129
|
+
paddedValue
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const rDER = encodeInteger(r);
|
|
134
|
+
const sDER = encodeInteger(s);
|
|
135
|
+
|
|
136
|
+
// DER sequence: 0x30 (SEQUENCE tag) + length + rDER + sDER
|
|
137
|
+
const sequenceLength = rDER.length + sDER.length;
|
|
138
|
+
return concatBytes(
|
|
139
|
+
new Uint8Array([0x30, sequenceLength]),
|
|
140
|
+
rDER,
|
|
141
|
+
sDER
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================
|
|
146
|
+
// BASE58 / WIF UTILITIES
|
|
147
|
+
// ============================================
|
|
148
|
+
|
|
149
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
150
|
+
|
|
151
|
+
function base58Decode(str) {
|
|
152
|
+
const bytes = [];
|
|
153
|
+
for (let i = 0; i < str.length; i++) {
|
|
154
|
+
const charIndex = BASE58_ALPHABET.indexOf(str[i]);
|
|
155
|
+
if (charIndex === -1) throw new Error('Invalid Base58 character: ' + str[i]);
|
|
156
|
+
|
|
157
|
+
let carry = charIndex;
|
|
158
|
+
for (let j = 0; j < bytes.length; j++) {
|
|
159
|
+
carry += bytes[j] * 58;
|
|
160
|
+
bytes[j] = carry & 0xff;
|
|
161
|
+
carry >>= 8;
|
|
162
|
+
}
|
|
163
|
+
while (carry > 0) {
|
|
164
|
+
bytes.push(carry & 0xff);
|
|
165
|
+
carry >>= 8;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < str.length && str[i] === '1'; i++) {
|
|
170
|
+
bytes.push(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new Uint8Array(bytes.reverse());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function wifToHex(wif) {
|
|
177
|
+
const decoded = base58Decode(wif);
|
|
178
|
+
|
|
179
|
+
if (decoded.length < 37) {
|
|
180
|
+
throw new Error('Invalid WIF: too short');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const payload = decoded.slice(0, -4);
|
|
184
|
+
const checksum = decoded.slice(-4);
|
|
185
|
+
const hash = await doubleSha256(payload);
|
|
186
|
+
|
|
187
|
+
for (let i = 0; i < 4; i++) {
|
|
188
|
+
if (checksum[i] !== hash[i]) {
|
|
189
|
+
throw new Error('Invalid WIF: checksum mismatch');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let privateKeyBytes;
|
|
194
|
+
if (payload.length === 34) {
|
|
195
|
+
privateKeyBytes = payload.slice(1, 33);
|
|
196
|
+
} else if (payload.length === 33) {
|
|
197
|
+
privateKeyBytes = payload.slice(1, 33);
|
|
198
|
+
} else {
|
|
199
|
+
throw new Error('Invalid WIF: unexpected length ' + payload.length);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return bytesToHex(privateKeyBytes);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isWIF(str) {
|
|
206
|
+
return /^[5KLcT][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(str);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================
|
|
210
|
+
// CRYPTOGRAPHIC FUNCTIONS
|
|
211
|
+
// ============================================
|
|
212
|
+
|
|
213
|
+
async function sha256(data) {
|
|
214
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
215
|
+
return new Uint8Array(hashBuffer);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function doubleSha256(data) {
|
|
219
|
+
const first = await sha256(data);
|
|
220
|
+
return sha256(first);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function ripemd160(data) {
|
|
224
|
+
let h0 = 0x67452301, h1 = 0xefcdab89, h2 = 0x98badcfe, h3 = 0x10325476, h4 = 0xc3d2e1f0;
|
|
225
|
+
|
|
226
|
+
const K1 = [0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e];
|
|
227
|
+
const K2 = [0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000];
|
|
228
|
+
|
|
229
|
+
const R1 = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,7,4,13,1,10,6,15,3,12,0,9,5,2,14,11,8,3,10,14,4,9,15,8,1,2,7,0,6,13,11,5,12,1,9,11,10,0,8,12,4,13,3,7,15,14,5,6,2,4,0,5,9,7,12,2,10,14,1,3,8,11,6,15,13];
|
|
230
|
+
const R2 = [5,14,7,0,9,2,11,4,13,6,15,8,1,10,3,12,6,11,3,7,0,13,5,10,14,15,8,12,4,9,1,2,15,5,1,3,7,14,6,9,11,8,12,2,10,0,4,13,8,6,4,1,3,11,15,0,5,12,2,13,9,7,10,14,12,15,10,4,1,5,8,7,6,2,13,14,0,3,9,11];
|
|
231
|
+
const S1 = [11,14,15,12,5,8,7,9,11,13,14,15,6,7,9,8,7,6,8,13,11,9,7,15,7,12,15,9,11,7,13,12,11,13,6,7,14,9,13,15,14,8,13,6,5,12,7,5,11,12,14,15,14,15,9,8,9,14,5,6,8,6,5,12,9,15,5,11,6,8,13,12,5,12,13,14,11,8,5,6];
|
|
232
|
+
const S2 = [8,9,9,11,13,15,15,5,7,7,8,11,14,14,12,6,9,13,15,7,12,8,9,11,7,7,12,7,6,15,13,11,9,7,15,11,8,6,6,14,12,13,5,14,13,13,7,5,15,5,8,11,14,14,6,14,6,9,12,9,12,5,15,8,8,5,12,9,12,5,14,6,8,13,6,5,15,13,11,11];
|
|
233
|
+
|
|
234
|
+
function rotl(x, n) { return ((x << n) | (x >>> (32 - n))) >>> 0; }
|
|
235
|
+
|
|
236
|
+
const bitLen = data.length * 8;
|
|
237
|
+
const padLen = (64 - ((data.length + 9) % 64)) % 64;
|
|
238
|
+
const padded = new Uint8Array(data.length + 1 + padLen + 8);
|
|
239
|
+
padded.set(data);
|
|
240
|
+
padded[data.length] = 0x80;
|
|
241
|
+
const view = new DataView(padded.buffer);
|
|
242
|
+
view.setUint32(padded.length - 8, bitLen, true);
|
|
243
|
+
|
|
244
|
+
const blocks = padded.length / 64;
|
|
245
|
+
|
|
246
|
+
for (let i = 0; i < blocks; i++) {
|
|
247
|
+
const X = new Uint32Array(16);
|
|
248
|
+
for (let j = 0; j < 16; j++) {
|
|
249
|
+
const offset = i * 64 + j * 4;
|
|
250
|
+
X[j] = padded[offset] | (padded[offset + 1] << 8) | (padded[offset + 2] << 16) | (padded[offset + 3] << 24);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let a1 = h0, b1 = h1, c1 = h2, d1 = h3, e1 = h4;
|
|
254
|
+
let a2 = h0, b2 = h1, c2 = h2, d2 = h3, e2 = h4;
|
|
255
|
+
|
|
256
|
+
for (let j = 0; j < 80; j++) {
|
|
257
|
+
const round = Math.floor(j / 16);
|
|
258
|
+
let f1, f2;
|
|
259
|
+
|
|
260
|
+
if (round === 0) { f1 = b1 ^ c1 ^ d1; f2 = b2 ^ (c2 | ~d2); }
|
|
261
|
+
else if (round === 1) { f1 = (b1 & c1) | (~b1 & d1); f2 = (b2 & d2) | (c2 & ~d2); }
|
|
262
|
+
else if (round === 2) { f1 = (b1 | ~c1) ^ d1; f2 = (b2 | ~c2) ^ d2; }
|
|
263
|
+
else if (round === 3) { f1 = (b1 & d1) | (c1 & ~d1); f2 = (b2 & c2) | (~b2 & d2); }
|
|
264
|
+
else { f1 = b1 ^ (c1 | ~d1); f2 = b2 ^ c2 ^ d2; }
|
|
265
|
+
|
|
266
|
+
const t1 = (rotl((a1 + f1 + X[R1[j]] + K1[round]) >>> 0, S1[j]) + e1) >>> 0;
|
|
267
|
+
a1 = e1; e1 = d1; d1 = rotl(c1, 10); c1 = b1; b1 = t1;
|
|
268
|
+
|
|
269
|
+
const t2 = (rotl((a2 + f2 + X[R2[j]] + K2[round]) >>> 0, S2[j]) + e2) >>> 0;
|
|
270
|
+
a2 = e2; e2 = d2; d2 = rotl(c2, 10); c2 = b2; b2 = t2;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const t = (h1 + c1 + d2) >>> 0;
|
|
274
|
+
h1 = (h2 + d1 + e2) >>> 0;
|
|
275
|
+
h2 = (h3 + e1 + a2) >>> 0;
|
|
276
|
+
h3 = (h4 + a1 + b2) >>> 0;
|
|
277
|
+
h4 = (h0 + b1 + c2) >>> 0;
|
|
278
|
+
h0 = t;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const result = new Uint8Array(20);
|
|
282
|
+
const rv = new DataView(result.buffer);
|
|
283
|
+
rv.setUint32(0, h0, true); rv.setUint32(4, h1, true);
|
|
284
|
+
rv.setUint32(8, h2, true); rv.setUint32(12, h3, true);
|
|
285
|
+
rv.setUint32(16, h4, true);
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function hash160(data) {
|
|
290
|
+
const sha = await sha256(data);
|
|
291
|
+
return ripemd160(sha);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function kdfSha256(sharedSecret, outputLen) {
|
|
295
|
+
const output = new Uint8Array(outputLen);
|
|
296
|
+
let offset = 0;
|
|
297
|
+
let counter = 1;
|
|
298
|
+
|
|
299
|
+
while (offset < outputLen) {
|
|
300
|
+
const counterBytes = new Uint8Array(4);
|
|
301
|
+
counterBytes[0] = (counter >> 24) & 0xff;
|
|
302
|
+
counterBytes[1] = (counter >> 16) & 0xff;
|
|
303
|
+
counterBytes[2] = (counter >> 8) & 0xff;
|
|
304
|
+
counterBytes[3] = counter & 0xff;
|
|
305
|
+
|
|
306
|
+
const data = concatBytes(sharedSecret, counterBytes);
|
|
307
|
+
const hash = await sha256(data);
|
|
308
|
+
|
|
309
|
+
const remaining = outputLen - offset;
|
|
310
|
+
const toCopy = Math.min(remaining, 32);
|
|
311
|
+
output.set(hash.slice(0, toCopy), offset);
|
|
312
|
+
offset += toCopy;
|
|
313
|
+
counter++;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return output;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function randomBytes(length) {
|
|
320
|
+
const bytes = new Uint8Array(length);
|
|
321
|
+
crypto.getRandomValues(bytes);
|
|
322
|
+
return bytes;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function aes256CbcEncrypt(plaintext, key, iv) {
|
|
326
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
327
|
+
'raw', key, { name: 'AES-CBC' }, false, ['encrypt']
|
|
328
|
+
);
|
|
329
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
330
|
+
{ name: 'AES-CBC', iv }, cryptoKey, plaintext
|
|
331
|
+
);
|
|
332
|
+
return new Uint8Array(ciphertext);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function hmacSha256(key, data) {
|
|
336
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
337
|
+
'raw',
|
|
338
|
+
key,
|
|
339
|
+
{ name: 'HMAC', hash: { name: 'SHA-256' } },
|
|
340
|
+
false,
|
|
341
|
+
['sign']
|
|
342
|
+
);
|
|
343
|
+
const mac = await crypto.subtle.sign('HMAC', cryptoKey, data);
|
|
344
|
+
return new Uint8Array(mac);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ============================================
|
|
348
|
+
// ECIES ENCRYPTION (using @bitcoinerlab/secp256k1)
|
|
349
|
+
// ============================================
|
|
350
|
+
|
|
351
|
+
async function eciesEncrypt(plaintext, recipientPubKeys) {
|
|
352
|
+
// Neurai Core-compatible hybrid ECIES (see src/depinecies.cpp)
|
|
353
|
+
// - Ephemeral keypair per message
|
|
354
|
+
// - AES key derived from ephemeral privkey via KDF_SHA256
|
|
355
|
+
// - Payload: [IV(16) || ciphertext || HMAC_SHA256(aesKey, ciphertext)]
|
|
356
|
+
// - Per-recipient package: [recipientIV(16) || AES256_CBC(encKey, aesKey) || HMAC_SHA256(encKey, encryptedAESKey)]
|
|
357
|
+
// - encKey derived from ECDH secret (secp256k1_ecdh default), then KDF_SHA256
|
|
358
|
+
|
|
359
|
+
// Generate ephemeral key pair
|
|
360
|
+
const ephemeralPrivKey = randomBytes(32);
|
|
361
|
+
const ephemeralPubKey = secp256k1.pointFromScalar(ephemeralPrivKey, true);
|
|
362
|
+
if (!(ephemeralPubKey instanceof Uint8Array) || ephemeralPubKey.length !== 33) {
|
|
363
|
+
throw new Error('Failed to generate ephemeral public key');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Derive AES key from ephemeral private key (matches KDF_SHA256 in C++)
|
|
367
|
+
const aesKey = await kdfSha256(ephemeralPrivKey, 32);
|
|
368
|
+
|
|
369
|
+
// Encrypt message with AES-256-CBC (PKCS7 padding)
|
|
370
|
+
const iv = randomBytes(16);
|
|
371
|
+
const ciphertext = await aes256CbcEncrypt(plaintext, aesKey, iv);
|
|
372
|
+
|
|
373
|
+
// HMAC over ciphertext only (matches C++)
|
|
374
|
+
const payloadHmac = await hmacSha256(aesKey, ciphertext);
|
|
375
|
+
const payload = concatBytes(iv, ciphertext, payloadHmac);
|
|
376
|
+
|
|
377
|
+
// For each recipient, encrypt the AES key
|
|
378
|
+
const recipientKeys = new Map();
|
|
379
|
+
|
|
380
|
+
for (const recipientPubKey of recipientPubKeys) {
|
|
381
|
+
if (!(recipientPubKey instanceof Uint8Array) || recipientPubKey.length !== 33) {
|
|
382
|
+
throw new Error('Recipient pubkey must be 33 bytes compressed');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ECDH secret must match libsecp256k1's default: SHA256(compressed(shared_point))
|
|
386
|
+
// See src/secp256k1/src/modules/ecdh/main_impl.h
|
|
387
|
+
const sharedPointCompressed = secp256k1.pointMultiply(recipientPubKey, ephemeralPrivKey, true);
|
|
388
|
+
const sharedSecret = await sha256(sharedPointCompressed);
|
|
389
|
+
|
|
390
|
+
// Derive per-recipient encryption key
|
|
391
|
+
const encKey = await kdfSha256(sharedSecret, 32);
|
|
392
|
+
|
|
393
|
+
// Encrypt the AES key using AES-256-CBC with random per-recipient IV
|
|
394
|
+
const recipientIV = randomBytes(16);
|
|
395
|
+
const encryptedAESKey = await aes256CbcEncrypt(aesKey, encKey, recipientIV);
|
|
396
|
+
|
|
397
|
+
// HMAC over encrypted AES key
|
|
398
|
+
const recipientHmac = await hmacSha256(encKey, encryptedAESKey);
|
|
399
|
+
|
|
400
|
+
const recipientPackage = concatBytes(recipientIV, encryptedAESKey, recipientHmac);
|
|
401
|
+
|
|
402
|
+
// Map key is address hash160 (CKeyID): Hash160(serialized pubkey)
|
|
403
|
+
const keyHash = await hash160(recipientPubKey);
|
|
404
|
+
const keyHashHex = bytesToHex(keyHash);
|
|
405
|
+
recipientKeys.set(keyHashHex, recipientPackage);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
ephemeralPubKey,
|
|
410
|
+
encryptedPayload: payload,
|
|
411
|
+
recipientKeys
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function serializeEciesMessage(msg) {
|
|
416
|
+
const parts = [];
|
|
417
|
+
parts.push(serializeVector(msg.ephemeralPubKey));
|
|
418
|
+
|
|
419
|
+
// CECIESEncryptedMessage serializes encryptedPayload as a vector (CompactSize + bytes)
|
|
420
|
+
parts.push(serializeVector(msg.encryptedPayload));
|
|
421
|
+
|
|
422
|
+
// recipientKeys is a std::map<uint160, vector<unsigned char>>
|
|
423
|
+
// Serialize as: CompactSize(count) + repeated (uint160 key raw 20 bytes) + (vector value)
|
|
424
|
+
const entries = Array.from(msg.recipientKeys.entries()).map(([hash160Hex, recipientPackage]) => {
|
|
425
|
+
const keyBytes = hexToBytes(hash160Hex);
|
|
426
|
+
if (keyBytes.length !== 20) throw new Error('recipient key hash160 must be 20 bytes');
|
|
427
|
+
return { keyBytes, recipientPackage };
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Deterministic order (helps reproducibility; C++ map is ordered)
|
|
431
|
+
entries.sort((a, b) => {
|
|
432
|
+
for (let i = 0; i < 20; i++) {
|
|
433
|
+
if (a.keyBytes[i] !== b.keyBytes[i]) return a.keyBytes[i] - b.keyBytes[i];
|
|
434
|
+
}
|
|
435
|
+
return 0;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
parts.push(writeCompactSize(entries.length));
|
|
439
|
+
for (const { keyBytes, recipientPackage } of entries) {
|
|
440
|
+
parts.push(keyBytes);
|
|
441
|
+
parts.push(serializeVector(recipientPackage));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return concatBytes(...parts);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ============================================
|
|
448
|
+
// MAIN API
|
|
449
|
+
// ============================================
|
|
450
|
+
|
|
451
|
+
async function buildDepinMessage(params) {
|
|
452
|
+
// Validate
|
|
453
|
+
if (!params.token) throw new Error('Token is required');
|
|
454
|
+
if (!params.senderAddress) throw new Error('Sender address is required');
|
|
455
|
+
if (!params.senderPubKey || params.senderPubKey.length !== 66) {
|
|
456
|
+
throw new Error('Sender public key must be 66 hex characters');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle private key - can be WIF or hex
|
|
460
|
+
let privateKeyHex = params.privateKey;
|
|
461
|
+
if (!privateKeyHex) {
|
|
462
|
+
throw new Error('Private key is required');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Auto-detect and convert WIF to hex
|
|
466
|
+
if (isWIF(privateKeyHex)) {
|
|
467
|
+
console.log('Detected WIF format, converting to hex...');
|
|
468
|
+
privateKeyHex = await wifToHex(privateKeyHex);
|
|
469
|
+
console.log('Private key converted successfully');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (privateKeyHex.length !== 64) {
|
|
473
|
+
throw new Error('Private key must be 64 hex characters (or WIF format)');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!params.message) throw new Error('Message is required');
|
|
477
|
+
if (!params.recipientPubKeys || params.recipientPubKeys.length === 0) {
|
|
478
|
+
throw new Error('At least one recipient is required');
|
|
479
|
+
}
|
|
480
|
+
if (!params.timestamp || params.timestamp <= 0) {
|
|
481
|
+
throw new Error('Timestamp must be positive');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Convert to bytes
|
|
485
|
+
const privateKey = hexToBytes(privateKeyHex);
|
|
486
|
+
const senderPubKey = hexToBytes(params.senderPubKey);
|
|
487
|
+
|
|
488
|
+
// Parse recipients
|
|
489
|
+
const recipientPubKeys = params.recipientPubKeys.map(pk => {
|
|
490
|
+
if (pk.length !== 66) throw new Error('Recipient pubkey must be 66 hex chars');
|
|
491
|
+
return hexToBytes(pk);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Include sender so they can decrypt their own messages
|
|
495
|
+
const senderHex = params.senderPubKey.toLowerCase();
|
|
496
|
+
if (!params.recipientPubKeys.some(pk => pk.toLowerCase() === senderHex)) {
|
|
497
|
+
recipientPubKeys.push(senderPubKey);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Encode message
|
|
501
|
+
const encoder = new TextEncoder();
|
|
502
|
+
const messageBytes = encoder.encode(params.message);
|
|
503
|
+
|
|
504
|
+
// ECIES encrypt
|
|
505
|
+
const eciesMsg = await eciesEncrypt(messageBytes, recipientPubKeys);
|
|
506
|
+
const encryptedPayload = serializeEciesMessage(eciesMsg);
|
|
507
|
+
|
|
508
|
+
// Build hash data for signing
|
|
509
|
+
const hashData = concatBytes(
|
|
510
|
+
serializeString(params.token),
|
|
511
|
+
serializeString(params.senderAddress),
|
|
512
|
+
serializeInt64(params.timestamp),
|
|
513
|
+
serializeVector(encryptedPayload)
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
// Neurai Core uses CHashWriter (SER_GETHASH), which is double-SHA256.
|
|
517
|
+
// This MUST match VerifyDepinMessageSignature in src/depinmsgpool.cpp.
|
|
518
|
+
const messageHashBytes = await doubleSha256(hashData);
|
|
519
|
+
// Neurai (like Bitcoin) typically displays uint256 hashes byte-reversed.
|
|
520
|
+
// This makes `messageHash` match what you'll see in debug.log.
|
|
521
|
+
const messageHash = bytesToHex(messageHashBytes.slice().reverse());
|
|
522
|
+
|
|
523
|
+
// Sign with secp256k1
|
|
524
|
+
const sigResult = secp256k1.sign(messageHashBytes, privateKey);
|
|
525
|
+
|
|
526
|
+
// Convert to DER format if needed
|
|
527
|
+
let signature;
|
|
528
|
+
if (sigResult instanceof Uint8Array) {
|
|
529
|
+
// Check if it's raw (64 bytes) or already DER (70-72 bytes typically)
|
|
530
|
+
if (sigResult.length === 64) {
|
|
531
|
+
// Raw signature - convert to DER
|
|
532
|
+
signature = encodeDER(sigResult);
|
|
533
|
+
} else {
|
|
534
|
+
// Assume already DER
|
|
535
|
+
signature = sigResult;
|
|
536
|
+
}
|
|
537
|
+
} else if (typeof sigResult === 'object' && sigResult.toDER) {
|
|
538
|
+
// Some libraries provide toDER method
|
|
539
|
+
signature = sigResult.toDER();
|
|
540
|
+
} else if (typeof sigResult === 'object' && sigResult.signature) {
|
|
541
|
+
// Some libraries return { signature, recovery }
|
|
542
|
+
if (sigResult.signature.length === 64) {
|
|
543
|
+
signature = encodeDER(sigResult.signature);
|
|
544
|
+
} else {
|
|
545
|
+
signature = sigResult.signature;
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
throw new Error('Unknown signature format from secp256k1.sign()');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Serialize complete message
|
|
552
|
+
const serialized = concatBytes(
|
|
553
|
+
serializeString(params.token),
|
|
554
|
+
serializeString(params.senderAddress),
|
|
555
|
+
serializeInt64(params.timestamp),
|
|
556
|
+
serializeVector(signature),
|
|
557
|
+
serializeVector(encryptedPayload)
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
hex: bytesToHex(serialized),
|
|
562
|
+
messageHash,
|
|
563
|
+
messageHashBytes: bytesToHex(messageHashBytes),
|
|
564
|
+
encryptedSize: encryptedPayload.length,
|
|
565
|
+
recipientCount: recipientPubKeys.length
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Export for browser (IIFE global)
|
|
570
|
+
export {
|
|
571
|
+
buildDepinMessage,
|
|
572
|
+
wifToHex,
|
|
573
|
+
isWIF,
|
|
574
|
+
hexToBytes,
|
|
575
|
+
bytesToHex,
|
|
576
|
+
sha256,
|
|
577
|
+
doubleSha256,
|
|
578
|
+
hash160,
|
|
579
|
+
base58Decode
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// Expose on globalThis (browser + Node) for the IIFE bundle.
|
|
583
|
+
if (typeof globalThis !== 'undefined') {
|
|
584
|
+
globalThis.neuraiDepinMsg = {
|
|
585
|
+
buildDepinMessage,
|
|
586
|
+
wifToHex,
|
|
587
|
+
isWIF,
|
|
588
|
+
utils: {
|
|
589
|
+
hexToBytes,
|
|
590
|
+
bytesToHex,
|
|
591
|
+
sha256,
|
|
592
|
+
doubleSha256,
|
|
593
|
+
hash160,
|
|
594
|
+
base58Decode
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
}
|