@interest-protocol/vortex-sdk 11.3.1 → 12.0.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.
@@ -1,9 +1,9 @@
1
- import { PaginatedEvents } from '@mysten/sui/client';
2
- import { Commitment } from '../vortex-api.types';
3
- import { VortexKeypair } from '../entities/keypair';
1
+ import type { PaginatedEvents } from '@mysten/sui/client';
2
+ import type { Commitment } from '../vortex-api.types';
3
+ import type { VortexKeypair } from '../entities/keypair';
4
+ import type { Vortex } from '../vortex';
5
+ import type { VortexPool } from '../vortex.types';
4
6
  import { Utxo } from '../entities/utxo';
5
- import { Vortex } from '../vortex';
6
- import { VortexPool } from '../vortex.types';
7
7
  interface GetUnspentUtxosArgs {
8
8
  commitmentEvents: PaginatedEvents;
9
9
  vortexKeypair: VortexKeypair;
@@ -18,7 +18,7 @@ interface GetUnspentUtxosWithApiArgs {
18
18
  vortexPool: string | VortexPool;
19
19
  }
20
20
  interface GetUnspentUtxosWithApiAndCommitmentsArgs {
21
- commitments: Pick<Commitment, 'coinType' | 'encryptedOutput'>[];
21
+ commitments: Pick<Commitment, 'coinType' | 'encryptedOutput' | 'index'>[];
22
22
  vortexKeypair: VortexKeypair;
23
23
  vortexSdk: Vortex;
24
24
  vortexPool: string | VortexPool;
@@ -26,7 +26,7 @@ interface GetUnspentUtxosWithApiAndCommitmentsArgs {
26
26
  export declare const getUnspentUtxosWithApi: ({ commitments, vortexKeypair, vortexSdk, vortexPool, }: GetUnspentUtxosWithApiArgs) => Promise<Utxo[]>;
27
27
  export declare const getUnspentUtxosWithApiAndCommitments: ({ commitments, vortexKeypair, vortexSdk, vortexPool, }: GetUnspentUtxosWithApiAndCommitmentsArgs) => Promise<{
28
28
  unspentUtxos: Utxo[];
29
- userCommitments: Pick<Commitment, "coinType" | "encryptedOutput">[];
29
+ userCommitments: Pick<Commitment, "index" | "coinType" | "encryptedOutput">[];
30
30
  }>;
31
31
  export {};
32
32
  //# sourceMappingURL=decrypt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/utils/decrypt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAErD,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACnC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,UAAU,mBAAmB;IAC3B,gBAAgB,EAAE,eAAe,CAAC;IAClC,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;CACjC;AAED,eAAO,MAAM,eAAe,GAAU,6DAKnC,mBAAmB,oBAkCrB,CAAC;AAEF,UAAU,0BAA0B;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;CACjC;AAED,UAAU,wCAAwC;IAChD,WAAW,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,iBAAiB,CAAC,EAAE,CAAC;IAChE,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;CACjC;AAED,eAAO,MAAM,sBAAsB,GAAU,wDAK1C,0BAA0B,oBA2C5B,CAAC;AAEF,eAAO,MAAM,oCAAoC,GAAU,wDAKxD,wCAAwC;;;EAmD1C,CAAC"}
1
+ {"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../src/utils/decrypt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGlD,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAIxC,UAAU,mBAAmB;IAC3B,gBAAgB,EAAE,eAAe,CAAC;IAClC,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;CACjC;AAED,eAAO,MAAM,eAAe,GAAU,6DAKnC,mBAAmB,oBAyCrB,CAAC;AAEF,UAAU,0BAA0B;IAClC,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;CACjC;AAED,UAAU,wCAAwC;IAChD,WAAW,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,iBAAiB,GAAG,OAAO,CAAC,EAAE,CAAC;IAC1E,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,UAAU,CAAC;CACjC;AAED,eAAO,MAAM,sBAAsB,GAAU,wDAK1C,0BAA0B,oBA8C5B,CAAC;AAEF,eAAO,MAAM,oCAAoC,GAAU,wDAKxD,wCAAwC;;;EAoD1C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interest-protocol/vortex-sdk",
3
- "version": "11.3.1",
3
+ "version": "12.0.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "module": "./dist/index.mjs",
@@ -108,8 +108,267 @@ describe(VortexKeypair.name, () => {
108
108
  keypair1.encryptionKey
109
109
  );
110
110
 
111
- // Attempting to decrypt with wrong keypair should throw or produce garbage
112
- expect(() => keypair2.decryptUtxo(encrypted)).toThrow();
111
+ // Attempting to decrypt with wrong keypair should throw
112
+ expect(() => keypair2.decryptUtxo(encrypted)).toThrow(
113
+ 'Decryption failed: HMAC verification failed'
114
+ );
115
+ });
116
+
117
+ it('should ALWAYS fail with wrong keypair - 100 iterations', () => {
118
+ // This test verifies the key-committing property of HMAC
119
+ // With the old Poly1305, this could sometimes succeed with garbage data
120
+ // With HMAC-SHA256, it should ALWAYS fail
121
+
122
+ const correctKeypair = VortexKeypair.generate();
123
+ const utxo = {
124
+ amount: 1000000n,
125
+ blinding: 123456789n,
126
+ index: 42n,
127
+ vortexPool:
128
+ '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
129
+ };
130
+
131
+ const encrypted = VortexKeypair.encryptUtxoFor(
132
+ utxo,
133
+ correctKeypair.encryptionKey
134
+ );
135
+
136
+ let failureCount = 0;
137
+ const iterations = 100;
138
+
139
+ for (let i = 0; i < iterations; i++) {
140
+ const wrongKeypair = VortexKeypair.generate();
141
+ try {
142
+ wrongKeypair.decryptUtxo(encrypted);
143
+ // If we get here, decryption "succeeded" (BUG!)
144
+ } catch {
145
+ failureCount++;
146
+ }
147
+ }
148
+
149
+ // ALL attempts must fail - this proves the key-committing property
150
+ expect(failureCount).toBe(iterations);
151
+ });
152
+
153
+ it('should throw specific HMAC error message on wrong key', () => {
154
+ const keypair1 = VortexKeypair.generate();
155
+ const keypair2 = VortexKeypair.generate();
156
+
157
+ const utxo = {
158
+ amount: 500n,
159
+ blinding: 12345n,
160
+ index: 1n,
161
+ vortexPool: '0xabc',
162
+ };
163
+
164
+ const encrypted = VortexKeypair.encryptUtxoFor(
165
+ utxo,
166
+ keypair1.encryptionKey
167
+ );
168
+
169
+ expect(() => keypair2.decryptUtxo(encrypted)).toThrow(
170
+ 'HMAC verification failed'
171
+ );
172
+ });
173
+
174
+ it('should decrypt correctly with the right keypair after failed attempts', () => {
175
+ const correctKeypair = VortexKeypair.generate();
176
+ const utxo = {
177
+ amount: 9999n,
178
+ blinding: 8888n,
179
+ index: 7n,
180
+ vortexPool: '0xdef',
181
+ };
182
+
183
+ const encrypted = VortexKeypair.encryptUtxoFor(
184
+ utxo,
185
+ correctKeypair.encryptionKey
186
+ );
187
+
188
+ // Try 10 wrong keypairs first
189
+ for (let i = 0; i < 10; i++) {
190
+ const wrongKeypair = VortexKeypair.generate();
191
+ expect(() => wrongKeypair.decryptUtxo(encrypted)).toThrow();
192
+ }
193
+
194
+ // Now decrypt with correct keypair - should still work
195
+ const decrypted = correctKeypair.decryptUtxo(encrypted);
196
+ expect(decrypted.amount).toBe(utxo.amount);
197
+ expect(decrypted.blinding).toBe(utxo.blinding);
198
+ expect(decrypted.index).toBe(utxo.index);
199
+ });
200
+
201
+ it('should handle large amounts correctly', () => {
202
+ const keypair = VortexKeypair.generate();
203
+ const utxo = {
204
+ amount: BigInt('999999999999999999999999999'),
205
+ blinding: BigInt('888888888888888888888888888'),
206
+ index: BigInt('77777777777'),
207
+ vortexPool:
208
+ '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
209
+ };
210
+
211
+ const encrypted = VortexKeypair.encryptUtxoFor(
212
+ utxo,
213
+ keypair.encryptionKey
214
+ );
215
+ const decrypted = keypair.decryptUtxo(encrypted);
216
+
217
+ expect(decrypted.amount).toBe(utxo.amount);
218
+ expect(decrypted.blinding).toBe(utxo.blinding);
219
+ expect(decrypted.index).toBe(utxo.index);
220
+ });
221
+
222
+ it('should handle zero values correctly', () => {
223
+ const keypair = VortexKeypair.generate();
224
+ const utxo = {
225
+ amount: 0n,
226
+ blinding: 0n,
227
+ index: 0n,
228
+ vortexPool: '0x0',
229
+ };
230
+
231
+ const encrypted = VortexKeypair.encryptUtxoFor(
232
+ utxo,
233
+ keypair.encryptionKey
234
+ );
235
+ const decrypted = keypair.decryptUtxo(encrypted);
236
+
237
+ expect(decrypted.amount).toBe(utxo.amount);
238
+ expect(decrypted.blinding).toBe(utxo.blinding);
239
+ expect(decrypted.index).toBe(utxo.index);
240
+ });
241
+
242
+ it('should produce different ciphertexts for same data (random nonce)', () => {
243
+ const keypair = VortexKeypair.generate();
244
+ const utxo = {
245
+ amount: 1000n,
246
+ blinding: 999n,
247
+ index: 5n,
248
+ vortexPool: '0x123',
249
+ };
250
+
251
+ const encrypted1 = VortexKeypair.encryptUtxoFor(
252
+ utxo,
253
+ keypair.encryptionKey
254
+ );
255
+ const encrypted2 = VortexKeypair.encryptUtxoFor(
256
+ utxo,
257
+ keypair.encryptionKey
258
+ );
259
+
260
+ // Different ciphertexts due to random nonce
261
+ expect(encrypted1).not.toBe(encrypted2);
262
+
263
+ // But both decrypt to same data
264
+ const decrypted1 = keypair.decryptUtxo(encrypted1);
265
+ const decrypted2 = keypair.decryptUtxo(encrypted2);
266
+
267
+ expect(decrypted1.amount).toBe(decrypted2.amount);
268
+ expect(decrypted1.blinding).toBe(decrypted2.blinding);
269
+ expect(decrypted1.index).toBe(decrypted2.index);
270
+ });
271
+ });
272
+
273
+ describe('BigInt encryption/decryption', () => {
274
+ it('should encrypt and decrypt a BigInt value', () => {
275
+ const keypair = VortexKeypair.generate();
276
+ const originalValue = 123456n;
277
+
278
+ const encrypted = VortexKeypair.encryptBigIntFor(
279
+ originalValue,
280
+ keypair.encryptionKey
281
+ );
282
+ const decrypted = keypair.decryptBigInt(encrypted);
283
+
284
+ expect(decrypted).toBe(originalValue);
285
+ });
286
+
287
+ it('should handle zero', () => {
288
+ const keypair = VortexKeypair.generate();
289
+ const originalValue = 0n;
290
+
291
+ const encrypted = VortexKeypair.encryptBigIntFor(
292
+ originalValue,
293
+ keypair.encryptionKey
294
+ );
295
+ const decrypted = keypair.decryptBigInt(encrypted);
296
+
297
+ expect(decrypted).toBe(originalValue);
298
+ });
299
+
300
+ it('should handle large BigInt values', () => {
301
+ const keypair = VortexKeypair.generate();
302
+ const originalValue = BigInt(
303
+ '21888242871839275222246405745257275088548364400416034343698204186575808495616'
304
+ );
305
+
306
+ const encrypted = VortexKeypair.encryptBigIntFor(
307
+ originalValue,
308
+ keypair.encryptionKey
309
+ );
310
+ const decrypted = keypair.decryptBigInt(encrypted);
311
+
312
+ expect(decrypted).toBe(originalValue);
313
+ });
314
+
315
+ it('should fail to decrypt with wrong keypair', () => {
316
+ const keypair1 = VortexKeypair.generate();
317
+ const keypair2 = VortexKeypair.generate();
318
+ const originalValue = 123456n;
319
+
320
+ const encrypted = VortexKeypair.encryptBigIntFor(
321
+ originalValue,
322
+ keypair1.encryptionKey
323
+ );
324
+
325
+ expect(() => keypair2.decryptBigInt(encrypted)).toThrow(
326
+ 'Decryption failed: HMAC verification failed'
327
+ );
328
+ });
329
+
330
+ it('should produce different ciphertexts for same value (random nonce)', () => {
331
+ const keypair = VortexKeypair.generate();
332
+ const value = 999999n;
333
+
334
+ const encrypted1 = VortexKeypair.encryptBigIntFor(
335
+ value,
336
+ keypair.encryptionKey
337
+ );
338
+ const encrypted2 = VortexKeypair.encryptBigIntFor(
339
+ value,
340
+ keypair.encryptionKey
341
+ );
342
+
343
+ expect(encrypted1).not.toBe(encrypted2);
344
+
345
+ // But both decrypt to the same value
346
+ expect(keypair.decryptBigInt(encrypted1)).toBe(value);
347
+ expect(keypair.decryptBigInt(encrypted2)).toBe(value);
348
+ });
349
+
350
+ it('should ALWAYS fail with wrong keypair - 100 iterations', () => {
351
+ const correctKeypair = VortexKeypair.generate();
352
+ const value = BigInt('12345678901234567890');
353
+
354
+ const encrypted = VortexKeypair.encryptBigIntFor(
355
+ value,
356
+ correctKeypair.encryptionKey
357
+ );
358
+
359
+ let failureCount = 0;
360
+ const iterations = 100;
361
+
362
+ for (let i = 0; i < iterations; i++) {
363
+ const wrongKeypair = VortexKeypair.generate();
364
+ try {
365
+ wrongKeypair.decryptBigInt(encrypted);
366
+ } catch {
367
+ failureCount++;
368
+ }
369
+ }
370
+
371
+ expect(failureCount).toBe(iterations);
113
372
  });
114
373
  });
115
374
 
package/src/constants.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { SUI_TYPE_ARG } from '@mysten/sui/utils';
2
-
3
1
  export const BN254_FIELD_MODULUS =
4
2
  21888242871839275222246405745257275088548364400416034343698204186575808495617n;
5
3
 
@@ -73,24 +71,24 @@ export const ERROR_CODES = {
73
71
  };
74
72
 
75
73
  export const VORTEX_PACKAGE_ID =
76
- '0xcf81b96e392f82b776ee980108357426b726c4043c838822545a307e12c5ded6';
74
+ '0xd9d3b65c318e7d7dd208050a28e113a45256765b4c45acd119626d8a228d7555';
77
75
 
78
76
  export const VORTEX_UPGRADE_CAP =
79
- '0xc2d1925fd45559e09c51f5491ec96d61f9e9108c967d34fe22a053c1b307ddfc';
77
+ '0xa0d5c1dec2ad7c732cf8d01378eba8fc1451841b051fa0292d3ad07ce92aad80';
80
78
 
81
79
  export const REGISTRY_OBJECT_ID =
82
- '0xf2c11c297e0581e0279714f6ba47e26d03d9a70756036fab5882ebc0f1d2b3b1';
80
+ '0x6cec550fb43435462f34b15aee9f8b8030e0433e16c8104e966013f0443b2e36';
83
81
 
84
82
  export const VORTEX_SWAP_PACKAGE_ID =
85
- '0x2ddd33debbac3e0461b3551bb00bd40d3055ea5cd441b4fad8624dcbb095e8fb';
83
+ '0x325610fc0c15c91682f8bf263ef0991c28fe7ff77fd98266821c3e696e459cdf';
86
84
 
87
85
  export const VORTEX_SWAP_UPGRADE_CAP =
88
- '0xec0beaf1453b0e09d92f8addf25abcfb4bc6ce43ead828914836cadc8df249a0';
86
+ '0x3f535b61b6baa2174dc301179bd2bc392563086ddca23b8eec69da505d54e3c1';
89
87
 
90
88
  export const SECRET_PACKAGE_ID =
91
89
  '0x2d57ed0dd0d5f44d91f865fee3bc33d13ef3ce97c7daf88ad5f5fbb32468ccd6';
92
90
 
93
- export const INITIAL_SHARED_VERSION = '692442863';
91
+ export const INITIAL_SHARED_VERSION = '738926997';
94
92
 
95
93
  export const LSK_FETCH_OFFSET = 'fetch_offset';
96
94
 
@@ -106,8 +104,3 @@ export const TREASURY_ADDRESS =
106
104
  export const DEPOSIT_FEE_IN_BASIS_POINTS = 50n;
107
105
 
108
106
  export const BASIS_POINTS = 10_000n;
109
-
110
- export const VORTEX_POOL_IDS = {
111
- [SUI_TYPE_ARG]:
112
- '0x1e3672f35853fccded923505434b5138543829231f025120d57fda95b86b504c',
113
- };
@@ -1,5 +1,6 @@
1
+ /* eslint-disable */
1
2
  import * as Scalar from './scalar';
2
- import { getRandomBytes } from './random';
3
+ import { randomBytes } from '@noble/ciphers/utils.js';
3
4
 
4
5
  export class F1Field {
5
6
  type: string;
@@ -288,7 +289,7 @@ export class F1Field {
288
289
  const nBytes = (this.bitLength * 2) / 8;
289
290
  let res = this.zero;
290
291
  for (let i = 0; i < nBytes; i++) {
291
- res = (res << BigInt(8)) + BigInt(getRandomBytes(1)[0]!);
292
+ res = (res << BigInt(8)) + BigInt(randomBytes(1)[0]!);
292
293
  }
293
294
  return res % this.p;
294
295
  }
@@ -1,6 +1,6 @@
1
+ /* eslint-disable */
1
2
  import * as utils from './utils';
2
3
  import * as Scalar from './scalar';
3
4
  import { F1Field } from './f1field';
4
- import { getRandomBytes } from './random';
5
5
 
6
- export { utils, Scalar, F1Field, getRandomBytes };
6
+ export { utils, Scalar, F1Field };
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  const hexLen = [0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4];
2
3
 
3
4
  export const fromString = (s: string, radix?: number): bigint => {
@@ -1,3 +1,4 @@
1
+ /* eslint-disable */
1
2
  import * as Scalar from './scalar';
2
3
  import { fromBase64 } from '@mysten/sui/utils';
3
4
  export function unStringifyBigInts(o: unknown): unknown {
@@ -6,10 +6,22 @@ import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
6
6
  import invariant from 'tiny-invariant';
7
7
  import { randomBytes } from '@noble/ciphers/utils.js';
8
8
  import { x25519 } from '@noble/curves/ed25519.js';
9
- import { xsalsa20poly1305 } from '@noble/ciphers/salsa.js';
9
+ import { xchacha20 } from '@noble/ciphers/chacha.js';
10
+ import { hmac } from '@noble/hashes/hmac.js';
11
+ import { sha256 } from '@noble/hashes/sha2.js';
10
12
  import { blake2b } from '@noble/hashes/blake2.js';
11
13
  import { normalizeSuiAddress } from '@mysten/sui/utils';
12
14
 
15
+ // Constant-time comparison to prevent timing attacks
16
+ function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
17
+ if (a.length !== b.length) return false;
18
+ let diff = 0;
19
+ for (let i = 0; i < a.length; i++) {
20
+ diff |= a[i] ^ b[i];
21
+ }
22
+ return diff === 0;
23
+ }
24
+
13
25
  export interface UtxoPayload {
14
26
  amount: bigint;
15
27
  blinding: bigint;
@@ -21,6 +33,7 @@ interface EncryptedMessage {
21
33
  version: string;
22
34
  nonce: string; // base64
23
35
  ephemPublicKey: string; // base64
36
+ hmacTag: string; // base64 - HMAC-SHA256 tag for key-committing authentication
24
37
  ciphertext: string; // base64
25
38
  }
26
39
 
@@ -30,12 +43,14 @@ type SignMessageFn = (message: Uint8Array) => Promise<{
30
43
  bytes: string;
31
44
  }>;
32
45
 
46
+ // Message format: nonce (24) | ephemPublicKey (32) | hmacTag (32) | ciphertext (variable)
33
47
  function packEncryptedMessage(encryptedMessage: EncryptedMessage): string {
34
48
  const nonceBuf = Buffer.from(encryptedMessage.nonce, 'base64');
35
49
  const ephemPublicKeyBuf = Buffer.from(
36
50
  encryptedMessage.ephemPublicKey,
37
51
  'base64'
38
52
  );
53
+ const hmacTagBuf = Buffer.from(encryptedMessage.hmacTag, 'base64');
39
54
  const ciphertextBuf = Buffer.from(encryptedMessage.ciphertext, 'base64');
40
55
 
41
56
  const messageBuff = Buffer.concat([
@@ -43,6 +58,8 @@ function packEncryptedMessage(encryptedMessage: EncryptedMessage): string {
43
58
  nonceBuf,
44
59
  Buffer.alloc(32 - ephemPublicKeyBuf.length),
45
60
  ephemPublicKeyBuf,
61
+ Buffer.alloc(32 - hmacTagBuf.length),
62
+ hmacTagBuf,
46
63
  ciphertextBuf,
47
64
  ]);
48
65
 
@@ -58,12 +75,14 @@ function unpackEncryptedMessage(encryptedMessage: string): EncryptedMessage {
58
75
 
59
76
  const nonceBuf = messageBuff.subarray(0, 24);
60
77
  const ephemPublicKeyBuf = messageBuff.subarray(24, 56);
61
- const ciphertextBuf = messageBuff.subarray(56);
78
+ const hmacTagBuf = messageBuff.subarray(56, 88);
79
+ const ciphertextBuf = messageBuff.subarray(88);
62
80
 
63
81
  return {
64
- version: 'x25519-xsalsa20-poly1305',
82
+ version: 'x25519-xchacha20-hmac-sha256',
65
83
  nonce: nonceBuf.toString('base64'),
66
84
  ephemPublicKey: ephemPublicKeyBuf.toString('base64'),
85
+ hmacTag: hmacTagBuf.toString('base64'),
67
86
  ciphertext: ciphertextBuf.toString('base64'),
68
87
  };
69
88
  }
@@ -159,14 +178,19 @@ export class VortexKeypair {
159
178
  recipientPublicKey
160
179
  );
161
180
 
181
+ // Encrypt with XChaCha20
162
182
  const nonce = randomBytes(24);
163
- const cipher = xsalsa20poly1305(sharedSecret, nonce);
164
- const ciphertext = cipher.encrypt(bytes);
183
+ const ciphertext = xchacha20(sharedSecret, nonce, bytes);
184
+
185
+ // Compute HMAC-SHA256 over ciphertext (Encrypt-then-MAC)
186
+ // This provides key-committing authentication
187
+ const hmacTag = hmac(sha256, sharedSecret, ciphertext);
165
188
 
166
189
  const encryptedMessage: EncryptedMessage = {
167
- version: 'x25519-xsalsa20-poly1305',
190
+ version: 'x25519-xchacha20-hmac-sha256',
168
191
  nonce: Buffer.from(nonce).toString('base64'),
169
192
  ephemPublicKey: Buffer.from(ephemeralPublicKey).toString('base64'),
193
+ hmacTag: Buffer.from(hmacTag).toString('base64'),
170
194
  ciphertext: Buffer.from(ciphertext).toString('base64'),
171
195
  };
172
196
 
@@ -189,10 +213,28 @@ export class VortexKeypair {
189
213
 
190
214
  invariant(parts.length === 4, 'Invalid UTXO format after decryption');
191
215
 
216
+ const amount = BigInt(parts[0]);
217
+ const blinding = BigInt(parts[1]);
218
+ const index = BigInt(parts[2]);
219
+
220
+ // Validate values are within BN254 field to prevent proof failures
221
+ invariant(
222
+ amount >= 0n && amount < BN254_FIELD_MODULUS,
223
+ 'Amount exceeds field modulus'
224
+ );
225
+ invariant(
226
+ blinding >= 0n && blinding < BN254_FIELD_MODULUS,
227
+ 'Blinding exceeds field modulus'
228
+ );
229
+ invariant(
230
+ index >= 0n && index < BN254_FIELD_MODULUS,
231
+ 'Index exceeds field modulus'
232
+ );
233
+
192
234
  return {
193
- amount: BigInt(parts[0]),
194
- blinding: BigInt(parts[1]),
195
- index: BigInt(parts[2]),
235
+ amount,
236
+ blinding,
237
+ index,
196
238
  vortexPool: normalizeSuiAddress(parts[3]),
197
239
  };
198
240
  }
@@ -254,12 +296,20 @@ export class VortexKeypair {
254
296
  ephemeralPublicKey
255
297
  );
256
298
 
257
- // Decrypt using XSalsa20-Poly1305
258
- const nonce = Buffer.from(encryptedMessage.nonce, 'base64');
259
299
  const ciphertext = Buffer.from(encryptedMessage.ciphertext, 'base64');
300
+ const receivedHmacTag = Buffer.from(encryptedMessage.hmacTag, 'base64');
260
301
 
261
- const cipher = xsalsa20poly1305(sharedSecret, nonce);
262
- const decrypted = cipher.decrypt(ciphertext);
302
+ // Verify HMAC-SHA256 first (Encrypt-then-MAC verification)
303
+ // This is key-committing: wrong key = wrong HMAC = guaranteed failure
304
+ const expectedHmacTag = hmac(sha256, sharedSecret, ciphertext);
305
+
306
+ if (!constantTimeEqual(receivedHmacTag, expectedHmacTag)) {
307
+ throw new Error('Decryption failed: HMAC verification failed');
308
+ }
309
+
310
+ // Decrypt using XChaCha20
311
+ const nonce = Buffer.from(encryptedMessage.nonce, 'base64');
312
+ const decrypted = xchacha20(sharedSecret, nonce, ciphertext);
263
313
 
264
314
  return Buffer.from(decrypted);
265
315
  }
@@ -1,6 +1,8 @@
1
1
  import { VortexKeypair } from './keypair';
2
2
  import { poseidon3, poseidon4 } from '../crypto';
3
- import { normalizeSuiAddress } from '@mysten/sui/utils';
3
+ import { normalizeSuiAddress, toHex } from '@mysten/sui/utils';
4
+ import { randomBytes } from '@noble/ciphers/utils.js';
5
+ import { BN254_FIELD_MODULUS } from '../constants';
4
6
 
5
7
  interface UtxoConstructorArgs {
6
8
  amount: bigint;
@@ -53,7 +55,10 @@ export class Utxo {
53
55
  }
54
56
 
55
57
  static blinding() {
56
- return BigInt(Math.floor(Math.random() * 1_000_000_000));
58
+ // Use cryptographically secure randomness (works on web + Node.js)
59
+ // 31 bytes = 248 bits, safely below BN254 field modulus (~254 bits)
60
+ const bytes = randomBytes(31);
61
+ return BigInt('0x' + toHex(bytes)) % BN254_FIELD_MODULUS;
57
62
  }
58
63
 
59
64
  commitment() {