wolfronix-sdk 2.4.2 → 2.4.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/README.md +189 -726
- package/dist/index.d.mts +161 -3
- package/dist/index.d.ts +161 -3
- package/dist/index.global.js +3 -55
- package/dist/index.js +1083 -18470
- package/dist/index.mjs +1071 -36
- package/package.json +4 -4
- package/dist/chunk-EDTKPA2L.mjs +0 -33
- package/dist/undici-BDVTXO27.mjs +0 -18410
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import "./chunk-EDTKPA2L.mjs";
|
|
2
|
-
|
|
3
1
|
// src/crypto.ts
|
|
4
2
|
var getCrypto = () => {
|
|
5
3
|
if (typeof globalThis.crypto !== "undefined") {
|
|
@@ -273,6 +271,641 @@ var ValidationError = class extends WolfronixError {
|
|
|
273
271
|
this.name = "ValidationError";
|
|
274
272
|
}
|
|
275
273
|
};
|
|
274
|
+
var RECOVERY_WORDS = [
|
|
275
|
+
"able",
|
|
276
|
+
"about",
|
|
277
|
+
"absorb",
|
|
278
|
+
"access",
|
|
279
|
+
"acid",
|
|
280
|
+
"across",
|
|
281
|
+
"action",
|
|
282
|
+
"adapt",
|
|
283
|
+
"admit",
|
|
284
|
+
"adult",
|
|
285
|
+
"agent",
|
|
286
|
+
"agree",
|
|
287
|
+
"ahead",
|
|
288
|
+
"air",
|
|
289
|
+
"alert",
|
|
290
|
+
"alpha",
|
|
291
|
+
"anchor",
|
|
292
|
+
"angle",
|
|
293
|
+
"apple",
|
|
294
|
+
"arch",
|
|
295
|
+
"arena",
|
|
296
|
+
"argue",
|
|
297
|
+
"armed",
|
|
298
|
+
"arrow",
|
|
299
|
+
"asset",
|
|
300
|
+
"atlas",
|
|
301
|
+
"attack",
|
|
302
|
+
"audio",
|
|
303
|
+
"august",
|
|
304
|
+
"auto",
|
|
305
|
+
"avoid",
|
|
306
|
+
"awake",
|
|
307
|
+
"aware",
|
|
308
|
+
"badge",
|
|
309
|
+
"balance",
|
|
310
|
+
"banana",
|
|
311
|
+
"basic",
|
|
312
|
+
"beach",
|
|
313
|
+
"beauty",
|
|
314
|
+
"before",
|
|
315
|
+
"begin",
|
|
316
|
+
"below",
|
|
317
|
+
"benefit",
|
|
318
|
+
"best",
|
|
319
|
+
"beyond",
|
|
320
|
+
"bicycle",
|
|
321
|
+
"bird",
|
|
322
|
+
"black",
|
|
323
|
+
"bless",
|
|
324
|
+
"board",
|
|
325
|
+
"bold",
|
|
326
|
+
"bonus",
|
|
327
|
+
"border",
|
|
328
|
+
"borrow",
|
|
329
|
+
"bottle",
|
|
330
|
+
"bottom",
|
|
331
|
+
"brain",
|
|
332
|
+
"brand",
|
|
333
|
+
"brave",
|
|
334
|
+
"breeze",
|
|
335
|
+
"brick",
|
|
336
|
+
"brief",
|
|
337
|
+
"bring",
|
|
338
|
+
"brother",
|
|
339
|
+
"budget",
|
|
340
|
+
"build",
|
|
341
|
+
"camera",
|
|
342
|
+
"camp",
|
|
343
|
+
"canal",
|
|
344
|
+
"carbon",
|
|
345
|
+
"carry",
|
|
346
|
+
"casual",
|
|
347
|
+
"center",
|
|
348
|
+
"chain",
|
|
349
|
+
"change",
|
|
350
|
+
"charge",
|
|
351
|
+
"chase",
|
|
352
|
+
"cheap",
|
|
353
|
+
"check",
|
|
354
|
+
"chief",
|
|
355
|
+
"choice",
|
|
356
|
+
"circle",
|
|
357
|
+
"city",
|
|
358
|
+
"claim",
|
|
359
|
+
"class",
|
|
360
|
+
"clean",
|
|
361
|
+
"clear",
|
|
362
|
+
"client",
|
|
363
|
+
"clock",
|
|
364
|
+
"cloud",
|
|
365
|
+
"coach",
|
|
366
|
+
"coast",
|
|
367
|
+
"color",
|
|
368
|
+
"column",
|
|
369
|
+
"combo",
|
|
370
|
+
"common",
|
|
371
|
+
"concept",
|
|
372
|
+
"confirm",
|
|
373
|
+
"connect",
|
|
374
|
+
"copy",
|
|
375
|
+
"core",
|
|
376
|
+
"corner",
|
|
377
|
+
"correct",
|
|
378
|
+
"cost",
|
|
379
|
+
"cover",
|
|
380
|
+
"craft",
|
|
381
|
+
"create",
|
|
382
|
+
"credit",
|
|
383
|
+
"cross",
|
|
384
|
+
"crowd",
|
|
385
|
+
"crystal",
|
|
386
|
+
"current",
|
|
387
|
+
"custom",
|
|
388
|
+
"cycle",
|
|
389
|
+
"daily",
|
|
390
|
+
"danger",
|
|
391
|
+
"data",
|
|
392
|
+
"dealer",
|
|
393
|
+
"debate",
|
|
394
|
+
"decide",
|
|
395
|
+
"deep",
|
|
396
|
+
"define",
|
|
397
|
+
"degree",
|
|
398
|
+
"delay",
|
|
399
|
+
"demand",
|
|
400
|
+
"denial",
|
|
401
|
+
"design",
|
|
402
|
+
"detail",
|
|
403
|
+
"device",
|
|
404
|
+
"dialog",
|
|
405
|
+
"digital",
|
|
406
|
+
"direct",
|
|
407
|
+
"doctor",
|
|
408
|
+
"domain",
|
|
409
|
+
"double",
|
|
410
|
+
"draft",
|
|
411
|
+
"dragon",
|
|
412
|
+
"drama",
|
|
413
|
+
"dream",
|
|
414
|
+
"drive",
|
|
415
|
+
"early",
|
|
416
|
+
"earth",
|
|
417
|
+
"easy",
|
|
418
|
+
"echo",
|
|
419
|
+
"edge",
|
|
420
|
+
"edit",
|
|
421
|
+
"effect",
|
|
422
|
+
"either",
|
|
423
|
+
"elder",
|
|
424
|
+
"element",
|
|
425
|
+
"elite",
|
|
426
|
+
"email",
|
|
427
|
+
"energy",
|
|
428
|
+
"engine",
|
|
429
|
+
"enough",
|
|
430
|
+
"enter",
|
|
431
|
+
"equal",
|
|
432
|
+
"error",
|
|
433
|
+
"escape",
|
|
434
|
+
"estate",
|
|
435
|
+
"event",
|
|
436
|
+
"exact",
|
|
437
|
+
"example",
|
|
438
|
+
"exchange",
|
|
439
|
+
"exist",
|
|
440
|
+
"expand",
|
|
441
|
+
"expect",
|
|
442
|
+
"expert",
|
|
443
|
+
"extra",
|
|
444
|
+
"fabric",
|
|
445
|
+
"factor",
|
|
446
|
+
"family",
|
|
447
|
+
"famous",
|
|
448
|
+
"feature",
|
|
449
|
+
"fence",
|
|
450
|
+
"field",
|
|
451
|
+
"figure",
|
|
452
|
+
"filter",
|
|
453
|
+
"final",
|
|
454
|
+
"finger",
|
|
455
|
+
"finish",
|
|
456
|
+
"first",
|
|
457
|
+
"focus",
|
|
458
|
+
"follow",
|
|
459
|
+
"force",
|
|
460
|
+
"forest",
|
|
461
|
+
"format",
|
|
462
|
+
"forward",
|
|
463
|
+
"frame",
|
|
464
|
+
"fresh",
|
|
465
|
+
"front",
|
|
466
|
+
"future",
|
|
467
|
+
"gallery",
|
|
468
|
+
"general",
|
|
469
|
+
"giant",
|
|
470
|
+
"global",
|
|
471
|
+
"gold",
|
|
472
|
+
"good",
|
|
473
|
+
"grace",
|
|
474
|
+
"grant",
|
|
475
|
+
"green",
|
|
476
|
+
"group",
|
|
477
|
+
"guard",
|
|
478
|
+
"habit",
|
|
479
|
+
"half",
|
|
480
|
+
"hammer",
|
|
481
|
+
"handle",
|
|
482
|
+
"happy",
|
|
483
|
+
"harbor",
|
|
484
|
+
"health",
|
|
485
|
+
"height",
|
|
486
|
+
"hidden",
|
|
487
|
+
"history",
|
|
488
|
+
"honest",
|
|
489
|
+
"host",
|
|
490
|
+
"hotel",
|
|
491
|
+
"human",
|
|
492
|
+
"hybrid",
|
|
493
|
+
"idea",
|
|
494
|
+
"image",
|
|
495
|
+
"impact",
|
|
496
|
+
"income",
|
|
497
|
+
"index",
|
|
498
|
+
"input",
|
|
499
|
+
"inside",
|
|
500
|
+
"insight",
|
|
501
|
+
"island",
|
|
502
|
+
"item",
|
|
503
|
+
"jacket",
|
|
504
|
+
"jazz",
|
|
505
|
+
"join",
|
|
506
|
+
"jungle",
|
|
507
|
+
"keep",
|
|
508
|
+
"keyboard",
|
|
509
|
+
"kind",
|
|
510
|
+
"king",
|
|
511
|
+
"kitchen",
|
|
512
|
+
"label",
|
|
513
|
+
"ladder",
|
|
514
|
+
"language",
|
|
515
|
+
"large",
|
|
516
|
+
"laser",
|
|
517
|
+
"later",
|
|
518
|
+
"launch",
|
|
519
|
+
"layer",
|
|
520
|
+
"leader",
|
|
521
|
+
"learn",
|
|
522
|
+
"level",
|
|
523
|
+
"light",
|
|
524
|
+
"limit",
|
|
525
|
+
"linear",
|
|
526
|
+
"link",
|
|
527
|
+
"listen",
|
|
528
|
+
"local",
|
|
529
|
+
"logic",
|
|
530
|
+
"lucky",
|
|
531
|
+
"machine",
|
|
532
|
+
"magic",
|
|
533
|
+
"major",
|
|
534
|
+
"manage",
|
|
535
|
+
"manual",
|
|
536
|
+
"market",
|
|
537
|
+
"master",
|
|
538
|
+
"matrix",
|
|
539
|
+
"matter",
|
|
540
|
+
"member",
|
|
541
|
+
"memory",
|
|
542
|
+
"message",
|
|
543
|
+
"method",
|
|
544
|
+
"middle",
|
|
545
|
+
"million",
|
|
546
|
+
"mind",
|
|
547
|
+
"mirror",
|
|
548
|
+
"mobile",
|
|
549
|
+
"model",
|
|
550
|
+
"module",
|
|
551
|
+
"moment",
|
|
552
|
+
"monitor",
|
|
553
|
+
"moral",
|
|
554
|
+
"motion",
|
|
555
|
+
"mountain",
|
|
556
|
+
"music",
|
|
557
|
+
"native",
|
|
558
|
+
"nature",
|
|
559
|
+
"network",
|
|
560
|
+
"never",
|
|
561
|
+
"normal",
|
|
562
|
+
"notice",
|
|
563
|
+
"number",
|
|
564
|
+
"object",
|
|
565
|
+
"ocean",
|
|
566
|
+
"offer",
|
|
567
|
+
"office",
|
|
568
|
+
"online",
|
|
569
|
+
"option",
|
|
570
|
+
"orange",
|
|
571
|
+
"order",
|
|
572
|
+
"origin",
|
|
573
|
+
"output",
|
|
574
|
+
"owner",
|
|
575
|
+
"packet",
|
|
576
|
+
"panel",
|
|
577
|
+
"paper",
|
|
578
|
+
"parent",
|
|
579
|
+
"partner",
|
|
580
|
+
"pattern",
|
|
581
|
+
"pause",
|
|
582
|
+
"payment",
|
|
583
|
+
"people",
|
|
584
|
+
"perfect",
|
|
585
|
+
"phone",
|
|
586
|
+
"phrase",
|
|
587
|
+
"pilot",
|
|
588
|
+
"pixel",
|
|
589
|
+
"planet",
|
|
590
|
+
"platform",
|
|
591
|
+
"please",
|
|
592
|
+
"plus",
|
|
593
|
+
"policy",
|
|
594
|
+
"portal",
|
|
595
|
+
"position",
|
|
596
|
+
"power",
|
|
597
|
+
"predict",
|
|
598
|
+
"premium",
|
|
599
|
+
"prepare",
|
|
600
|
+
"present",
|
|
601
|
+
"pretty",
|
|
602
|
+
"price",
|
|
603
|
+
"prime",
|
|
604
|
+
"private",
|
|
605
|
+
"process",
|
|
606
|
+
"profile",
|
|
607
|
+
"project",
|
|
608
|
+
"protect",
|
|
609
|
+
"public",
|
|
610
|
+
"quality",
|
|
611
|
+
"quick",
|
|
612
|
+
"quiet",
|
|
613
|
+
"radio",
|
|
614
|
+
"random",
|
|
615
|
+
"rapid",
|
|
616
|
+
"rate",
|
|
617
|
+
"ready",
|
|
618
|
+
"reason",
|
|
619
|
+
"record",
|
|
620
|
+
"recover",
|
|
621
|
+
"region",
|
|
622
|
+
"release",
|
|
623
|
+
"remote",
|
|
624
|
+
"repair",
|
|
625
|
+
"repeat",
|
|
626
|
+
"report",
|
|
627
|
+
"request",
|
|
628
|
+
"result",
|
|
629
|
+
"return",
|
|
630
|
+
"review",
|
|
631
|
+
"right",
|
|
632
|
+
"rival",
|
|
633
|
+
"river",
|
|
634
|
+
"robot",
|
|
635
|
+
"route",
|
|
636
|
+
"royal",
|
|
637
|
+
"safe",
|
|
638
|
+
"sample",
|
|
639
|
+
"scale",
|
|
640
|
+
"scene",
|
|
641
|
+
"school",
|
|
642
|
+
"science",
|
|
643
|
+
"screen",
|
|
644
|
+
"search",
|
|
645
|
+
"secure",
|
|
646
|
+
"select",
|
|
647
|
+
"seller",
|
|
648
|
+
"senior",
|
|
649
|
+
"series",
|
|
650
|
+
"server",
|
|
651
|
+
"session",
|
|
652
|
+
"shadow",
|
|
653
|
+
"shape",
|
|
654
|
+
"share",
|
|
655
|
+
"shield",
|
|
656
|
+
"shift",
|
|
657
|
+
"ship",
|
|
658
|
+
"short",
|
|
659
|
+
"signal",
|
|
660
|
+
"silver",
|
|
661
|
+
"simple",
|
|
662
|
+
"single",
|
|
663
|
+
"skill",
|
|
664
|
+
"smart",
|
|
665
|
+
"smooth",
|
|
666
|
+
"social",
|
|
667
|
+
"solid",
|
|
668
|
+
"source",
|
|
669
|
+
"space",
|
|
670
|
+
"special",
|
|
671
|
+
"speed",
|
|
672
|
+
"spirit",
|
|
673
|
+
"split",
|
|
674
|
+
"square",
|
|
675
|
+
"stable",
|
|
676
|
+
"stack",
|
|
677
|
+
"stage",
|
|
678
|
+
"start",
|
|
679
|
+
"state",
|
|
680
|
+
"status",
|
|
681
|
+
"steel",
|
|
682
|
+
"step",
|
|
683
|
+
"stock",
|
|
684
|
+
"store",
|
|
685
|
+
"storm",
|
|
686
|
+
"story",
|
|
687
|
+
"stream",
|
|
688
|
+
"strike",
|
|
689
|
+
"strong",
|
|
690
|
+
"studio",
|
|
691
|
+
"style",
|
|
692
|
+
"subject",
|
|
693
|
+
"submit",
|
|
694
|
+
"success",
|
|
695
|
+
"sudden",
|
|
696
|
+
"sugar",
|
|
697
|
+
"supply",
|
|
698
|
+
"support",
|
|
699
|
+
"surface",
|
|
700
|
+
"switch",
|
|
701
|
+
"system",
|
|
702
|
+
"table",
|
|
703
|
+
"target",
|
|
704
|
+
"task",
|
|
705
|
+
"team",
|
|
706
|
+
"temple",
|
|
707
|
+
"tempo",
|
|
708
|
+
"tenant",
|
|
709
|
+
"term",
|
|
710
|
+
"test",
|
|
711
|
+
"theme",
|
|
712
|
+
"theory",
|
|
713
|
+
"thing",
|
|
714
|
+
"thread",
|
|
715
|
+
"time",
|
|
716
|
+
"title",
|
|
717
|
+
"token",
|
|
718
|
+
"tool",
|
|
719
|
+
"topic",
|
|
720
|
+
"total",
|
|
721
|
+
"tower",
|
|
722
|
+
"track",
|
|
723
|
+
"trade",
|
|
724
|
+
"traffic",
|
|
725
|
+
"train",
|
|
726
|
+
"travel",
|
|
727
|
+
"trust",
|
|
728
|
+
"tunnel",
|
|
729
|
+
"type",
|
|
730
|
+
"unable",
|
|
731
|
+
"update",
|
|
732
|
+
"upload",
|
|
733
|
+
"usage",
|
|
734
|
+
"useful",
|
|
735
|
+
"user",
|
|
736
|
+
"valid",
|
|
737
|
+
"value",
|
|
738
|
+
"vector",
|
|
739
|
+
"verify",
|
|
740
|
+
"version",
|
|
741
|
+
"video",
|
|
742
|
+
"view",
|
|
743
|
+
"virtual",
|
|
744
|
+
"vision",
|
|
745
|
+
"voice",
|
|
746
|
+
"volume",
|
|
747
|
+
"wait",
|
|
748
|
+
"wallet",
|
|
749
|
+
"watch",
|
|
750
|
+
"water",
|
|
751
|
+
"wealth",
|
|
752
|
+
"web",
|
|
753
|
+
"welcome",
|
|
754
|
+
"window",
|
|
755
|
+
"winner",
|
|
756
|
+
"wire",
|
|
757
|
+
"wise",
|
|
758
|
+
"wonder",
|
|
759
|
+
"work",
|
|
760
|
+
"world",
|
|
761
|
+
"write",
|
|
762
|
+
"xenon",
|
|
763
|
+
"year",
|
|
764
|
+
"yield",
|
|
765
|
+
"zone"
|
|
766
|
+
];
|
|
767
|
+
function randomInt(maxExclusive) {
|
|
768
|
+
const values = new Uint32Array(1);
|
|
769
|
+
globalThis.crypto.getRandomValues(values);
|
|
770
|
+
return values[0] % maxExclusive;
|
|
771
|
+
}
|
|
772
|
+
function generateRecoveryWords(count = 24) {
|
|
773
|
+
const words = [];
|
|
774
|
+
for (let i = 0; i < count; i++) {
|
|
775
|
+
words.push(RECOVERY_WORDS[randomInt(RECOVERY_WORDS.length)]);
|
|
776
|
+
}
|
|
777
|
+
return words;
|
|
778
|
+
}
|
|
779
|
+
var PFS_PROTOCOL = "wfx-dr-v1";
|
|
780
|
+
var ZERO_32 = new Uint8Array(32);
|
|
781
|
+
function toBase64(buf) {
|
|
782
|
+
if (typeof Buffer !== "undefined") {
|
|
783
|
+
return Buffer.from(buf).toString("base64");
|
|
784
|
+
}
|
|
785
|
+
const bytes = new Uint8Array(buf);
|
|
786
|
+
let binary = "";
|
|
787
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
788
|
+
return btoa(binary);
|
|
789
|
+
}
|
|
790
|
+
function fromBase64(b64) {
|
|
791
|
+
if (typeof Buffer !== "undefined") {
|
|
792
|
+
const buf = Buffer.from(b64, "base64");
|
|
793
|
+
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
794
|
+
}
|
|
795
|
+
const binary = atob(b64);
|
|
796
|
+
const bytes = new Uint8Array(binary.length);
|
|
797
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
798
|
+
return bytes.buffer;
|
|
799
|
+
}
|
|
800
|
+
function normalizeJwk(jwk) {
|
|
801
|
+
return JSON.stringify({
|
|
802
|
+
kty: jwk.kty || "",
|
|
803
|
+
crv: jwk.crv || "",
|
|
804
|
+
x: jwk.x || "",
|
|
805
|
+
y: jwk.y || ""
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
function ratchetKeyId(jwk, n) {
|
|
809
|
+
const j = normalizeJwk(jwk);
|
|
810
|
+
if (typeof Buffer !== "undefined") {
|
|
811
|
+
return `${Buffer.from(j).toString("base64")}:${n}`;
|
|
812
|
+
}
|
|
813
|
+
return `${btoa(j)}:${n}`;
|
|
814
|
+
}
|
|
815
|
+
async function generatePfsRatchetKeyPair() {
|
|
816
|
+
return globalThis.crypto.subtle.generateKey(
|
|
817
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
818
|
+
true,
|
|
819
|
+
["deriveBits"]
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
async function exportPublicJwk(key) {
|
|
823
|
+
return globalThis.crypto.subtle.exportKey("jwk", key);
|
|
824
|
+
}
|
|
825
|
+
async function exportPrivateJwk(key) {
|
|
826
|
+
return globalThis.crypto.subtle.exportKey("jwk", key);
|
|
827
|
+
}
|
|
828
|
+
async function importPfsPublicJwk(jwk) {
|
|
829
|
+
return globalThis.crypto.subtle.importKey(
|
|
830
|
+
"jwk",
|
|
831
|
+
jwk,
|
|
832
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
833
|
+
false,
|
|
834
|
+
[]
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
async function importPfsPrivateJwk(jwk) {
|
|
838
|
+
return globalThis.crypto.subtle.importKey(
|
|
839
|
+
"jwk",
|
|
840
|
+
jwk,
|
|
841
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
842
|
+
false,
|
|
843
|
+
["deriveBits"]
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
async function deriveEcdhSecret(privateJwk, publicJwk) {
|
|
847
|
+
const priv = await importPfsPrivateJwk(privateJwk);
|
|
848
|
+
const pub = await importPfsPublicJwk(publicJwk);
|
|
849
|
+
return globalThis.crypto.subtle.deriveBits({ name: "ECDH", public: pub }, priv, 256);
|
|
850
|
+
}
|
|
851
|
+
async function hkdfExpand(ikm, salt, info, outBits) {
|
|
852
|
+
const ikmKey = await globalThis.crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
|
|
853
|
+
return globalThis.crypto.subtle.deriveBits(
|
|
854
|
+
{
|
|
855
|
+
name: "HKDF",
|
|
856
|
+
hash: "SHA-256",
|
|
857
|
+
salt,
|
|
858
|
+
info: new TextEncoder().encode(info)
|
|
859
|
+
},
|
|
860
|
+
ikmKey,
|
|
861
|
+
outBits
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
async function hmacSha256(keyRaw, input) {
|
|
865
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
866
|
+
"raw",
|
|
867
|
+
keyRaw,
|
|
868
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
869
|
+
false,
|
|
870
|
+
["sign"]
|
|
871
|
+
);
|
|
872
|
+
return globalThis.crypto.subtle.sign("HMAC", key, new TextEncoder().encode(input));
|
|
873
|
+
}
|
|
874
|
+
async function deriveRootAndChains(rootKeyB64, dhSecret) {
|
|
875
|
+
const rootKeyRaw = rootKeyB64 ? fromBase64(rootKeyB64) : ZERO_32.buffer;
|
|
876
|
+
const mixed = await hkdfExpand(dhSecret, rootKeyRaw, `${PFS_PROTOCOL}:root`, 96 * 8);
|
|
877
|
+
const bytes = new Uint8Array(mixed);
|
|
878
|
+
return {
|
|
879
|
+
rootKey: toBase64(bytes.slice(0, 32).buffer),
|
|
880
|
+
chainA: toBase64(bytes.slice(32, 64).buffer),
|
|
881
|
+
chainB: toBase64(bytes.slice(64, 96).buffer)
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
async function deriveMessageKey(chainKeyB64, n) {
|
|
885
|
+
return hmacSha256(fromBase64(chainKeyB64), `msg:${n}`);
|
|
886
|
+
}
|
|
887
|
+
async function deriveNextChainKey(chainKeyB64) {
|
|
888
|
+
const next = await hmacSha256(fromBase64(chainKeyB64), "chain");
|
|
889
|
+
return toBase64(next);
|
|
890
|
+
}
|
|
891
|
+
async function encryptWithRawKey(rawKey, plaintext) {
|
|
892
|
+
const key = await globalThis.crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt"]);
|
|
893
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
894
|
+
const out = await globalThis.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext));
|
|
895
|
+
return {
|
|
896
|
+
ciphertext: toBase64(out),
|
|
897
|
+
iv: toBase64(iv.buffer)
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
async function decryptWithRawKey(rawKey, ciphertextB64, ivB64) {
|
|
901
|
+
const key = await globalThis.crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["decrypt"]);
|
|
902
|
+
const out = await globalThis.crypto.subtle.decrypt(
|
|
903
|
+
{ name: "AES-GCM", iv: new Uint8Array(fromBase64(ivB64)) },
|
|
904
|
+
key,
|
|
905
|
+
fromBase64(ciphertextB64)
|
|
906
|
+
);
|
|
907
|
+
return new TextDecoder().decode(out);
|
|
908
|
+
}
|
|
276
909
|
var Wolfronix = class {
|
|
277
910
|
/**
|
|
278
911
|
* Create a new Wolfronix client
|
|
@@ -293,6 +926,9 @@ var Wolfronix = class {
|
|
|
293
926
|
this.publicKey = null;
|
|
294
927
|
this.privateKey = null;
|
|
295
928
|
this.publicKeyPEM = null;
|
|
929
|
+
this.pfsIdentityPrivateJwk = null;
|
|
930
|
+
this.pfsIdentityPublicJwk = null;
|
|
931
|
+
this.pfsSessions = /* @__PURE__ */ new Map();
|
|
296
932
|
if (typeof config === "string") {
|
|
297
933
|
this.config = {
|
|
298
934
|
baseUrl: config,
|
|
@@ -356,14 +992,8 @@ var Wolfronix = class {
|
|
|
356
992
|
headers,
|
|
357
993
|
signal: controller.signal
|
|
358
994
|
};
|
|
359
|
-
if (this.config.insecure && typeof process !== "undefined") {
|
|
360
|
-
|
|
361
|
-
const { Agent } = await import("./undici-BDVTXO27.mjs");
|
|
362
|
-
fetchOptions.dispatcher = new Agent({
|
|
363
|
-
connect: { rejectUnauthorized: false }
|
|
364
|
-
});
|
|
365
|
-
} catch {
|
|
366
|
-
}
|
|
995
|
+
if (this.config.insecure && typeof process !== "undefined" && process.env) {
|
|
996
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
367
997
|
}
|
|
368
998
|
if (formData) {
|
|
369
999
|
fetchOptions.body = formData;
|
|
@@ -418,6 +1048,59 @@ var Wolfronix = class {
|
|
|
418
1048
|
throw new AuthenticationError("Not authenticated. Call login() or register() first.");
|
|
419
1049
|
}
|
|
420
1050
|
}
|
|
1051
|
+
toBlob(file) {
|
|
1052
|
+
if (file instanceof File || file instanceof Blob) {
|
|
1053
|
+
return file;
|
|
1054
|
+
}
|
|
1055
|
+
if (file instanceof ArrayBuffer) {
|
|
1056
|
+
return new Blob([new Uint8Array(file)]);
|
|
1057
|
+
}
|
|
1058
|
+
if (file instanceof Uint8Array) {
|
|
1059
|
+
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
1060
|
+
return new Blob([arrayBuffer]);
|
|
1061
|
+
}
|
|
1062
|
+
throw new ValidationError("Invalid file type. Expected File, Blob, Buffer, or ArrayBuffer");
|
|
1063
|
+
}
|
|
1064
|
+
async ensurePfsIdentity() {
|
|
1065
|
+
if (this.pfsIdentityPrivateJwk && this.pfsIdentityPublicJwk) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const kp = await generatePfsRatchetKeyPair();
|
|
1069
|
+
this.pfsIdentityPrivateJwk = await exportPrivateJwk(kp.privateKey);
|
|
1070
|
+
this.pfsIdentityPublicJwk = await exportPublicJwk(kp.publicKey);
|
|
1071
|
+
}
|
|
1072
|
+
getPfsSession(sessionId) {
|
|
1073
|
+
const session = this.pfsSessions.get(sessionId);
|
|
1074
|
+
if (!session) {
|
|
1075
|
+
throw new ValidationError(`PFS session not found: ${sessionId}`);
|
|
1076
|
+
}
|
|
1077
|
+
return session;
|
|
1078
|
+
}
|
|
1079
|
+
async ratchetForSend(session) {
|
|
1080
|
+
const nextRatchet = await generatePfsRatchetKeyPair();
|
|
1081
|
+
const nextPriv = await exportPrivateJwk(nextRatchet.privateKey);
|
|
1082
|
+
const nextPub = await exportPublicJwk(nextRatchet.publicKey);
|
|
1083
|
+
const dh = await deriveEcdhSecret(nextPriv, session.their_ratchet_public_jwk);
|
|
1084
|
+
const mixed = await deriveRootAndChains(session.root_key, dh);
|
|
1085
|
+
session.root_key = mixed.rootKey;
|
|
1086
|
+
session.send_chain_key = mixed.chainA;
|
|
1087
|
+
session.recv_chain_key = mixed.chainB;
|
|
1088
|
+
session.prev_send_count = session.send_count;
|
|
1089
|
+
session.send_count = 0;
|
|
1090
|
+
session.my_ratchet_private_jwk = nextPriv;
|
|
1091
|
+
session.my_ratchet_public_jwk = nextPub;
|
|
1092
|
+
session.updated_at = Date.now();
|
|
1093
|
+
}
|
|
1094
|
+
async ratchetForReceive(session, theirRatchetPub) {
|
|
1095
|
+
const dh = await deriveEcdhSecret(session.my_ratchet_private_jwk, theirRatchetPub);
|
|
1096
|
+
const mixed = await deriveRootAndChains(session.root_key, dh);
|
|
1097
|
+
session.root_key = mixed.rootKey;
|
|
1098
|
+
session.recv_chain_key = mixed.chainA;
|
|
1099
|
+
session.send_chain_key = mixed.chainB;
|
|
1100
|
+
session.recv_count = 0;
|
|
1101
|
+
session.their_ratchet_public_jwk = theirRatchetPub;
|
|
1102
|
+
session.updated_at = Date.now();
|
|
1103
|
+
}
|
|
421
1104
|
// ==========================================================================
|
|
422
1105
|
// Authentication Methods
|
|
423
1106
|
// ==========================================================================
|
|
@@ -429,13 +1112,23 @@ var Wolfronix = class {
|
|
|
429
1112
|
* const { user_id, token } = await wfx.register('user@example.com', 'password123');
|
|
430
1113
|
* ```
|
|
431
1114
|
*/
|
|
432
|
-
async register(email, password) {
|
|
1115
|
+
async register(email, password, options = {}) {
|
|
433
1116
|
if (!email || !password) {
|
|
434
1117
|
throw new ValidationError("Email and password are required");
|
|
435
1118
|
}
|
|
436
1119
|
const keyPair = await generateKeyPair();
|
|
437
1120
|
const publicKeyPEM = await exportKeyToPEM(keyPair.publicKey, "public");
|
|
438
1121
|
const { encryptedKey, salt } = await wrapPrivateKey(keyPair.privateKey, password);
|
|
1122
|
+
const enableRecovery = options.enableRecovery !== false;
|
|
1123
|
+
const recoveryWords = enableRecovery ? options.recoveryPhrase ? options.recoveryPhrase.trim().split(/\s+/).filter(Boolean) : generateRecoveryWords(24) : [];
|
|
1124
|
+
const recoveryPhrase = recoveryWords.join(" ");
|
|
1125
|
+
let recoveryEncryptedPrivateKey = "";
|
|
1126
|
+
let recoverySalt = "";
|
|
1127
|
+
if (enableRecovery && recoveryPhrase) {
|
|
1128
|
+
const recoveryWrap = await wrapPrivateKey(keyPair.privateKey, recoveryPhrase);
|
|
1129
|
+
recoveryEncryptedPrivateKey = recoveryWrap.encryptedKey;
|
|
1130
|
+
recoverySalt = recoveryWrap.salt;
|
|
1131
|
+
}
|
|
439
1132
|
const response = await this.request("POST", "/api/v1/keys/register", {
|
|
440
1133
|
body: {
|
|
441
1134
|
client_id: this.config.clientId,
|
|
@@ -443,18 +1136,30 @@ var Wolfronix = class {
|
|
|
443
1136
|
// Using email as user_id for simplicity
|
|
444
1137
|
public_key_pem: publicKeyPEM,
|
|
445
1138
|
encrypted_private_key: encryptedKey,
|
|
446
|
-
salt
|
|
1139
|
+
salt,
|
|
1140
|
+
recovery_encrypted_private_key: recoveryEncryptedPrivateKey,
|
|
1141
|
+
recovery_salt: recoverySalt
|
|
447
1142
|
},
|
|
448
1143
|
includeAuth: false
|
|
449
1144
|
});
|
|
450
|
-
if (response.success) {
|
|
1145
|
+
if (response.status === "success" || response.success) {
|
|
451
1146
|
this.userId = email;
|
|
452
1147
|
this.publicKey = keyPair.publicKey;
|
|
453
1148
|
this.privateKey = keyPair.privateKey;
|
|
454
1149
|
this.publicKeyPEM = publicKeyPEM;
|
|
455
1150
|
this.token = "zk-session";
|
|
456
1151
|
}
|
|
457
|
-
|
|
1152
|
+
const out = {
|
|
1153
|
+
success: response.status === "success" || response.success === true,
|
|
1154
|
+
user_id: response.user_id || email,
|
|
1155
|
+
token: this.token || "zk-session",
|
|
1156
|
+
message: response.message || "Keys registered successfully"
|
|
1157
|
+
};
|
|
1158
|
+
if (enableRecovery && recoveryPhrase) {
|
|
1159
|
+
out.recoveryPhrase = recoveryPhrase;
|
|
1160
|
+
out.recoveryWords = recoveryWords;
|
|
1161
|
+
}
|
|
1162
|
+
return out;
|
|
458
1163
|
}
|
|
459
1164
|
/**
|
|
460
1165
|
* Login with existing credentials
|
|
@@ -498,6 +1203,69 @@ var Wolfronix = class {
|
|
|
498
1203
|
throw new AuthenticationError("Invalid password (decryption failed)");
|
|
499
1204
|
}
|
|
500
1205
|
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Recover account keys using a 24-word recovery phrase and set a new password.
|
|
1208
|
+
* Returns a fresh local auth session if recovery succeeds.
|
|
1209
|
+
*/
|
|
1210
|
+
async recoverAccount(email, recoveryPhrase, newPassword) {
|
|
1211
|
+
if (!email || !recoveryPhrase || !newPassword) {
|
|
1212
|
+
throw new ValidationError("email, recoveryPhrase, and newPassword are required");
|
|
1213
|
+
}
|
|
1214
|
+
const response = await this.request("POST", "/api/v1/keys/recover", {
|
|
1215
|
+
body: {
|
|
1216
|
+
client_id: this.config.clientId,
|
|
1217
|
+
user_id: email
|
|
1218
|
+
},
|
|
1219
|
+
includeAuth: false
|
|
1220
|
+
});
|
|
1221
|
+
if (!response.recovery_encrypted_private_key || !response.recovery_salt || !response.public_key_pem) {
|
|
1222
|
+
throw new AuthenticationError("Recovery material not found for this account");
|
|
1223
|
+
}
|
|
1224
|
+
const recoveredPrivateKey = await unwrapPrivateKey(
|
|
1225
|
+
response.recovery_encrypted_private_key,
|
|
1226
|
+
recoveryPhrase,
|
|
1227
|
+
response.recovery_salt
|
|
1228
|
+
);
|
|
1229
|
+
const newPasswordWrap = await wrapPrivateKey(recoveredPrivateKey, newPassword);
|
|
1230
|
+
await this.request("POST", "/api/v1/keys/update-password", {
|
|
1231
|
+
body: {
|
|
1232
|
+
client_id: this.config.clientId,
|
|
1233
|
+
user_id: email,
|
|
1234
|
+
encrypted_private_key: newPasswordWrap.encryptedKey,
|
|
1235
|
+
salt: newPasswordWrap.salt
|
|
1236
|
+
},
|
|
1237
|
+
includeAuth: false
|
|
1238
|
+
});
|
|
1239
|
+
this.privateKey = recoveredPrivateKey;
|
|
1240
|
+
this.publicKeyPEM = response.public_key_pem;
|
|
1241
|
+
this.publicKey = await importKeyFromPEM(response.public_key_pem, "public");
|
|
1242
|
+
this.userId = email;
|
|
1243
|
+
this.token = "zk-session";
|
|
1244
|
+
return {
|
|
1245
|
+
success: true,
|
|
1246
|
+
user_id: email,
|
|
1247
|
+
token: this.token,
|
|
1248
|
+
message: "Account recovered successfully"
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Rotates long-term RSA identity keys and re-wraps with password (+ optional recovery phrase).
|
|
1253
|
+
* Use this periodically to reduce long-term key exposure.
|
|
1254
|
+
*/
|
|
1255
|
+
async rotateIdentityKeys(password, recoveryPhrase) {
|
|
1256
|
+
this.ensureAuthenticated();
|
|
1257
|
+
if (!password) {
|
|
1258
|
+
throw new ValidationError("password is required");
|
|
1259
|
+
}
|
|
1260
|
+
if (recoveryPhrase !== void 0 && !recoveryPhrase.trim()) {
|
|
1261
|
+
throw new ValidationError("recoveryPhrase must be non-empty when provided");
|
|
1262
|
+
}
|
|
1263
|
+
throw new WolfronixError(
|
|
1264
|
+
"rotateIdentityKeys is not supported by the current server API. Use recoverAccount() to re-wrap the existing private key with a new password.",
|
|
1265
|
+
"NOT_SUPPORTED",
|
|
1266
|
+
501
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
501
1269
|
/**
|
|
502
1270
|
* Set authentication token directly (useful for server-side apps)
|
|
503
1271
|
*
|
|
@@ -554,20 +1322,8 @@ var Wolfronix = class {
|
|
|
554
1322
|
async encrypt(file, filename) {
|
|
555
1323
|
this.ensureAuthenticated();
|
|
556
1324
|
const formData = new FormData();
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
} else if (file instanceof Blob) {
|
|
560
|
-
formData.append("file", file, filename || "file");
|
|
561
|
-
} else if (file instanceof ArrayBuffer) {
|
|
562
|
-
const blob = new Blob([new Uint8Array(file)]);
|
|
563
|
-
formData.append("file", blob, filename || "file");
|
|
564
|
-
} else if (file instanceof Uint8Array) {
|
|
565
|
-
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
|
566
|
-
const blob = new Blob([arrayBuffer]);
|
|
567
|
-
formData.append("file", blob, filename || "file");
|
|
568
|
-
} else {
|
|
569
|
-
throw new ValidationError("Invalid file type. Expected File, Blob, Buffer, or ArrayBuffer");
|
|
570
|
-
}
|
|
1325
|
+
const blob = this.toBlob(file);
|
|
1326
|
+
formData.append("file", blob, filename || (file instanceof File ? file.name : "file"));
|
|
571
1327
|
formData.append("user_id", this.userId || "");
|
|
572
1328
|
if (!this.publicKeyPEM) {
|
|
573
1329
|
throw new Error("Public key not available. Is user logged in?");
|
|
@@ -581,6 +1337,65 @@ var Wolfronix = class {
|
|
|
581
1337
|
file_id: String(response.file_id)
|
|
582
1338
|
};
|
|
583
1339
|
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Resumable large-file encryption upload.
|
|
1342
|
+
* Splits a file into chunks (default 10MB) and uploads each chunk independently.
|
|
1343
|
+
* If upload fails mid-way, pass the returned state as `existingState` to resume.
|
|
1344
|
+
*/
|
|
1345
|
+
async encryptResumable(file, options = {}) {
|
|
1346
|
+
this.ensureAuthenticated();
|
|
1347
|
+
const chunkSize = options.chunkSizeBytes || 10 * 1024 * 1024;
|
|
1348
|
+
if (chunkSize < 1024 * 1024) {
|
|
1349
|
+
throw new ValidationError("chunkSizeBytes must be at least 1MB");
|
|
1350
|
+
}
|
|
1351
|
+
const blob = this.toBlob(file);
|
|
1352
|
+
const filename = options.filename || (file instanceof File ? file.name : "file.bin");
|
|
1353
|
+
const totalChunks = Math.ceil(blob.size / chunkSize);
|
|
1354
|
+
const baseUploadId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1355
|
+
const state = options.existingState || {
|
|
1356
|
+
upload_id: baseUploadId,
|
|
1357
|
+
filename,
|
|
1358
|
+
file_size: blob.size,
|
|
1359
|
+
chunk_size_bytes: chunkSize,
|
|
1360
|
+
total_chunks: totalChunks,
|
|
1361
|
+
uploaded_chunks: [],
|
|
1362
|
+
chunk_file_ids: new Array(totalChunks).fill(""),
|
|
1363
|
+
created_at: Date.now(),
|
|
1364
|
+
updated_at: Date.now()
|
|
1365
|
+
};
|
|
1366
|
+
if (state.file_size !== blob.size || state.total_chunks !== totalChunks) {
|
|
1367
|
+
throw new ValidationError("existingState does not match current file/chunking settings");
|
|
1368
|
+
}
|
|
1369
|
+
const uploadedSet = new Set(state.uploaded_chunks);
|
|
1370
|
+
let uploaded = uploadedSet.size;
|
|
1371
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
1372
|
+
if (uploadedSet.has(i)) {
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
const start = i * chunkSize;
|
|
1376
|
+
const end = Math.min(start + chunkSize, blob.size);
|
|
1377
|
+
const chunkBlob = blob.slice(start, end);
|
|
1378
|
+
const chunkName = `${filename}.part-${String(i + 1).padStart(6, "0")}-of-${String(totalChunks).padStart(6, "0")}`;
|
|
1379
|
+
const enc = await this.encrypt(chunkBlob, chunkName);
|
|
1380
|
+
state.chunk_file_ids[i] = enc.file_id;
|
|
1381
|
+
state.uploaded_chunks.push(i);
|
|
1382
|
+
state.updated_at = Date.now();
|
|
1383
|
+
uploaded++;
|
|
1384
|
+
if (options.onProgress) {
|
|
1385
|
+
options.onProgress(uploaded, totalChunks);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const result = {
|
|
1389
|
+
upload_id: state.upload_id,
|
|
1390
|
+
filename: state.filename,
|
|
1391
|
+
total_chunks: state.total_chunks,
|
|
1392
|
+
chunk_size_bytes: state.chunk_size_bytes,
|
|
1393
|
+
uploaded_chunks: state.uploaded_chunks.length,
|
|
1394
|
+
chunk_file_ids: state.chunk_file_ids,
|
|
1395
|
+
complete: state.uploaded_chunks.length === state.total_chunks
|
|
1396
|
+
};
|
|
1397
|
+
return { result, state };
|
|
1398
|
+
}
|
|
584
1399
|
/**
|
|
585
1400
|
* Decrypt and retrieve a file using zero-knowledge flow.
|
|
586
1401
|
*
|
|
@@ -641,6 +1456,41 @@ var Wolfronix = class {
|
|
|
641
1456
|
}
|
|
642
1457
|
});
|
|
643
1458
|
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Decrypts and reassembles a chunked upload produced by `encryptResumable`.
|
|
1461
|
+
*/
|
|
1462
|
+
async decryptChunkedToBuffer(manifest, role = "owner") {
|
|
1463
|
+
this.ensureAuthenticated();
|
|
1464
|
+
if (!manifest?.chunk_file_ids?.length) {
|
|
1465
|
+
throw new ValidationError("manifest.chunk_file_ids is required");
|
|
1466
|
+
}
|
|
1467
|
+
const chunks = [];
|
|
1468
|
+
let totalLength = 0;
|
|
1469
|
+
for (const fileId of manifest.chunk_file_ids) {
|
|
1470
|
+
if (!fileId) {
|
|
1471
|
+
throw new ValidationError("manifest contains empty chunk file ID");
|
|
1472
|
+
}
|
|
1473
|
+
const part = await this.decryptToBuffer(fileId, role);
|
|
1474
|
+
const bytes = new Uint8Array(part);
|
|
1475
|
+
chunks.push(bytes);
|
|
1476
|
+
totalLength += bytes.byteLength;
|
|
1477
|
+
}
|
|
1478
|
+
const merged = new Uint8Array(totalLength);
|
|
1479
|
+
let offset = 0;
|
|
1480
|
+
for (const part of chunks) {
|
|
1481
|
+
merged.set(part, offset);
|
|
1482
|
+
offset += part.byteLength;
|
|
1483
|
+
}
|
|
1484
|
+
return merged.buffer;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Decrypts and reassembles a chunked upload into a Blob.
|
|
1488
|
+
* This is a browser-friendly alias over `decryptChunkedToBuffer`.
|
|
1489
|
+
*/
|
|
1490
|
+
async decryptChunkedManifest(manifest, role = "owner") {
|
|
1491
|
+
const merged = await this.decryptChunkedToBuffer(manifest, role);
|
|
1492
|
+
return new Blob([merged], { type: "application/octet-stream" });
|
|
1493
|
+
}
|
|
644
1494
|
/**
|
|
645
1495
|
* Fetch the encrypted key_part_a for a file (for client-side decryption)
|
|
646
1496
|
*
|
|
@@ -762,6 +1612,197 @@ var Wolfronix = class {
|
|
|
762
1612
|
throw new Error("Decryption failed. You may not be the intended recipient.");
|
|
763
1613
|
}
|
|
764
1614
|
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Create/share a pre-key bundle for Double Ratchet PFS session setup.
|
|
1617
|
+
* Exchange this bundle out-of-band with the peer.
|
|
1618
|
+
*/
|
|
1619
|
+
async createPfsPreKeyBundle() {
|
|
1620
|
+
this.ensureAuthenticated();
|
|
1621
|
+
await this.ensurePfsIdentity();
|
|
1622
|
+
return {
|
|
1623
|
+
protocol: "wfx-dr-v1",
|
|
1624
|
+
user_id: this.userId || void 0,
|
|
1625
|
+
ratchet_pub_jwk: this.pfsIdentityPublicJwk,
|
|
1626
|
+
created_at: Date.now()
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Initialize a local PFS ratchet session from peer bundle.
|
|
1631
|
+
* Both sides must call this with opposite `asInitiator` values.
|
|
1632
|
+
*/
|
|
1633
|
+
async initPfsSession(sessionId, peerBundle, asInitiator) {
|
|
1634
|
+
this.ensureAuthenticated();
|
|
1635
|
+
if (!sessionId) {
|
|
1636
|
+
throw new ValidationError("sessionId is required");
|
|
1637
|
+
}
|
|
1638
|
+
if (!peerBundle || peerBundle.protocol !== PFS_PROTOCOL || !peerBundle.ratchet_pub_jwk) {
|
|
1639
|
+
throw new ValidationError("Invalid peerBundle");
|
|
1640
|
+
}
|
|
1641
|
+
await this.ensurePfsIdentity();
|
|
1642
|
+
const myPriv = this.pfsIdentityPrivateJwk;
|
|
1643
|
+
const myPub = this.pfsIdentityPublicJwk;
|
|
1644
|
+
const theirPub = peerBundle.ratchet_pub_jwk;
|
|
1645
|
+
const dh = await deriveEcdhSecret(myPriv, theirPub);
|
|
1646
|
+
const mixed = await deriveRootAndChains(toBase64(ZERO_32.buffer), dh);
|
|
1647
|
+
const session = {
|
|
1648
|
+
protocol: "wfx-dr-v1",
|
|
1649
|
+
session_id: sessionId,
|
|
1650
|
+
role: asInitiator ? "initiator" : "responder",
|
|
1651
|
+
root_key: mixed.rootKey,
|
|
1652
|
+
send_chain_key: asInitiator ? mixed.chainA : mixed.chainB,
|
|
1653
|
+
recv_chain_key: asInitiator ? mixed.chainB : mixed.chainA,
|
|
1654
|
+
send_count: 0,
|
|
1655
|
+
recv_count: 0,
|
|
1656
|
+
prev_send_count: 0,
|
|
1657
|
+
my_ratchet_private_jwk: myPriv,
|
|
1658
|
+
my_ratchet_public_jwk: myPub,
|
|
1659
|
+
their_ratchet_public_jwk: theirPub,
|
|
1660
|
+
skipped_keys: {},
|
|
1661
|
+
created_at: Date.now(),
|
|
1662
|
+
updated_at: Date.now()
|
|
1663
|
+
};
|
|
1664
|
+
this.pfsSessions.set(sessionId, session);
|
|
1665
|
+
return session;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Export session state for persistence (e.g., localStorage/DB).
|
|
1669
|
+
*/
|
|
1670
|
+
exportPfsSession(sessionId) {
|
|
1671
|
+
const session = this.getPfsSession(sessionId);
|
|
1672
|
+
return JSON.parse(JSON.stringify(session));
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Import session state from storage.
|
|
1676
|
+
*/
|
|
1677
|
+
importPfsSession(session) {
|
|
1678
|
+
if (!session || session.protocol !== PFS_PROTOCOL || !session.session_id) {
|
|
1679
|
+
throw new ValidationError("Invalid PFS session payload");
|
|
1680
|
+
}
|
|
1681
|
+
this.pfsSessions.set(session.session_id, JSON.parse(JSON.stringify(session)));
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Encrypt a message using Double Ratchet session state.
|
|
1685
|
+
*/
|
|
1686
|
+
async pfsEncryptMessage(sessionId, plaintext) {
|
|
1687
|
+
this.ensureAuthenticated();
|
|
1688
|
+
if (!plaintext) {
|
|
1689
|
+
throw new ValidationError("plaintext is required");
|
|
1690
|
+
}
|
|
1691
|
+
const session = this.getPfsSession(sessionId);
|
|
1692
|
+
await this.ratchetForSend(session);
|
|
1693
|
+
const n = session.send_count;
|
|
1694
|
+
const msgKey = await deriveMessageKey(session.send_chain_key, n);
|
|
1695
|
+
const enc = await encryptWithRawKey(msgKey, plaintext);
|
|
1696
|
+
session.send_chain_key = await deriveNextChainKey(session.send_chain_key);
|
|
1697
|
+
session.send_count += 1;
|
|
1698
|
+
session.updated_at = Date.now();
|
|
1699
|
+
return {
|
|
1700
|
+
v: 1,
|
|
1701
|
+
type: "pfs_ratchet",
|
|
1702
|
+
session_id: sessionId,
|
|
1703
|
+
n,
|
|
1704
|
+
pn: session.prev_send_count,
|
|
1705
|
+
ratchet_pub_jwk: session.my_ratchet_public_jwk,
|
|
1706
|
+
iv: enc.iv,
|
|
1707
|
+
ciphertext: enc.ciphertext,
|
|
1708
|
+
timestamp: Date.now()
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* Decrypt a Double Ratchet packet for a session.
|
|
1713
|
+
* Handles basic out-of-order delivery through skipped message keys.
|
|
1714
|
+
*/
|
|
1715
|
+
async pfsDecryptMessage(sessionId, packet) {
|
|
1716
|
+
this.ensureAuthenticated();
|
|
1717
|
+
const session = this.getPfsSession(sessionId);
|
|
1718
|
+
const msg = typeof packet === "string" ? JSON.parse(packet) : packet;
|
|
1719
|
+
if (!msg || msg.type !== "pfs_ratchet" || msg.session_id !== sessionId) {
|
|
1720
|
+
throw new ValidationError("Invalid PFS message packet");
|
|
1721
|
+
}
|
|
1722
|
+
if (normalizeJwk(msg.ratchet_pub_jwk) !== normalizeJwk(session.their_ratchet_public_jwk)) {
|
|
1723
|
+
await this.ratchetForReceive(session, msg.ratchet_pub_jwk);
|
|
1724
|
+
}
|
|
1725
|
+
while (session.recv_count < msg.n) {
|
|
1726
|
+
const skippedKey = await deriveMessageKey(session.recv_chain_key, session.recv_count);
|
|
1727
|
+
session.skipped_keys[ratchetKeyId(session.their_ratchet_public_jwk, session.recv_count)] = toBase64(skippedKey);
|
|
1728
|
+
session.recv_chain_key = await deriveNextChainKey(session.recv_chain_key);
|
|
1729
|
+
session.recv_count += 1;
|
|
1730
|
+
}
|
|
1731
|
+
const skipId = ratchetKeyId(session.their_ratchet_public_jwk, msg.n);
|
|
1732
|
+
let msgKey;
|
|
1733
|
+
if (session.skipped_keys[skipId]) {
|
|
1734
|
+
msgKey = fromBase64(session.skipped_keys[skipId]);
|
|
1735
|
+
delete session.skipped_keys[skipId];
|
|
1736
|
+
} else {
|
|
1737
|
+
msgKey = await deriveMessageKey(session.recv_chain_key, msg.n);
|
|
1738
|
+
session.recv_chain_key = await deriveNextChainKey(session.recv_chain_key);
|
|
1739
|
+
session.recv_count = msg.n + 1;
|
|
1740
|
+
}
|
|
1741
|
+
session.updated_at = Date.now();
|
|
1742
|
+
return decryptWithRawKey(msgKey, msg.ciphertext, msg.iv);
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Group message encryption using sender-key fanout:
|
|
1746
|
+
* message encrypted once with AES key, AES key wrapped for each group member with their RSA public key.
|
|
1747
|
+
*/
|
|
1748
|
+
async encryptGroupMessage(text, groupId, recipientIds) {
|
|
1749
|
+
this.ensureAuthenticated();
|
|
1750
|
+
if (!text || !groupId) {
|
|
1751
|
+
throw new ValidationError("text and groupId are required");
|
|
1752
|
+
}
|
|
1753
|
+
if (!recipientIds?.length) {
|
|
1754
|
+
throw new ValidationError("recipientIds cannot be empty");
|
|
1755
|
+
}
|
|
1756
|
+
const uniqueRecipients = Array.from(new Set(recipientIds.filter(Boolean)));
|
|
1757
|
+
if (this.userId && !uniqueRecipients.includes(this.userId)) {
|
|
1758
|
+
uniqueRecipients.push(this.userId);
|
|
1759
|
+
}
|
|
1760
|
+
const sessionKey = await generateSessionKey();
|
|
1761
|
+
const { encrypted: ciphertext, iv } = await encryptData(text, sessionKey);
|
|
1762
|
+
const rawSessionKey = await exportSessionKey(sessionKey);
|
|
1763
|
+
const recipientKeys = {};
|
|
1764
|
+
for (const rid of uniqueRecipients) {
|
|
1765
|
+
const pem = await this.getPublicKey(rid);
|
|
1766
|
+
const pub = await importKeyFromPEM(pem, "public");
|
|
1767
|
+
recipientKeys[rid] = await rsaEncrypt(rawSessionKey, pub);
|
|
1768
|
+
}
|
|
1769
|
+
const packet = {
|
|
1770
|
+
v: 1,
|
|
1771
|
+
type: "group_sender_key",
|
|
1772
|
+
sender_id: this.userId || "",
|
|
1773
|
+
group_id: groupId,
|
|
1774
|
+
timestamp: Date.now(),
|
|
1775
|
+
ciphertext,
|
|
1776
|
+
iv,
|
|
1777
|
+
recipient_keys: recipientKeys
|
|
1778
|
+
};
|
|
1779
|
+
return JSON.stringify(packet);
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Decrypt a packet produced by `encryptGroupMessage`.
|
|
1783
|
+
*/
|
|
1784
|
+
async decryptGroupMessage(packetJson) {
|
|
1785
|
+
this.ensureAuthenticated();
|
|
1786
|
+
if (!this.privateKey || !this.userId) {
|
|
1787
|
+
throw new Error("Private key not available. Is user logged in?");
|
|
1788
|
+
}
|
|
1789
|
+
let packet;
|
|
1790
|
+
try {
|
|
1791
|
+
packet = JSON.parse(packetJson);
|
|
1792
|
+
} catch {
|
|
1793
|
+
throw new ValidationError("Invalid group packet format");
|
|
1794
|
+
}
|
|
1795
|
+
if (packet.type !== "group_sender_key" || !packet.recipient_keys || !packet.ciphertext || !packet.iv) {
|
|
1796
|
+
throw new ValidationError("Invalid group packet structure");
|
|
1797
|
+
}
|
|
1798
|
+
const wrappedKey = packet.recipient_keys[this.userId];
|
|
1799
|
+
if (!wrappedKey) {
|
|
1800
|
+
throw new PermissionDeniedError("You are not a recipient of this group message");
|
|
1801
|
+
}
|
|
1802
|
+
const rawSessionKey = await rsaDecrypt(wrappedKey, this.privateKey);
|
|
1803
|
+
const sessionKey = await importSessionKey(rawSessionKey);
|
|
1804
|
+
return decryptData(packet.ciphertext, packet.iv, sessionKey);
|
|
1805
|
+
}
|
|
765
1806
|
// ==========================================================================
|
|
766
1807
|
// Server-Side Message Encryption (Dual-Key Split)
|
|
767
1808
|
// ==========================================================================
|
|
@@ -1158,14 +2199,8 @@ var WolfronixAdmin = class {
|
|
|
1158
2199
|
headers,
|
|
1159
2200
|
signal: controller.signal
|
|
1160
2201
|
};
|
|
1161
|
-
if (this.insecure && typeof process !== "undefined") {
|
|
1162
|
-
|
|
1163
|
-
const { Agent } = await import("./undici-BDVTXO27.mjs");
|
|
1164
|
-
fetchOptions.dispatcher = new Agent({
|
|
1165
|
-
connect: { rejectUnauthorized: false }
|
|
1166
|
-
});
|
|
1167
|
-
} catch {
|
|
1168
|
-
}
|
|
2202
|
+
if (this.insecure && typeof process !== "undefined" && process.env) {
|
|
2203
|
+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
1169
2204
|
}
|
|
1170
2205
|
if (body) {
|
|
1171
2206
|
fetchOptions.body = JSON.stringify(body);
|