@unicitylabs/nostr-js-sdk 0.1.0 → 0.2.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/README.md +86 -5
- package/dist/browser/index.js +1650 -75
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.min.js +7 -6
- package/dist/browser/index.min.js.map +1 -1
- package/dist/browser/index.umd.js +1659 -80
- package/dist/browser/index.umd.js.map +1 -1
- package/dist/browser/index.umd.min.js +7 -6
- package/dist/browser/index.umd.min.js.map +1 -1
- package/dist/cjs/NostrKeyManager.js +57 -0
- package/dist/cjs/NostrKeyManager.js.map +1 -1
- package/dist/cjs/client/NostrClient.js +46 -0
- package/dist/cjs/client/NostrClient.js.map +1 -1
- package/dist/cjs/crypto/index.js +4 -2
- package/dist/cjs/crypto/index.js.map +1 -1
- package/dist/cjs/crypto/nip04.js +14 -3
- package/dist/cjs/crypto/nip04.js.map +1 -1
- package/dist/cjs/crypto/nip44.js +297 -0
- package/dist/cjs/crypto/nip44.js.map +1 -0
- package/dist/cjs/index.js +27 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/messaging/index.js +24 -0
- package/dist/cjs/messaging/index.js.map +1 -0
- package/dist/cjs/messaging/nip17.js +267 -0
- package/dist/cjs/messaging/nip17.js.map +1 -0
- package/dist/cjs/messaging/types.js +20 -0
- package/dist/cjs/messaging/types.js.map +1 -0
- package/dist/cjs/nametag/NametagUtils.js +1 -1
- package/dist/cjs/nametag/NametagUtils.js.map +1 -1
- package/dist/cjs/payment/PaymentRequestProtocol.js +2 -0
- package/dist/cjs/payment/PaymentRequestProtocol.js.map +1 -1
- package/dist/cjs/protocol/EventKinds.js +13 -1
- package/dist/cjs/protocol/EventKinds.js.map +1 -1
- package/dist/esm/NostrKeyManager.js +57 -0
- package/dist/esm/NostrKeyManager.js.map +1 -1
- package/dist/esm/client/NostrClient.js +46 -0
- package/dist/esm/client/NostrClient.js.map +1 -1
- package/dist/esm/crypto/index.js +3 -1
- package/dist/esm/crypto/index.js.map +1 -1
- package/dist/esm/crypto/nip04.js +14 -3
- package/dist/esm/crypto/nip04.js.map +1 -1
- package/dist/esm/crypto/nip44.js +283 -0
- package/dist/esm/crypto/nip44.js.map +1 -0
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/messaging/index.js +8 -0
- package/dist/esm/messaging/index.js.map +1 -0
- package/dist/esm/messaging/nip17.js +229 -0
- package/dist/esm/messaging/nip17.js.map +1 -0
- package/dist/esm/messaging/types.js +16 -0
- package/dist/esm/messaging/types.js.map +1 -0
- package/dist/esm/nametag/NametagUtils.js +1 -1
- package/dist/esm/nametag/NametagUtils.js.map +1 -1
- package/dist/esm/payment/PaymentRequestProtocol.js +2 -0
- package/dist/esm/payment/PaymentRequestProtocol.js.map +1 -1
- package/dist/esm/protocol/EventKinds.js +12 -0
- package/dist/esm/protocol/EventKinds.js.map +1 -1
- package/dist/types/NostrKeyManager.d.ts +36 -0
- package/dist/types/NostrKeyManager.d.ts.map +1 -1
- package/dist/types/client/NostrClient.d.ts +31 -0
- package/dist/types/client/NostrClient.d.ts.map +1 -1
- package/dist/types/crypto/index.d.ts +1 -1
- package/dist/types/crypto/index.d.ts.map +1 -1
- package/dist/types/crypto/nip04.d.ts.map +1 -1
- package/dist/types/crypto/nip44.d.ts +78 -0
- package/dist/types/crypto/nip44.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/messaging/index.d.ts +8 -0
- package/dist/types/messaging/index.d.ts.map +1 -0
- package/dist/types/messaging/nip17.d.ts +42 -0
- package/dist/types/messaging/nip17.d.ts.map +1 -0
- package/dist/types/messaging/types.d.ts +59 -0
- package/dist/types/messaging/types.d.ts.map +1 -0
- package/dist/types/payment/PaymentRequestProtocol.d.ts.map +1 -1
- package/dist/types/protocol/EventKinds.d.ts +6 -0
- package/dist/types/protocol/EventKinds.d.ts.map +1 -1
- package/package.json +2 -1
|
@@ -18,17 +18,17 @@
|
|
|
18
18
|
// Makes the utils un-importable in browsers without a bundler.
|
|
19
19
|
// Once node.js 18 is deprecated (2025-04-30), we can just drop the import.
|
|
20
20
|
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
21
|
-
function isBytes(a) {
|
|
21
|
+
function isBytes$1(a) {
|
|
22
22
|
return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');
|
|
23
23
|
}
|
|
24
24
|
/** Asserts something is positive integer. */
|
|
25
|
-
function anumber(n) {
|
|
25
|
+
function anumber$1(n) {
|
|
26
26
|
if (!Number.isSafeInteger(n) || n < 0)
|
|
27
27
|
throw new Error('positive integer expected, got ' + n);
|
|
28
28
|
}
|
|
29
29
|
/** Asserts something is Uint8Array. */
|
|
30
|
-
function abytes(b, ...lengths) {
|
|
31
|
-
if (!isBytes(b))
|
|
30
|
+
function abytes$1(b, ...lengths) {
|
|
31
|
+
if (!isBytes$1(b))
|
|
32
32
|
throw new Error('Uint8Array expected');
|
|
33
33
|
if (lengths.length > 0 && !lengths.includes(b.length))
|
|
34
34
|
throw new Error('Uint8Array expected of length ' + lengths + ', got length=' + b.length);
|
|
@@ -37,32 +37,32 @@
|
|
|
37
37
|
function ahash(h) {
|
|
38
38
|
if (typeof h !== 'function' || typeof h.create !== 'function')
|
|
39
39
|
throw new Error('Hash should be wrapped by utils.createHasher');
|
|
40
|
-
anumber(h.outputLen);
|
|
41
|
-
anumber(h.blockLen);
|
|
40
|
+
anumber$1(h.outputLen);
|
|
41
|
+
anumber$1(h.blockLen);
|
|
42
42
|
}
|
|
43
43
|
/** Asserts a hash instance has not been destroyed / finished */
|
|
44
|
-
function aexists(instance, checkFinished = true) {
|
|
44
|
+
function aexists$1(instance, checkFinished = true) {
|
|
45
45
|
if (instance.destroyed)
|
|
46
46
|
throw new Error('Hash instance has been destroyed');
|
|
47
47
|
if (checkFinished && instance.finished)
|
|
48
48
|
throw new Error('Hash#digest() has already been called');
|
|
49
49
|
}
|
|
50
50
|
/** Asserts output is properly-sized byte array */
|
|
51
|
-
function aoutput(out, instance) {
|
|
52
|
-
abytes(out);
|
|
51
|
+
function aoutput$1(out, instance) {
|
|
52
|
+
abytes$1(out);
|
|
53
53
|
const min = instance.outputLen;
|
|
54
54
|
if (out.length < min) {
|
|
55
55
|
throw new Error('digestInto() expects output buffer of length at least ' + min);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
/** Zeroize a byte array. Warning: JS provides no guarantees. */
|
|
59
|
-
function clean(...arrays) {
|
|
59
|
+
function clean$1(...arrays) {
|
|
60
60
|
for (let i = 0; i < arrays.length; i++) {
|
|
61
61
|
arrays[i].fill(0);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
/** Create DataView of an array for easy byte-level manipulation. */
|
|
65
|
-
function createView(arr) {
|
|
65
|
+
function createView$1(arr) {
|
|
66
66
|
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
67
67
|
}
|
|
68
68
|
/** The rotate right (circular right shift) operation for uint32 */
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
* @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123'
|
|
81
81
|
*/
|
|
82
82
|
function bytesToHex(bytes) {
|
|
83
|
-
abytes(bytes);
|
|
83
|
+
abytes$1(bytes);
|
|
84
84
|
// @ts-ignore
|
|
85
85
|
if (hasHexBuiltin)
|
|
86
86
|
return bytes.toHex();
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
* Converts string to bytes using UTF8 encoding.
|
|
133
133
|
* @example utf8ToBytes('abc') // Uint8Array.from([97, 98, 99])
|
|
134
134
|
*/
|
|
135
|
-
function utf8ToBytes(str) {
|
|
135
|
+
function utf8ToBytes$1(str) {
|
|
136
136
|
if (typeof str !== 'string')
|
|
137
137
|
throw new Error('string expected');
|
|
138
138
|
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
|
@@ -142,10 +142,10 @@
|
|
|
142
142
|
* Warning: when Uint8Array is passed, it would NOT get copied.
|
|
143
143
|
* Keep in mind for future mutable operations.
|
|
144
144
|
*/
|
|
145
|
-
function toBytes(data) {
|
|
145
|
+
function toBytes$1(data) {
|
|
146
146
|
if (typeof data === 'string')
|
|
147
|
-
data = utf8ToBytes(data);
|
|
148
|
-
abytes(data);
|
|
147
|
+
data = utf8ToBytes$1(data);
|
|
148
|
+
abytes$1(data);
|
|
149
149
|
return data;
|
|
150
150
|
}
|
|
151
151
|
/** Copies several Uint8Arrays into one. */
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
let sum = 0;
|
|
154
154
|
for (let i = 0; i < arrays.length; i++) {
|
|
155
155
|
const a = arrays[i];
|
|
156
|
-
abytes(a);
|
|
156
|
+
abytes$1(a);
|
|
157
157
|
sum += a.length;
|
|
158
158
|
}
|
|
159
159
|
const res = new Uint8Array(sum);
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
}
|
|
170
170
|
/** Wraps hash function, creating an interface on top of it */
|
|
171
171
|
function createHasher(hashCons) {
|
|
172
|
-
const hashC = (msg) => hashCons().update(toBytes(msg)).digest();
|
|
172
|
+
const hashC = (msg) => hashCons().update(toBytes$1(msg)).digest();
|
|
173
173
|
const tmp = hashCons();
|
|
174
174
|
hashC.outputLen = tmp.outputLen;
|
|
175
175
|
hashC.blockLen = tmp.blockLen;
|
|
@@ -191,22 +191,22 @@
|
|
|
191
191
|
var utils = /*#__PURE__*/Object.freeze({
|
|
192
192
|
__proto__: null,
|
|
193
193
|
Hash: Hash,
|
|
194
|
-
abytes: abytes,
|
|
195
|
-
aexists: aexists,
|
|
194
|
+
abytes: abytes$1,
|
|
195
|
+
aexists: aexists$1,
|
|
196
196
|
ahash: ahash,
|
|
197
|
-
anumber: anumber,
|
|
198
|
-
aoutput: aoutput,
|
|
197
|
+
anumber: anumber$1,
|
|
198
|
+
aoutput: aoutput$1,
|
|
199
199
|
bytesToHex: bytesToHex,
|
|
200
|
-
clean: clean,
|
|
200
|
+
clean: clean$1,
|
|
201
201
|
concatBytes: concatBytes,
|
|
202
202
|
createHasher: createHasher,
|
|
203
|
-
createView: createView,
|
|
203
|
+
createView: createView$1,
|
|
204
204
|
hexToBytes: hexToBytes,
|
|
205
|
-
isBytes: isBytes,
|
|
205
|
+
isBytes: isBytes$1,
|
|
206
206
|
randomBytes: randomBytes,
|
|
207
207
|
rotr: rotr,
|
|
208
|
-
toBytes: toBytes,
|
|
209
|
-
utf8ToBytes: utf8ToBytes
|
|
208
|
+
toBytes: toBytes$1,
|
|
209
|
+
utf8ToBytes: utf8ToBytes$1
|
|
210
210
|
});
|
|
211
211
|
|
|
212
212
|
/**
|
|
@@ -417,7 +417,7 @@
|
|
|
417
417
|
* @module
|
|
418
418
|
*/
|
|
419
419
|
/** Polyfill for Safari 14. https://caniuse.com/mdn-javascript_builtins_dataview_setbiguint64 */
|
|
420
|
-
function setBigUint64(view, byteOffset, value, isLE) {
|
|
420
|
+
function setBigUint64$1(view, byteOffset, value, isLE) {
|
|
421
421
|
if (typeof view.setBigUint64 === 'function')
|
|
422
422
|
return view.setBigUint64(byteOffset, value, isLE);
|
|
423
423
|
const _32n = BigInt(32);
|
|
@@ -453,19 +453,19 @@
|
|
|
453
453
|
this.padOffset = padOffset;
|
|
454
454
|
this.isLE = isLE;
|
|
455
455
|
this.buffer = new Uint8Array(blockLen);
|
|
456
|
-
this.view = createView(this.buffer);
|
|
456
|
+
this.view = createView$1(this.buffer);
|
|
457
457
|
}
|
|
458
458
|
update(data) {
|
|
459
|
-
aexists(this);
|
|
460
|
-
data = toBytes(data);
|
|
461
|
-
abytes(data);
|
|
459
|
+
aexists$1(this);
|
|
460
|
+
data = toBytes$1(data);
|
|
461
|
+
abytes$1(data);
|
|
462
462
|
const { view, buffer, blockLen } = this;
|
|
463
463
|
const len = data.length;
|
|
464
464
|
for (let pos = 0; pos < len;) {
|
|
465
465
|
const take = Math.min(blockLen - this.pos, len - pos);
|
|
466
466
|
// Fast path: we have at least one block in input, cast it to view and process
|
|
467
467
|
if (take === blockLen) {
|
|
468
|
-
const dataView = createView(data);
|
|
468
|
+
const dataView = createView$1(data);
|
|
469
469
|
for (; blockLen <= len - pos; pos += blockLen)
|
|
470
470
|
this.process(dataView, pos);
|
|
471
471
|
continue;
|
|
@@ -483,8 +483,8 @@
|
|
|
483
483
|
return this;
|
|
484
484
|
}
|
|
485
485
|
digestInto(out) {
|
|
486
|
-
aexists(this);
|
|
487
|
-
aoutput(out, this);
|
|
486
|
+
aexists$1(this);
|
|
487
|
+
aoutput$1(out, this);
|
|
488
488
|
this.finished = true;
|
|
489
489
|
// Padding
|
|
490
490
|
// We can avoid allocation of buffer for padding completely if it
|
|
@@ -493,7 +493,7 @@
|
|
|
493
493
|
let { pos } = this;
|
|
494
494
|
// append the bit '1' to the message
|
|
495
495
|
buffer[pos++] = 0b10000000;
|
|
496
|
-
clean(this.buffer.subarray(pos));
|
|
496
|
+
clean$1(this.buffer.subarray(pos));
|
|
497
497
|
// we have less than padOffset left in buffer, so we cannot put length in
|
|
498
498
|
// current block, need process it and pad again
|
|
499
499
|
if (this.padOffset > blockLen - pos) {
|
|
@@ -506,9 +506,9 @@
|
|
|
506
506
|
// Note: sha512 requires length to be 128bit integer, but length in JS will overflow before that
|
|
507
507
|
// You need to write around 2 exabytes (u64_max / 8 / (1024**6)) for this to happen.
|
|
508
508
|
// So we just write lowest 64 bits of that value.
|
|
509
|
-
setBigUint64(view, blockLen - 8, BigInt(this.length * 8), isLE);
|
|
509
|
+
setBigUint64$1(view, blockLen - 8, BigInt(this.length * 8), isLE);
|
|
510
510
|
this.process(view, 0);
|
|
511
|
-
const oview = createView(out);
|
|
511
|
+
const oview = createView$1(out);
|
|
512
512
|
const len = this.outputLen;
|
|
513
513
|
// NOTE: we do division by 4 later, which should be fused in single op with modulo by JIT
|
|
514
514
|
if (len % 4)
|
|
@@ -644,11 +644,11 @@
|
|
|
644
644
|
this.set(A, B, C, D, E, F, G, H);
|
|
645
645
|
}
|
|
646
646
|
roundClean() {
|
|
647
|
-
clean(SHA256_W);
|
|
647
|
+
clean$1(SHA256_W);
|
|
648
648
|
}
|
|
649
649
|
destroy() {
|
|
650
650
|
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
651
|
-
clean(this.buffer);
|
|
651
|
+
clean$1(this.buffer);
|
|
652
652
|
}
|
|
653
653
|
}
|
|
654
654
|
/**
|
|
@@ -670,7 +670,7 @@
|
|
|
670
670
|
this.finished = false;
|
|
671
671
|
this.destroyed = false;
|
|
672
672
|
ahash(hash);
|
|
673
|
-
const key = toBytes(_key);
|
|
673
|
+
const key = toBytes$1(_key);
|
|
674
674
|
this.iHash = hash.create();
|
|
675
675
|
if (typeof this.iHash.update !== 'function')
|
|
676
676
|
throw new Error('Expected instance of class which extends utils.Hash');
|
|
@@ -689,16 +689,16 @@
|
|
|
689
689
|
for (let i = 0; i < pad.length; i++)
|
|
690
690
|
pad[i] ^= 0x36 ^ 0x5c;
|
|
691
691
|
this.oHash.update(pad);
|
|
692
|
-
clean(pad);
|
|
692
|
+
clean$1(pad);
|
|
693
693
|
}
|
|
694
694
|
update(buf) {
|
|
695
|
-
aexists(this);
|
|
695
|
+
aexists$1(this);
|
|
696
696
|
this.iHash.update(buf);
|
|
697
697
|
return this;
|
|
698
698
|
}
|
|
699
699
|
digestInto(out) {
|
|
700
|
-
aexists(this);
|
|
701
|
-
abytes(out, this.outputLen);
|
|
700
|
+
aexists$1(this);
|
|
701
|
+
abytes$1(out, this.outputLen);
|
|
702
702
|
this.finished = true;
|
|
703
703
|
this.iHash.digestInto(out);
|
|
704
704
|
this.oHash.update(out);
|
|
@@ -763,7 +763,7 @@
|
|
|
763
763
|
// tmp name until v2
|
|
764
764
|
/** Asserts something is Uint8Array. */
|
|
765
765
|
function _abytes2(value, length, title = '') {
|
|
766
|
-
const bytes = isBytes(value);
|
|
766
|
+
const bytes = isBytes$1(value);
|
|
767
767
|
const len = value?.length;
|
|
768
768
|
const needsLen = length !== undefined;
|
|
769
769
|
if (!bytes || (needsLen && len !== length)) {
|
|
@@ -789,7 +789,7 @@
|
|
|
789
789
|
return hexToNumber(bytesToHex(bytes));
|
|
790
790
|
}
|
|
791
791
|
function bytesToNumberLE(bytes) {
|
|
792
|
-
abytes(bytes);
|
|
792
|
+
abytes$1(bytes);
|
|
793
793
|
return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse()));
|
|
794
794
|
}
|
|
795
795
|
function numberToBytesBE(n, len) {
|
|
@@ -817,7 +817,7 @@
|
|
|
817
817
|
throw new Error(title + ' must be hex string or Uint8Array, cause: ' + e);
|
|
818
818
|
}
|
|
819
819
|
}
|
|
820
|
-
else if (isBytes(hex)) {
|
|
820
|
+
else if (isBytes$1(hex)) {
|
|
821
821
|
// Uint8Array.from() instead of hash.slice() because node.js Buffer
|
|
822
822
|
// is instance of Uint8Array, and its slice() creates **mutable** copy
|
|
823
823
|
res = Uint8Array.from(hex);
|
|
@@ -1258,7 +1258,7 @@
|
|
|
1258
1258
|
function nLength(n, nBitLength) {
|
|
1259
1259
|
// Bit size, byte size of CURVE.n
|
|
1260
1260
|
if (nBitLength !== undefined)
|
|
1261
|
-
anumber(nBitLength);
|
|
1261
|
+
anumber$1(nBitLength);
|
|
1262
1262
|
const _nBitLength = nBitLength !== undefined ? nBitLength : n.toString(2).length;
|
|
1263
1263
|
const nByteLength = Math.ceil(_nBitLength / 8);
|
|
1264
1264
|
return { nBitLength: _nBitLength, nByteLength };
|
|
@@ -2897,7 +2897,7 @@
|
|
|
2897
2897
|
function tryParsingSig(sg) {
|
|
2898
2898
|
// Try to deduce format
|
|
2899
2899
|
let sig = undefined;
|
|
2900
|
-
const isHex = typeof sg === 'string' || isBytes(sg);
|
|
2900
|
+
const isHex = typeof sg === 'string' || isBytes$1(sg);
|
|
2901
2901
|
const isObj = !isHex &&
|
|
2902
2902
|
sg !== null &&
|
|
2903
2903
|
typeof sg === 'object' &&
|
|
@@ -3140,7 +3140,7 @@
|
|
|
3140
3140
|
function taggedHash(tag, ...messages) {
|
|
3141
3141
|
let tagP = TAGGED_HASH_PREFIXES[tag];
|
|
3142
3142
|
if (tagP === undefined) {
|
|
3143
|
-
const tagH = sha256$1(utf8ToBytes(tag));
|
|
3143
|
+
const tagH = sha256$1(utf8ToBytes$1(tag));
|
|
3144
3144
|
tagP = concatBytes(tagH, tagH);
|
|
3145
3145
|
TAGGED_HASH_PREFIXES[tag] = tagP;
|
|
3146
3146
|
}
|
|
@@ -3414,6 +3414,17 @@
|
|
|
3414
3414
|
* AES-256-CBC encryption with ECDH key agreement and optional GZIP compression.
|
|
3415
3415
|
* Works in both Node.js and browser environments.
|
|
3416
3416
|
*/
|
|
3417
|
+
/**
|
|
3418
|
+
* Get the Web Crypto API (works in both Node.js and browser)
|
|
3419
|
+
*/
|
|
3420
|
+
async function getWebCrypto() {
|
|
3421
|
+
if (typeof globalThis.crypto?.subtle !== 'undefined') {
|
|
3422
|
+
return globalThis.crypto;
|
|
3423
|
+
}
|
|
3424
|
+
// Node.js environment - import webcrypto
|
|
3425
|
+
const nodeCrypto = await import('crypto');
|
|
3426
|
+
return nodeCrypto.webcrypto;
|
|
3427
|
+
}
|
|
3417
3428
|
/** Compression threshold in bytes */
|
|
3418
3429
|
const COMPRESSION_THRESHOLD = 1024;
|
|
3419
3430
|
/** Prefix for compressed messages */
|
|
@@ -3421,7 +3432,7 @@
|
|
|
3421
3432
|
/**
|
|
3422
3433
|
* Convert a Uint8Array to base64 string (browser and Node.js compatible)
|
|
3423
3434
|
*/
|
|
3424
|
-
function toBase64(bytes) {
|
|
3435
|
+
function toBase64$1(bytes) {
|
|
3425
3436
|
if (typeof Buffer !== 'undefined') {
|
|
3426
3437
|
return Buffer.from(bytes).toString('base64');
|
|
3427
3438
|
}
|
|
@@ -3435,7 +3446,7 @@
|
|
|
3435
3446
|
/**
|
|
3436
3447
|
* Convert a base64 string to Uint8Array (browser and Node.js compatible)
|
|
3437
3448
|
*/
|
|
3438
|
-
function fromBase64(base64) {
|
|
3449
|
+
function fromBase64$1(base64) {
|
|
3439
3450
|
if (typeof Buffer !== 'undefined') {
|
|
3440
3451
|
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
3441
3452
|
}
|
|
@@ -3530,7 +3541,7 @@
|
|
|
3530
3541
|
* Import an AES-256-CBC key for encryption/decryption
|
|
3531
3542
|
*/
|
|
3532
3543
|
async function importKey(keyBytes) {
|
|
3533
|
-
const crypto =
|
|
3544
|
+
const crypto = await getWebCrypto();
|
|
3534
3545
|
return crypto.subtle.importKey('raw', toBufferSource(keyBytes), { name: 'AES-CBC' }, false, [
|
|
3535
3546
|
'encrypt',
|
|
3536
3547
|
'decrypt',
|
|
@@ -3540,7 +3551,7 @@
|
|
|
3540
3551
|
* AES-256-CBC encrypt
|
|
3541
3552
|
*/
|
|
3542
3553
|
async function aesEncrypt(plaintext, key, iv) {
|
|
3543
|
-
const crypto =
|
|
3554
|
+
const crypto = await getWebCrypto();
|
|
3544
3555
|
const cryptoKey = await importKey(key);
|
|
3545
3556
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv: toBufferSource(iv) }, cryptoKey, toBufferSource(plaintext));
|
|
3546
3557
|
return new Uint8Array(ciphertext);
|
|
@@ -3549,7 +3560,7 @@
|
|
|
3549
3560
|
* AES-256-CBC decrypt
|
|
3550
3561
|
*/
|
|
3551
3562
|
async function aesDecrypt(ciphertext, key, iv) {
|
|
3552
|
-
const crypto =
|
|
3563
|
+
const crypto = await getWebCrypto();
|
|
3553
3564
|
const cryptoKey = await importKey(key);
|
|
3554
3565
|
const plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: toBufferSource(iv) }, cryptoKey, toBufferSource(ciphertext));
|
|
3555
3566
|
return new Uint8Array(plaintext);
|
|
@@ -3600,7 +3611,7 @@
|
|
|
3600
3611
|
* @param theirPublicKey 32-byte x-only public key
|
|
3601
3612
|
* @returns Encrypted content string
|
|
3602
3613
|
*/
|
|
3603
|
-
async function encrypt(message, myPrivateKey, theirPublicKey) {
|
|
3614
|
+
async function encrypt$1(message, myPrivateKey, theirPublicKey) {
|
|
3604
3615
|
const encoder = new TextEncoder();
|
|
3605
3616
|
let plaintext = encoder.encode(message);
|
|
3606
3617
|
// Check if compression is needed
|
|
@@ -3621,8 +3632,8 @@
|
|
|
3621
3632
|
// Encrypt
|
|
3622
3633
|
const ciphertext = await aesEncrypt(plaintextToEncrypt, sharedSecret, iv);
|
|
3623
3634
|
// Format output
|
|
3624
|
-
const ciphertextBase64 = toBase64(ciphertext);
|
|
3625
|
-
const ivBase64 = toBase64(iv);
|
|
3635
|
+
const ciphertextBase64 = toBase64$1(ciphertext);
|
|
3636
|
+
const ivBase64 = toBase64$1(iv);
|
|
3626
3637
|
if (useCompression) {
|
|
3627
3638
|
return `${COMPRESSION_PREFIX}${ciphertextBase64}?iv=${ivBase64}`;
|
|
3628
3639
|
}
|
|
@@ -3635,10 +3646,10 @@
|
|
|
3635
3646
|
* @param theirPublicKeyHex Hex-encoded public key
|
|
3636
3647
|
* @returns Encrypted content string
|
|
3637
3648
|
*/
|
|
3638
|
-
async function encryptHex(message, myPrivateKeyHex, theirPublicKeyHex) {
|
|
3649
|
+
async function encryptHex$1(message, myPrivateKeyHex, theirPublicKeyHex) {
|
|
3639
3650
|
const myPrivateKey = hexToBytes(myPrivateKeyHex);
|
|
3640
3651
|
const theirPublicKey = hexToBytes(theirPublicKeyHex);
|
|
3641
|
-
return encrypt(message, myPrivateKey, theirPublicKey);
|
|
3652
|
+
return encrypt$1(message, myPrivateKey, theirPublicKey);
|
|
3642
3653
|
}
|
|
3643
3654
|
/**
|
|
3644
3655
|
* Decrypt a NIP-04 encrypted message.
|
|
@@ -3648,7 +3659,7 @@
|
|
|
3648
3659
|
* @param theirPublicKey 32-byte x-only public key
|
|
3649
3660
|
* @returns Decrypted message
|
|
3650
3661
|
*/
|
|
3651
|
-
async function decrypt(encryptedContent, myPrivateKey, theirPublicKey) {
|
|
3662
|
+
async function decrypt$1(encryptedContent, myPrivateKey, theirPublicKey) {
|
|
3652
3663
|
// Check for compression prefix
|
|
3653
3664
|
let content = encryptedContent;
|
|
3654
3665
|
let isCompressed = false;
|
|
@@ -3663,8 +3674,8 @@
|
|
|
3663
3674
|
}
|
|
3664
3675
|
const ciphertextBase64 = parts[0];
|
|
3665
3676
|
const ivBase64 = parts[1];
|
|
3666
|
-
const ciphertext = fromBase64(ciphertextBase64);
|
|
3667
|
-
const iv = fromBase64(ivBase64);
|
|
3677
|
+
const ciphertext = fromBase64$1(ciphertextBase64);
|
|
3678
|
+
const iv = fromBase64$1(ivBase64);
|
|
3668
3679
|
if (iv.length !== 16) {
|
|
3669
3680
|
throw new Error('Invalid IV length');
|
|
3670
3681
|
}
|
|
@@ -3686,20 +3697,1221 @@
|
|
|
3686
3697
|
* @param theirPublicKeyHex Hex-encoded public key
|
|
3687
3698
|
* @returns Decrypted message
|
|
3688
3699
|
*/
|
|
3689
|
-
async function decryptHex(encryptedContent, myPrivateKeyHex, theirPublicKeyHex) {
|
|
3700
|
+
async function decryptHex$1(encryptedContent, myPrivateKeyHex, theirPublicKeyHex) {
|
|
3690
3701
|
const myPrivateKey = hexToBytes(myPrivateKeyHex);
|
|
3691
3702
|
const theirPublicKey = hexToBytes(theirPublicKeyHex);
|
|
3692
|
-
return decrypt(encryptedContent, myPrivateKey, theirPublicKey);
|
|
3703
|
+
return decrypt$1(encryptedContent, myPrivateKey, theirPublicKey);
|
|
3693
3704
|
}
|
|
3694
3705
|
|
|
3695
3706
|
var nip04 = /*#__PURE__*/Object.freeze({
|
|
3696
3707
|
__proto__: null,
|
|
3697
|
-
decrypt: decrypt,
|
|
3698
|
-
decryptHex: decryptHex,
|
|
3708
|
+
decrypt: decrypt$1,
|
|
3709
|
+
decryptHex: decryptHex$1,
|
|
3699
3710
|
deriveSharedSecret: deriveSharedSecret,
|
|
3700
3711
|
deriveSharedSecretHex: deriveSharedSecretHex,
|
|
3712
|
+
encrypt: encrypt$1,
|
|
3713
|
+
encryptHex: encryptHex$1
|
|
3714
|
+
});
|
|
3715
|
+
|
|
3716
|
+
/**
|
|
3717
|
+
* Utilities for hex, bytes, CSPRNG.
|
|
3718
|
+
* @module
|
|
3719
|
+
*/
|
|
3720
|
+
/*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */
|
|
3721
|
+
/** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */
|
|
3722
|
+
function isBytes(a) {
|
|
3723
|
+
return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');
|
|
3724
|
+
}
|
|
3725
|
+
/** Asserts something is boolean. */
|
|
3726
|
+
function abool(b) {
|
|
3727
|
+
if (typeof b !== 'boolean')
|
|
3728
|
+
throw new Error(`boolean expected, not ${b}`);
|
|
3729
|
+
}
|
|
3730
|
+
/** Asserts something is positive integer. */
|
|
3731
|
+
function anumber(n) {
|
|
3732
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
3733
|
+
throw new Error('positive integer expected, got ' + n);
|
|
3734
|
+
}
|
|
3735
|
+
/** Asserts something is Uint8Array. */
|
|
3736
|
+
function abytes(b, ...lengths) {
|
|
3737
|
+
if (!isBytes(b))
|
|
3738
|
+
throw new Error('Uint8Array expected');
|
|
3739
|
+
if (lengths.length > 0 && !lengths.includes(b.length))
|
|
3740
|
+
throw new Error('Uint8Array expected of length ' + lengths + ', got length=' + b.length);
|
|
3741
|
+
}
|
|
3742
|
+
/** Asserts a hash instance has not been destroyed / finished */
|
|
3743
|
+
function aexists(instance, checkFinished = true) {
|
|
3744
|
+
if (instance.destroyed)
|
|
3745
|
+
throw new Error('Hash instance has been destroyed');
|
|
3746
|
+
if (checkFinished && instance.finished)
|
|
3747
|
+
throw new Error('Hash#digest() has already been called');
|
|
3748
|
+
}
|
|
3749
|
+
/** Asserts output is properly-sized byte array */
|
|
3750
|
+
function aoutput(out, instance) {
|
|
3751
|
+
abytes(out);
|
|
3752
|
+
const min = instance.outputLen;
|
|
3753
|
+
if (out.length < min) {
|
|
3754
|
+
throw new Error('digestInto() expects output buffer of length at least ' + min);
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
/** Cast u8 / u16 / u32 to u32. */
|
|
3758
|
+
function u32(arr) {
|
|
3759
|
+
return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4));
|
|
3760
|
+
}
|
|
3761
|
+
/** Zeroize a byte array. Warning: JS provides no guarantees. */
|
|
3762
|
+
function clean(...arrays) {
|
|
3763
|
+
for (let i = 0; i < arrays.length; i++) {
|
|
3764
|
+
arrays[i].fill(0);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
/** Create DataView of an array for easy byte-level manipulation. */
|
|
3768
|
+
function createView(arr) {
|
|
3769
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
3770
|
+
}
|
|
3771
|
+
/** Is current platform little-endian? Most are. Big-Endian platform: IBM */
|
|
3772
|
+
const isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44)();
|
|
3773
|
+
/**
|
|
3774
|
+
* Converts string to bytes using UTF8 encoding.
|
|
3775
|
+
* @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99])
|
|
3776
|
+
*/
|
|
3777
|
+
function utf8ToBytes(str) {
|
|
3778
|
+
if (typeof str !== 'string')
|
|
3779
|
+
throw new Error('string expected');
|
|
3780
|
+
return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Normalizes (non-hex) string or Uint8Array to Uint8Array.
|
|
3784
|
+
* Warning: when Uint8Array is passed, it would NOT get copied.
|
|
3785
|
+
* Keep in mind for future mutable operations.
|
|
3786
|
+
*/
|
|
3787
|
+
function toBytes(data) {
|
|
3788
|
+
if (typeof data === 'string')
|
|
3789
|
+
data = utf8ToBytes(data);
|
|
3790
|
+
else if (isBytes(data))
|
|
3791
|
+
data = copyBytes(data);
|
|
3792
|
+
else
|
|
3793
|
+
throw new Error('Uint8Array expected, got ' + typeof data);
|
|
3794
|
+
return data;
|
|
3795
|
+
}
|
|
3796
|
+
function checkOpts(defaults, opts) {
|
|
3797
|
+
if (opts == null || typeof opts !== 'object')
|
|
3798
|
+
throw new Error('options must be defined');
|
|
3799
|
+
const merged = Object.assign(defaults, opts);
|
|
3800
|
+
return merged;
|
|
3801
|
+
}
|
|
3802
|
+
/** Compares 2 uint8array-s in kinda constant time. */
|
|
3803
|
+
function equalBytes(a, b) {
|
|
3804
|
+
if (a.length !== b.length)
|
|
3805
|
+
return false;
|
|
3806
|
+
let diff = 0;
|
|
3807
|
+
for (let i = 0; i < a.length; i++)
|
|
3808
|
+
diff |= a[i] ^ b[i];
|
|
3809
|
+
return diff === 0;
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* Wraps a cipher: validates args, ensures encrypt() can only be called once.
|
|
3813
|
+
* @__NO_SIDE_EFFECTS__
|
|
3814
|
+
*/
|
|
3815
|
+
const wrapCipher = (params, constructor) => {
|
|
3816
|
+
function wrappedCipher(key, ...args) {
|
|
3817
|
+
// Validate key
|
|
3818
|
+
abytes(key);
|
|
3819
|
+
// Big-Endian hardware is rare. Just in case someone still decides to run ciphers:
|
|
3820
|
+
if (!isLE)
|
|
3821
|
+
throw new Error('Non little-endian hardware is not yet supported');
|
|
3822
|
+
// Validate nonce if nonceLength is present
|
|
3823
|
+
if (params.nonceLength !== undefined) {
|
|
3824
|
+
const nonce = args[0];
|
|
3825
|
+
if (!nonce)
|
|
3826
|
+
throw new Error('nonce / iv required');
|
|
3827
|
+
if (params.varSizeNonce)
|
|
3828
|
+
abytes(nonce);
|
|
3829
|
+
else
|
|
3830
|
+
abytes(nonce, params.nonceLength);
|
|
3831
|
+
}
|
|
3832
|
+
// Validate AAD if tagLength present
|
|
3833
|
+
const tagl = params.tagLength;
|
|
3834
|
+
if (tagl && args[1] !== undefined) {
|
|
3835
|
+
abytes(args[1]);
|
|
3836
|
+
}
|
|
3837
|
+
const cipher = constructor(key, ...args);
|
|
3838
|
+
const checkOutput = (fnLength, output) => {
|
|
3839
|
+
if (output !== undefined) {
|
|
3840
|
+
if (fnLength !== 2)
|
|
3841
|
+
throw new Error('cipher output not supported');
|
|
3842
|
+
abytes(output);
|
|
3843
|
+
}
|
|
3844
|
+
};
|
|
3845
|
+
// Create wrapped cipher with validation and single-use encryption
|
|
3846
|
+
let called = false;
|
|
3847
|
+
const wrCipher = {
|
|
3848
|
+
encrypt(data, output) {
|
|
3849
|
+
if (called)
|
|
3850
|
+
throw new Error('cannot encrypt() twice with same key + nonce');
|
|
3851
|
+
called = true;
|
|
3852
|
+
abytes(data);
|
|
3853
|
+
checkOutput(cipher.encrypt.length, output);
|
|
3854
|
+
return cipher.encrypt(data, output);
|
|
3855
|
+
},
|
|
3856
|
+
decrypt(data, output) {
|
|
3857
|
+
abytes(data);
|
|
3858
|
+
if (tagl && data.length < tagl)
|
|
3859
|
+
throw new Error('invalid ciphertext length: smaller than tagLength=' + tagl);
|
|
3860
|
+
checkOutput(cipher.decrypt.length, output);
|
|
3861
|
+
return cipher.decrypt(data, output);
|
|
3862
|
+
},
|
|
3863
|
+
};
|
|
3864
|
+
return wrCipher;
|
|
3865
|
+
}
|
|
3866
|
+
Object.assign(wrappedCipher, params);
|
|
3867
|
+
return wrappedCipher;
|
|
3868
|
+
};
|
|
3869
|
+
/**
|
|
3870
|
+
* By default, returns u8a of length.
|
|
3871
|
+
* When out is available, it checks it for validity and uses it.
|
|
3872
|
+
*/
|
|
3873
|
+
function getOutput(expectedLength, out, onlyAligned = true) {
|
|
3874
|
+
if (out === undefined)
|
|
3875
|
+
return new Uint8Array(expectedLength);
|
|
3876
|
+
if (out.length !== expectedLength)
|
|
3877
|
+
throw new Error('invalid output length, expected ' + expectedLength + ', got: ' + out.length);
|
|
3878
|
+
if (onlyAligned && !isAligned32$1(out))
|
|
3879
|
+
throw new Error('invalid output, must be aligned');
|
|
3880
|
+
return out;
|
|
3881
|
+
}
|
|
3882
|
+
/** Polyfill for Safari 14. */
|
|
3883
|
+
function setBigUint64(view, byteOffset, value, isLE) {
|
|
3884
|
+
if (typeof view.setBigUint64 === 'function')
|
|
3885
|
+
return view.setBigUint64(byteOffset, value, isLE);
|
|
3886
|
+
const _32n = BigInt(32);
|
|
3887
|
+
const _u32_max = BigInt(0xffffffff);
|
|
3888
|
+
const wh = Number((value >> _32n) & _u32_max);
|
|
3889
|
+
const wl = Number(value & _u32_max);
|
|
3890
|
+
const h = 4 ;
|
|
3891
|
+
const l = 0 ;
|
|
3892
|
+
view.setUint32(byteOffset + h, wh, isLE);
|
|
3893
|
+
view.setUint32(byteOffset + l, wl, isLE);
|
|
3894
|
+
}
|
|
3895
|
+
function u64Lengths(dataLength, aadLength, isLE) {
|
|
3896
|
+
abool(isLE);
|
|
3897
|
+
const num = new Uint8Array(16);
|
|
3898
|
+
const view = createView(num);
|
|
3899
|
+
setBigUint64(view, 0, BigInt(aadLength), isLE);
|
|
3900
|
+
setBigUint64(view, 8, BigInt(dataLength), isLE);
|
|
3901
|
+
return num;
|
|
3902
|
+
}
|
|
3903
|
+
// Is byte array aligned to 4 byte offset (u32)?
|
|
3904
|
+
function isAligned32$1(bytes) {
|
|
3905
|
+
return bytes.byteOffset % 4 === 0;
|
|
3906
|
+
}
|
|
3907
|
+
// copy bytes to new u8a (aligned). Because Buffer.slice is broken.
|
|
3908
|
+
function copyBytes(bytes) {
|
|
3909
|
+
return Uint8Array.from(bytes);
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3912
|
+
/**
|
|
3913
|
+
* Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers.
|
|
3914
|
+
|
|
3915
|
+
RFC8439 requires multi-step cipher stream, where
|
|
3916
|
+
authKey starts with counter: 0, actual msg with counter: 1.
|
|
3917
|
+
|
|
3918
|
+
For this, we need a way to re-use nonce / counter:
|
|
3919
|
+
|
|
3920
|
+
const counter = new Uint8Array(4);
|
|
3921
|
+
chacha(..., counter, ...); // counter is now 1
|
|
3922
|
+
chacha(..., counter, ...); // counter is now 2
|
|
3923
|
+
|
|
3924
|
+
This is complicated:
|
|
3925
|
+
|
|
3926
|
+
- 32-bit counters are enough, no need for 64-bit: max ArrayBuffer size in JS is 4GB
|
|
3927
|
+
- Original papers don't allow mutating counters
|
|
3928
|
+
- Counter overflow is undefined [^1]
|
|
3929
|
+
- Idea A: allow providing (nonce | counter) instead of just nonce, re-use it
|
|
3930
|
+
- Caveat: Cannot be re-used through all cases:
|
|
3931
|
+
- * chacha has (counter | nonce)
|
|
3932
|
+
- * xchacha has (nonce16 | counter | nonce16)
|
|
3933
|
+
- Idea B: separate nonce / counter and provide separate API for counter re-use
|
|
3934
|
+
- Caveat: there are different counter sizes depending on an algorithm.
|
|
3935
|
+
- salsa & chacha also differ in structures of key & sigma:
|
|
3936
|
+
salsa20: s[0] | k(4) | s[1] | nonce(2) | ctr(2) | s[2] | k(4) | s[3]
|
|
3937
|
+
chacha: s(4) | k(8) | ctr(1) | nonce(3)
|
|
3938
|
+
chacha20orig: s(4) | k(8) | ctr(2) | nonce(2)
|
|
3939
|
+
- Idea C: helper method such as `setSalsaState(key, nonce, sigma, data)`
|
|
3940
|
+
- Caveat: we can't re-use counter array
|
|
3941
|
+
|
|
3942
|
+
xchacha [^2] uses the subkey and remaining 8 byte nonce with ChaCha20 as normal
|
|
3943
|
+
(prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce).
|
|
3944
|
+
|
|
3945
|
+
[^1]: https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/
|
|
3946
|
+
[^2]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2
|
|
3947
|
+
|
|
3948
|
+
* @module
|
|
3949
|
+
*/
|
|
3950
|
+
// prettier-ignore
|
|
3951
|
+
// We can't make top-level var depend on utils.utf8ToBytes
|
|
3952
|
+
// because it's not present in all envs. Creating a similar fn here
|
|
3953
|
+
const _utf8ToBytes = (str) => Uint8Array.from(str.split('').map((c) => c.charCodeAt(0)));
|
|
3954
|
+
const sigma16 = _utf8ToBytes('expand 16-byte k');
|
|
3955
|
+
const sigma32 = _utf8ToBytes('expand 32-byte k');
|
|
3956
|
+
const sigma16_32 = u32(sigma16);
|
|
3957
|
+
const sigma32_32 = u32(sigma32);
|
|
3958
|
+
function rotl(a, b) {
|
|
3959
|
+
return (a << b) | (a >>> (32 - b));
|
|
3960
|
+
}
|
|
3961
|
+
// Is byte array aligned to 4 byte offset (u32)?
|
|
3962
|
+
function isAligned32(b) {
|
|
3963
|
+
return b.byteOffset % 4 === 0;
|
|
3964
|
+
}
|
|
3965
|
+
// Salsa and Chacha block length is always 512-bit
|
|
3966
|
+
const BLOCK_LEN = 64;
|
|
3967
|
+
const BLOCK_LEN32 = 16;
|
|
3968
|
+
// new Uint32Array([2**32]) // => Uint32Array(1) [ 0 ]
|
|
3969
|
+
// new Uint32Array([2**32-1]) // => Uint32Array(1) [ 4294967295 ]
|
|
3970
|
+
const MAX_COUNTER = 2 ** 32 - 1;
|
|
3971
|
+
const U32_EMPTY = new Uint32Array();
|
|
3972
|
+
function runCipher(core, sigma, key, nonce, data, output, counter, rounds) {
|
|
3973
|
+
const len = data.length;
|
|
3974
|
+
const block = new Uint8Array(BLOCK_LEN);
|
|
3975
|
+
const b32 = u32(block);
|
|
3976
|
+
// Make sure that buffers aligned to 4 bytes
|
|
3977
|
+
const isAligned = isAligned32(data) && isAligned32(output);
|
|
3978
|
+
const d32 = isAligned ? u32(data) : U32_EMPTY;
|
|
3979
|
+
const o32 = isAligned ? u32(output) : U32_EMPTY;
|
|
3980
|
+
for (let pos = 0; pos < len; counter++) {
|
|
3981
|
+
core(sigma, key, nonce, b32, counter, rounds);
|
|
3982
|
+
if (counter >= MAX_COUNTER)
|
|
3983
|
+
throw new Error('arx: counter overflow');
|
|
3984
|
+
const take = Math.min(BLOCK_LEN, len - pos);
|
|
3985
|
+
// aligned to 4 bytes
|
|
3986
|
+
if (isAligned && take === BLOCK_LEN) {
|
|
3987
|
+
const pos32 = pos / 4;
|
|
3988
|
+
if (pos % 4 !== 0)
|
|
3989
|
+
throw new Error('arx: invalid block position');
|
|
3990
|
+
for (let j = 0, posj; j < BLOCK_LEN32; j++) {
|
|
3991
|
+
posj = pos32 + j;
|
|
3992
|
+
o32[posj] = d32[posj] ^ b32[j];
|
|
3993
|
+
}
|
|
3994
|
+
pos += BLOCK_LEN;
|
|
3995
|
+
continue;
|
|
3996
|
+
}
|
|
3997
|
+
for (let j = 0, posj; j < take; j++) {
|
|
3998
|
+
posj = pos + j;
|
|
3999
|
+
output[posj] = data[posj] ^ block[j];
|
|
4000
|
+
}
|
|
4001
|
+
pos += take;
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
/** Creates ARX-like (ChaCha, Salsa) cipher stream from core function. */
|
|
4005
|
+
function createCipher(core, opts) {
|
|
4006
|
+
const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts);
|
|
4007
|
+
if (typeof core !== 'function')
|
|
4008
|
+
throw new Error('core must be a function');
|
|
4009
|
+
anumber(counterLength);
|
|
4010
|
+
anumber(rounds);
|
|
4011
|
+
abool(counterRight);
|
|
4012
|
+
abool(allowShortKeys);
|
|
4013
|
+
return (key, nonce, data, output, counter = 0) => {
|
|
4014
|
+
abytes(key);
|
|
4015
|
+
abytes(nonce);
|
|
4016
|
+
abytes(data);
|
|
4017
|
+
const len = data.length;
|
|
4018
|
+
if (output === undefined)
|
|
4019
|
+
output = new Uint8Array(len);
|
|
4020
|
+
abytes(output);
|
|
4021
|
+
anumber(counter);
|
|
4022
|
+
if (counter < 0 || counter >= MAX_COUNTER)
|
|
4023
|
+
throw new Error('arx: counter overflow');
|
|
4024
|
+
if (output.length < len)
|
|
4025
|
+
throw new Error(`arx: output (${output.length}) is shorter than data (${len})`);
|
|
4026
|
+
const toClean = [];
|
|
4027
|
+
// Key & sigma
|
|
4028
|
+
// key=16 -> sigma16, k=key|key
|
|
4029
|
+
// key=32 -> sigma32, k=key
|
|
4030
|
+
let l = key.length;
|
|
4031
|
+
let k;
|
|
4032
|
+
let sigma;
|
|
4033
|
+
if (l === 32) {
|
|
4034
|
+
toClean.push((k = copyBytes(key)));
|
|
4035
|
+
sigma = sigma32_32;
|
|
4036
|
+
}
|
|
4037
|
+
else if (l === 16 && allowShortKeys) {
|
|
4038
|
+
k = new Uint8Array(32);
|
|
4039
|
+
k.set(key);
|
|
4040
|
+
k.set(key, 16);
|
|
4041
|
+
sigma = sigma16_32;
|
|
4042
|
+
toClean.push(k);
|
|
4043
|
+
}
|
|
4044
|
+
else {
|
|
4045
|
+
throw new Error(`arx: invalid 32-byte key, got length=${l}`);
|
|
4046
|
+
}
|
|
4047
|
+
// Nonce
|
|
4048
|
+
// salsa20: 8 (8-byte counter)
|
|
4049
|
+
// chacha20orig: 8 (8-byte counter)
|
|
4050
|
+
// chacha20: 12 (4-byte counter)
|
|
4051
|
+
// xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce)
|
|
4052
|
+
// xchacha20: 24 (16 -> hchacha, 8 -> old nonce)
|
|
4053
|
+
// Align nonce to 4 bytes
|
|
4054
|
+
if (!isAligned32(nonce))
|
|
4055
|
+
toClean.push((nonce = copyBytes(nonce)));
|
|
4056
|
+
const k32 = u32(k);
|
|
4057
|
+
// hsalsa & hchacha: handle extended nonce
|
|
4058
|
+
if (extendNonceFn) {
|
|
4059
|
+
if (nonce.length !== 24)
|
|
4060
|
+
throw new Error(`arx: extended nonce must be 24 bytes`);
|
|
4061
|
+
extendNonceFn(sigma, k32, u32(nonce.subarray(0, 16)), k32);
|
|
4062
|
+
nonce = nonce.subarray(16);
|
|
4063
|
+
}
|
|
4064
|
+
// Handle nonce counter
|
|
4065
|
+
const nonceNcLen = 16 - counterLength;
|
|
4066
|
+
if (nonceNcLen !== nonce.length)
|
|
4067
|
+
throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`);
|
|
4068
|
+
// Pad counter when nonce is 64 bit
|
|
4069
|
+
if (nonceNcLen !== 12) {
|
|
4070
|
+
const nc = new Uint8Array(12);
|
|
4071
|
+
nc.set(nonce, counterRight ? 0 : 12 - nonce.length);
|
|
4072
|
+
nonce = nc;
|
|
4073
|
+
toClean.push(nonce);
|
|
4074
|
+
}
|
|
4075
|
+
const n32 = u32(nonce);
|
|
4076
|
+
runCipher(core, sigma, k32, n32, data, output, counter, rounds);
|
|
4077
|
+
clean(...toClean);
|
|
4078
|
+
return output;
|
|
4079
|
+
};
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
/**
|
|
4083
|
+
* Poly1305 ([PDF](https://cr.yp.to/mac/poly1305-20050329.pdf),
|
|
4084
|
+
* [wiki](https://en.wikipedia.org/wiki/Poly1305))
|
|
4085
|
+
* is a fast and parallel secret-key message-authentication code suitable for
|
|
4086
|
+
* a wide variety of applications. It was standardized in
|
|
4087
|
+
* [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) and is now used in TLS 1.3.
|
|
4088
|
+
*
|
|
4089
|
+
* Polynomial MACs are not perfect for every situation:
|
|
4090
|
+
* they lack Random Key Robustness: the MAC can be forged, and can't be used in PAKE schemes.
|
|
4091
|
+
* See [invisible salamanders attack](https://keymaterial.net/2020/09/07/invisible-salamanders-in-aes-gcm-siv/).
|
|
4092
|
+
* To combat invisible salamanders, `hash(key)` can be included in ciphertext,
|
|
4093
|
+
* however, this would violate ciphertext indistinguishability:
|
|
4094
|
+
* an attacker would know which key was used - so `HKDF(key, i)`
|
|
4095
|
+
* could be used instead.
|
|
4096
|
+
*
|
|
4097
|
+
* Check out [original website](https://cr.yp.to/mac.html).
|
|
4098
|
+
* @module
|
|
4099
|
+
*/
|
|
4100
|
+
// Based on Public Domain poly1305-donna https://github.com/floodyberry/poly1305-donna
|
|
4101
|
+
const u8to16 = (a, i) => (a[i++] & 0xff) | ((a[i++] & 0xff) << 8);
|
|
4102
|
+
class Poly1305 {
|
|
4103
|
+
constructor(key) {
|
|
4104
|
+
this.blockLen = 16;
|
|
4105
|
+
this.outputLen = 16;
|
|
4106
|
+
this.buffer = new Uint8Array(16);
|
|
4107
|
+
this.r = new Uint16Array(10);
|
|
4108
|
+
this.h = new Uint16Array(10);
|
|
4109
|
+
this.pad = new Uint16Array(8);
|
|
4110
|
+
this.pos = 0;
|
|
4111
|
+
this.finished = false;
|
|
4112
|
+
key = toBytes(key);
|
|
4113
|
+
abytes(key, 32);
|
|
4114
|
+
const t0 = u8to16(key, 0);
|
|
4115
|
+
const t1 = u8to16(key, 2);
|
|
4116
|
+
const t2 = u8to16(key, 4);
|
|
4117
|
+
const t3 = u8to16(key, 6);
|
|
4118
|
+
const t4 = u8to16(key, 8);
|
|
4119
|
+
const t5 = u8to16(key, 10);
|
|
4120
|
+
const t6 = u8to16(key, 12);
|
|
4121
|
+
const t7 = u8to16(key, 14);
|
|
4122
|
+
// https://github.com/floodyberry/poly1305-donna/blob/e6ad6e091d30d7f4ec2d4f978be1fcfcbce72781/poly1305-donna-16.h#L47
|
|
4123
|
+
this.r[0] = t0 & 0x1fff;
|
|
4124
|
+
this.r[1] = ((t0 >>> 13) | (t1 << 3)) & 0x1fff;
|
|
4125
|
+
this.r[2] = ((t1 >>> 10) | (t2 << 6)) & 0x1f03;
|
|
4126
|
+
this.r[3] = ((t2 >>> 7) | (t3 << 9)) & 0x1fff;
|
|
4127
|
+
this.r[4] = ((t3 >>> 4) | (t4 << 12)) & 0x00ff;
|
|
4128
|
+
this.r[5] = (t4 >>> 1) & 0x1ffe;
|
|
4129
|
+
this.r[6] = ((t4 >>> 14) | (t5 << 2)) & 0x1fff;
|
|
4130
|
+
this.r[7] = ((t5 >>> 11) | (t6 << 5)) & 0x1f81;
|
|
4131
|
+
this.r[8] = ((t6 >>> 8) | (t7 << 8)) & 0x1fff;
|
|
4132
|
+
this.r[9] = (t7 >>> 5) & 0x007f;
|
|
4133
|
+
for (let i = 0; i < 8; i++)
|
|
4134
|
+
this.pad[i] = u8to16(key, 16 + 2 * i);
|
|
4135
|
+
}
|
|
4136
|
+
process(data, offset, isLast = false) {
|
|
4137
|
+
const hibit = isLast ? 0 : 1 << 11;
|
|
4138
|
+
const { h, r } = this;
|
|
4139
|
+
const r0 = r[0];
|
|
4140
|
+
const r1 = r[1];
|
|
4141
|
+
const r2 = r[2];
|
|
4142
|
+
const r3 = r[3];
|
|
4143
|
+
const r4 = r[4];
|
|
4144
|
+
const r5 = r[5];
|
|
4145
|
+
const r6 = r[6];
|
|
4146
|
+
const r7 = r[7];
|
|
4147
|
+
const r8 = r[8];
|
|
4148
|
+
const r9 = r[9];
|
|
4149
|
+
const t0 = u8to16(data, offset + 0);
|
|
4150
|
+
const t1 = u8to16(data, offset + 2);
|
|
4151
|
+
const t2 = u8to16(data, offset + 4);
|
|
4152
|
+
const t3 = u8to16(data, offset + 6);
|
|
4153
|
+
const t4 = u8to16(data, offset + 8);
|
|
4154
|
+
const t5 = u8to16(data, offset + 10);
|
|
4155
|
+
const t6 = u8to16(data, offset + 12);
|
|
4156
|
+
const t7 = u8to16(data, offset + 14);
|
|
4157
|
+
let h0 = h[0] + (t0 & 0x1fff);
|
|
4158
|
+
let h1 = h[1] + (((t0 >>> 13) | (t1 << 3)) & 0x1fff);
|
|
4159
|
+
let h2 = h[2] + (((t1 >>> 10) | (t2 << 6)) & 0x1fff);
|
|
4160
|
+
let h3 = h[3] + (((t2 >>> 7) | (t3 << 9)) & 0x1fff);
|
|
4161
|
+
let h4 = h[4] + (((t3 >>> 4) | (t4 << 12)) & 0x1fff);
|
|
4162
|
+
let h5 = h[5] + ((t4 >>> 1) & 0x1fff);
|
|
4163
|
+
let h6 = h[6] + (((t4 >>> 14) | (t5 << 2)) & 0x1fff);
|
|
4164
|
+
let h7 = h[7] + (((t5 >>> 11) | (t6 << 5)) & 0x1fff);
|
|
4165
|
+
let h8 = h[8] + (((t6 >>> 8) | (t7 << 8)) & 0x1fff);
|
|
4166
|
+
let h9 = h[9] + ((t7 >>> 5) | hibit);
|
|
4167
|
+
let c = 0;
|
|
4168
|
+
let d0 = c + h0 * r0 + h1 * (5 * r9) + h2 * (5 * r8) + h3 * (5 * r7) + h4 * (5 * r6);
|
|
4169
|
+
c = d0 >>> 13;
|
|
4170
|
+
d0 &= 0x1fff;
|
|
4171
|
+
d0 += h5 * (5 * r5) + h6 * (5 * r4) + h7 * (5 * r3) + h8 * (5 * r2) + h9 * (5 * r1);
|
|
4172
|
+
c += d0 >>> 13;
|
|
4173
|
+
d0 &= 0x1fff;
|
|
4174
|
+
let d1 = c + h0 * r1 + h1 * r0 + h2 * (5 * r9) + h3 * (5 * r8) + h4 * (5 * r7);
|
|
4175
|
+
c = d1 >>> 13;
|
|
4176
|
+
d1 &= 0x1fff;
|
|
4177
|
+
d1 += h5 * (5 * r6) + h6 * (5 * r5) + h7 * (5 * r4) + h8 * (5 * r3) + h9 * (5 * r2);
|
|
4178
|
+
c += d1 >>> 13;
|
|
4179
|
+
d1 &= 0x1fff;
|
|
4180
|
+
let d2 = c + h0 * r2 + h1 * r1 + h2 * r0 + h3 * (5 * r9) + h4 * (5 * r8);
|
|
4181
|
+
c = d2 >>> 13;
|
|
4182
|
+
d2 &= 0x1fff;
|
|
4183
|
+
d2 += h5 * (5 * r7) + h6 * (5 * r6) + h7 * (5 * r5) + h8 * (5 * r4) + h9 * (5 * r3);
|
|
4184
|
+
c += d2 >>> 13;
|
|
4185
|
+
d2 &= 0x1fff;
|
|
4186
|
+
let d3 = c + h0 * r3 + h1 * r2 + h2 * r1 + h3 * r0 + h4 * (5 * r9);
|
|
4187
|
+
c = d3 >>> 13;
|
|
4188
|
+
d3 &= 0x1fff;
|
|
4189
|
+
d3 += h5 * (5 * r8) + h6 * (5 * r7) + h7 * (5 * r6) + h8 * (5 * r5) + h9 * (5 * r4);
|
|
4190
|
+
c += d3 >>> 13;
|
|
4191
|
+
d3 &= 0x1fff;
|
|
4192
|
+
let d4 = c + h0 * r4 + h1 * r3 + h2 * r2 + h3 * r1 + h4 * r0;
|
|
4193
|
+
c = d4 >>> 13;
|
|
4194
|
+
d4 &= 0x1fff;
|
|
4195
|
+
d4 += h5 * (5 * r9) + h6 * (5 * r8) + h7 * (5 * r7) + h8 * (5 * r6) + h9 * (5 * r5);
|
|
4196
|
+
c += d4 >>> 13;
|
|
4197
|
+
d4 &= 0x1fff;
|
|
4198
|
+
let d5 = c + h0 * r5 + h1 * r4 + h2 * r3 + h3 * r2 + h4 * r1;
|
|
4199
|
+
c = d5 >>> 13;
|
|
4200
|
+
d5 &= 0x1fff;
|
|
4201
|
+
d5 += h5 * r0 + h6 * (5 * r9) + h7 * (5 * r8) + h8 * (5 * r7) + h9 * (5 * r6);
|
|
4202
|
+
c += d5 >>> 13;
|
|
4203
|
+
d5 &= 0x1fff;
|
|
4204
|
+
let d6 = c + h0 * r6 + h1 * r5 + h2 * r4 + h3 * r3 + h4 * r2;
|
|
4205
|
+
c = d6 >>> 13;
|
|
4206
|
+
d6 &= 0x1fff;
|
|
4207
|
+
d6 += h5 * r1 + h6 * r0 + h7 * (5 * r9) + h8 * (5 * r8) + h9 * (5 * r7);
|
|
4208
|
+
c += d6 >>> 13;
|
|
4209
|
+
d6 &= 0x1fff;
|
|
4210
|
+
let d7 = c + h0 * r7 + h1 * r6 + h2 * r5 + h3 * r4 + h4 * r3;
|
|
4211
|
+
c = d7 >>> 13;
|
|
4212
|
+
d7 &= 0x1fff;
|
|
4213
|
+
d7 += h5 * r2 + h6 * r1 + h7 * r0 + h8 * (5 * r9) + h9 * (5 * r8);
|
|
4214
|
+
c += d7 >>> 13;
|
|
4215
|
+
d7 &= 0x1fff;
|
|
4216
|
+
let d8 = c + h0 * r8 + h1 * r7 + h2 * r6 + h3 * r5 + h4 * r4;
|
|
4217
|
+
c = d8 >>> 13;
|
|
4218
|
+
d8 &= 0x1fff;
|
|
4219
|
+
d8 += h5 * r3 + h6 * r2 + h7 * r1 + h8 * r0 + h9 * (5 * r9);
|
|
4220
|
+
c += d8 >>> 13;
|
|
4221
|
+
d8 &= 0x1fff;
|
|
4222
|
+
let d9 = c + h0 * r9 + h1 * r8 + h2 * r7 + h3 * r6 + h4 * r5;
|
|
4223
|
+
c = d9 >>> 13;
|
|
4224
|
+
d9 &= 0x1fff;
|
|
4225
|
+
d9 += h5 * r4 + h6 * r3 + h7 * r2 + h8 * r1 + h9 * r0;
|
|
4226
|
+
c += d9 >>> 13;
|
|
4227
|
+
d9 &= 0x1fff;
|
|
4228
|
+
c = ((c << 2) + c) | 0;
|
|
4229
|
+
c = (c + d0) | 0;
|
|
4230
|
+
d0 = c & 0x1fff;
|
|
4231
|
+
c = c >>> 13;
|
|
4232
|
+
d1 += c;
|
|
4233
|
+
h[0] = d0;
|
|
4234
|
+
h[1] = d1;
|
|
4235
|
+
h[2] = d2;
|
|
4236
|
+
h[3] = d3;
|
|
4237
|
+
h[4] = d4;
|
|
4238
|
+
h[5] = d5;
|
|
4239
|
+
h[6] = d6;
|
|
4240
|
+
h[7] = d7;
|
|
4241
|
+
h[8] = d8;
|
|
4242
|
+
h[9] = d9;
|
|
4243
|
+
}
|
|
4244
|
+
finalize() {
|
|
4245
|
+
const { h, pad } = this;
|
|
4246
|
+
const g = new Uint16Array(10);
|
|
4247
|
+
let c = h[1] >>> 13;
|
|
4248
|
+
h[1] &= 0x1fff;
|
|
4249
|
+
for (let i = 2; i < 10; i++) {
|
|
4250
|
+
h[i] += c;
|
|
4251
|
+
c = h[i] >>> 13;
|
|
4252
|
+
h[i] &= 0x1fff;
|
|
4253
|
+
}
|
|
4254
|
+
h[0] += c * 5;
|
|
4255
|
+
c = h[0] >>> 13;
|
|
4256
|
+
h[0] &= 0x1fff;
|
|
4257
|
+
h[1] += c;
|
|
4258
|
+
c = h[1] >>> 13;
|
|
4259
|
+
h[1] &= 0x1fff;
|
|
4260
|
+
h[2] += c;
|
|
4261
|
+
g[0] = h[0] + 5;
|
|
4262
|
+
c = g[0] >>> 13;
|
|
4263
|
+
g[0] &= 0x1fff;
|
|
4264
|
+
for (let i = 1; i < 10; i++) {
|
|
4265
|
+
g[i] = h[i] + c;
|
|
4266
|
+
c = g[i] >>> 13;
|
|
4267
|
+
g[i] &= 0x1fff;
|
|
4268
|
+
}
|
|
4269
|
+
g[9] -= 1 << 13;
|
|
4270
|
+
let mask = (c ^ 1) - 1;
|
|
4271
|
+
for (let i = 0; i < 10; i++)
|
|
4272
|
+
g[i] &= mask;
|
|
4273
|
+
mask = ~mask;
|
|
4274
|
+
for (let i = 0; i < 10; i++)
|
|
4275
|
+
h[i] = (h[i] & mask) | g[i];
|
|
4276
|
+
h[0] = (h[0] | (h[1] << 13)) & 0xffff;
|
|
4277
|
+
h[1] = ((h[1] >>> 3) | (h[2] << 10)) & 0xffff;
|
|
4278
|
+
h[2] = ((h[2] >>> 6) | (h[3] << 7)) & 0xffff;
|
|
4279
|
+
h[3] = ((h[3] >>> 9) | (h[4] << 4)) & 0xffff;
|
|
4280
|
+
h[4] = ((h[4] >>> 12) | (h[5] << 1) | (h[6] << 14)) & 0xffff;
|
|
4281
|
+
h[5] = ((h[6] >>> 2) | (h[7] << 11)) & 0xffff;
|
|
4282
|
+
h[6] = ((h[7] >>> 5) | (h[8] << 8)) & 0xffff;
|
|
4283
|
+
h[7] = ((h[8] >>> 8) | (h[9] << 5)) & 0xffff;
|
|
4284
|
+
let f = h[0] + pad[0];
|
|
4285
|
+
h[0] = f & 0xffff;
|
|
4286
|
+
for (let i = 1; i < 8; i++) {
|
|
4287
|
+
f = (((h[i] + pad[i]) | 0) + (f >>> 16)) | 0;
|
|
4288
|
+
h[i] = f & 0xffff;
|
|
4289
|
+
}
|
|
4290
|
+
clean(g);
|
|
4291
|
+
}
|
|
4292
|
+
update(data) {
|
|
4293
|
+
aexists(this);
|
|
4294
|
+
data = toBytes(data);
|
|
4295
|
+
abytes(data);
|
|
4296
|
+
const { buffer, blockLen } = this;
|
|
4297
|
+
const len = data.length;
|
|
4298
|
+
for (let pos = 0; pos < len;) {
|
|
4299
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
4300
|
+
// Fast path: we have at least one block in input
|
|
4301
|
+
if (take === blockLen) {
|
|
4302
|
+
for (; blockLen <= len - pos; pos += blockLen)
|
|
4303
|
+
this.process(data, pos);
|
|
4304
|
+
continue;
|
|
4305
|
+
}
|
|
4306
|
+
buffer.set(data.subarray(pos, pos + take), this.pos);
|
|
4307
|
+
this.pos += take;
|
|
4308
|
+
pos += take;
|
|
4309
|
+
if (this.pos === blockLen) {
|
|
4310
|
+
this.process(buffer, 0, false);
|
|
4311
|
+
this.pos = 0;
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
return this;
|
|
4315
|
+
}
|
|
4316
|
+
destroy() {
|
|
4317
|
+
clean(this.h, this.r, this.buffer, this.pad);
|
|
4318
|
+
}
|
|
4319
|
+
digestInto(out) {
|
|
4320
|
+
aexists(this);
|
|
4321
|
+
aoutput(out, this);
|
|
4322
|
+
this.finished = true;
|
|
4323
|
+
const { buffer, h } = this;
|
|
4324
|
+
let { pos } = this;
|
|
4325
|
+
if (pos) {
|
|
4326
|
+
buffer[pos++] = 1;
|
|
4327
|
+
for (; pos < 16; pos++)
|
|
4328
|
+
buffer[pos] = 0;
|
|
4329
|
+
this.process(buffer, 0, true);
|
|
4330
|
+
}
|
|
4331
|
+
this.finalize();
|
|
4332
|
+
let opos = 0;
|
|
4333
|
+
for (let i = 0; i < 8; i++) {
|
|
4334
|
+
out[opos++] = h[i] >>> 0;
|
|
4335
|
+
out[opos++] = h[i] >>> 8;
|
|
4336
|
+
}
|
|
4337
|
+
return out;
|
|
4338
|
+
}
|
|
4339
|
+
digest() {
|
|
4340
|
+
const { buffer, outputLen } = this;
|
|
4341
|
+
this.digestInto(buffer);
|
|
4342
|
+
const res = buffer.slice(0, outputLen);
|
|
4343
|
+
this.destroy();
|
|
4344
|
+
return res;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
function wrapConstructorWithKey(hashCons) {
|
|
4348
|
+
const hashC = (msg, key) => hashCons(key).update(toBytes(msg)).digest();
|
|
4349
|
+
const tmp = hashCons(new Uint8Array(32));
|
|
4350
|
+
hashC.outputLen = tmp.outputLen;
|
|
4351
|
+
hashC.blockLen = tmp.blockLen;
|
|
4352
|
+
hashC.create = (key) => hashCons(key);
|
|
4353
|
+
return hashC;
|
|
4354
|
+
}
|
|
4355
|
+
/** Poly1305 MAC from RFC 8439. */
|
|
4356
|
+
const poly1305 = wrapConstructorWithKey((key) => new Poly1305(key));
|
|
4357
|
+
|
|
4358
|
+
/**
|
|
4359
|
+
* [ChaCha20](https://cr.yp.to/chacha.html) stream cipher, released
|
|
4360
|
+
* in 2008. Developed after Salsa20, ChaCha aims to increase diffusion per round.
|
|
4361
|
+
* It was standardized in [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) and
|
|
4362
|
+
* is now used in TLS 1.3.
|
|
4363
|
+
*
|
|
4364
|
+
* [XChaCha20](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha)
|
|
4365
|
+
* extended-nonce variant is also provided. Similar to XSalsa, it's safe to use with
|
|
4366
|
+
* randomly-generated nonces.
|
|
4367
|
+
*
|
|
4368
|
+
* Check out [PDF](http://cr.yp.to/chacha/chacha-20080128.pdf) and
|
|
4369
|
+
* [wiki](https://en.wikipedia.org/wiki/Salsa20).
|
|
4370
|
+
* @module
|
|
4371
|
+
*/
|
|
4372
|
+
/**
|
|
4373
|
+
* ChaCha core function.
|
|
4374
|
+
*/
|
|
4375
|
+
// prettier-ignore
|
|
4376
|
+
function chachaCore(s, k, n, out, cnt, rounds = 20) {
|
|
4377
|
+
let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], // "expa" "nd 3" "2-by" "te k"
|
|
4378
|
+
y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], // Key Key Key Key
|
|
4379
|
+
y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], // Key Key Key Key
|
|
4380
|
+
y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce
|
|
4381
|
+
// Save state to temporary variables
|
|
4382
|
+
let x00 = y00, x01 = y01, x02 = y02, x03 = y03, x04 = y04, x05 = y05, x06 = y06, x07 = y07, x08 = y08, x09 = y09, x10 = y10, x11 = y11, x12 = y12, x13 = y13, x14 = y14, x15 = y15;
|
|
4383
|
+
for (let r = 0; r < rounds; r += 2) {
|
|
4384
|
+
x00 = (x00 + x04) | 0;
|
|
4385
|
+
x12 = rotl(x12 ^ x00, 16);
|
|
4386
|
+
x08 = (x08 + x12) | 0;
|
|
4387
|
+
x04 = rotl(x04 ^ x08, 12);
|
|
4388
|
+
x00 = (x00 + x04) | 0;
|
|
4389
|
+
x12 = rotl(x12 ^ x00, 8);
|
|
4390
|
+
x08 = (x08 + x12) | 0;
|
|
4391
|
+
x04 = rotl(x04 ^ x08, 7);
|
|
4392
|
+
x01 = (x01 + x05) | 0;
|
|
4393
|
+
x13 = rotl(x13 ^ x01, 16);
|
|
4394
|
+
x09 = (x09 + x13) | 0;
|
|
4395
|
+
x05 = rotl(x05 ^ x09, 12);
|
|
4396
|
+
x01 = (x01 + x05) | 0;
|
|
4397
|
+
x13 = rotl(x13 ^ x01, 8);
|
|
4398
|
+
x09 = (x09 + x13) | 0;
|
|
4399
|
+
x05 = rotl(x05 ^ x09, 7);
|
|
4400
|
+
x02 = (x02 + x06) | 0;
|
|
4401
|
+
x14 = rotl(x14 ^ x02, 16);
|
|
4402
|
+
x10 = (x10 + x14) | 0;
|
|
4403
|
+
x06 = rotl(x06 ^ x10, 12);
|
|
4404
|
+
x02 = (x02 + x06) | 0;
|
|
4405
|
+
x14 = rotl(x14 ^ x02, 8);
|
|
4406
|
+
x10 = (x10 + x14) | 0;
|
|
4407
|
+
x06 = rotl(x06 ^ x10, 7);
|
|
4408
|
+
x03 = (x03 + x07) | 0;
|
|
4409
|
+
x15 = rotl(x15 ^ x03, 16);
|
|
4410
|
+
x11 = (x11 + x15) | 0;
|
|
4411
|
+
x07 = rotl(x07 ^ x11, 12);
|
|
4412
|
+
x03 = (x03 + x07) | 0;
|
|
4413
|
+
x15 = rotl(x15 ^ x03, 8);
|
|
4414
|
+
x11 = (x11 + x15) | 0;
|
|
4415
|
+
x07 = rotl(x07 ^ x11, 7);
|
|
4416
|
+
x00 = (x00 + x05) | 0;
|
|
4417
|
+
x15 = rotl(x15 ^ x00, 16);
|
|
4418
|
+
x10 = (x10 + x15) | 0;
|
|
4419
|
+
x05 = rotl(x05 ^ x10, 12);
|
|
4420
|
+
x00 = (x00 + x05) | 0;
|
|
4421
|
+
x15 = rotl(x15 ^ x00, 8);
|
|
4422
|
+
x10 = (x10 + x15) | 0;
|
|
4423
|
+
x05 = rotl(x05 ^ x10, 7);
|
|
4424
|
+
x01 = (x01 + x06) | 0;
|
|
4425
|
+
x12 = rotl(x12 ^ x01, 16);
|
|
4426
|
+
x11 = (x11 + x12) | 0;
|
|
4427
|
+
x06 = rotl(x06 ^ x11, 12);
|
|
4428
|
+
x01 = (x01 + x06) | 0;
|
|
4429
|
+
x12 = rotl(x12 ^ x01, 8);
|
|
4430
|
+
x11 = (x11 + x12) | 0;
|
|
4431
|
+
x06 = rotl(x06 ^ x11, 7);
|
|
4432
|
+
x02 = (x02 + x07) | 0;
|
|
4433
|
+
x13 = rotl(x13 ^ x02, 16);
|
|
4434
|
+
x08 = (x08 + x13) | 0;
|
|
4435
|
+
x07 = rotl(x07 ^ x08, 12);
|
|
4436
|
+
x02 = (x02 + x07) | 0;
|
|
4437
|
+
x13 = rotl(x13 ^ x02, 8);
|
|
4438
|
+
x08 = (x08 + x13) | 0;
|
|
4439
|
+
x07 = rotl(x07 ^ x08, 7);
|
|
4440
|
+
x03 = (x03 + x04) | 0;
|
|
4441
|
+
x14 = rotl(x14 ^ x03, 16);
|
|
4442
|
+
x09 = (x09 + x14) | 0;
|
|
4443
|
+
x04 = rotl(x04 ^ x09, 12);
|
|
4444
|
+
x03 = (x03 + x04) | 0;
|
|
4445
|
+
x14 = rotl(x14 ^ x03, 8);
|
|
4446
|
+
x09 = (x09 + x14) | 0;
|
|
4447
|
+
x04 = rotl(x04 ^ x09, 7);
|
|
4448
|
+
}
|
|
4449
|
+
// Write output
|
|
4450
|
+
let oi = 0;
|
|
4451
|
+
out[oi++] = (y00 + x00) | 0;
|
|
4452
|
+
out[oi++] = (y01 + x01) | 0;
|
|
4453
|
+
out[oi++] = (y02 + x02) | 0;
|
|
4454
|
+
out[oi++] = (y03 + x03) | 0;
|
|
4455
|
+
out[oi++] = (y04 + x04) | 0;
|
|
4456
|
+
out[oi++] = (y05 + x05) | 0;
|
|
4457
|
+
out[oi++] = (y06 + x06) | 0;
|
|
4458
|
+
out[oi++] = (y07 + x07) | 0;
|
|
4459
|
+
out[oi++] = (y08 + x08) | 0;
|
|
4460
|
+
out[oi++] = (y09 + x09) | 0;
|
|
4461
|
+
out[oi++] = (y10 + x10) | 0;
|
|
4462
|
+
out[oi++] = (y11 + x11) | 0;
|
|
4463
|
+
out[oi++] = (y12 + x12) | 0;
|
|
4464
|
+
out[oi++] = (y13 + x13) | 0;
|
|
4465
|
+
out[oi++] = (y14 + x14) | 0;
|
|
4466
|
+
out[oi++] = (y15 + x15) | 0;
|
|
4467
|
+
}
|
|
4468
|
+
/**
|
|
4469
|
+
* ChaCha stream cipher. Conforms to RFC 8439 (IETF, TLS). 12-byte nonce, 4-byte counter.
|
|
4470
|
+
* With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance.
|
|
4471
|
+
*/
|
|
4472
|
+
const chacha20 = /* @__PURE__ */ createCipher(chachaCore, {
|
|
4473
|
+
counterRight: false,
|
|
4474
|
+
counterLength: 4,
|
|
4475
|
+
allowShortKeys: false,
|
|
4476
|
+
});
|
|
4477
|
+
const ZEROS16 = /* @__PURE__ */ new Uint8Array(16);
|
|
4478
|
+
// Pad to digest size with zeros
|
|
4479
|
+
const updatePadded = (h, msg) => {
|
|
4480
|
+
h.update(msg);
|
|
4481
|
+
const left = msg.length % 16;
|
|
4482
|
+
if (left)
|
|
4483
|
+
h.update(ZEROS16.subarray(left));
|
|
4484
|
+
};
|
|
4485
|
+
const ZEROS32 = /* @__PURE__ */ new Uint8Array(32);
|
|
4486
|
+
function computeTag(fn, key, nonce, data, AAD) {
|
|
4487
|
+
const authKey = fn(key, nonce, ZEROS32);
|
|
4488
|
+
const h = poly1305.create(authKey);
|
|
4489
|
+
if (AAD)
|
|
4490
|
+
updatePadded(h, AAD);
|
|
4491
|
+
updatePadded(h, data);
|
|
4492
|
+
const num = u64Lengths(data.length, AAD ? AAD.length : 0, true);
|
|
4493
|
+
h.update(num);
|
|
4494
|
+
const res = h.digest();
|
|
4495
|
+
clean(authKey, num);
|
|
4496
|
+
return res;
|
|
4497
|
+
}
|
|
4498
|
+
/**
|
|
4499
|
+
* AEAD algorithm from RFC 8439.
|
|
4500
|
+
* Salsa20 and chacha (RFC 8439) use poly1305 differently.
|
|
4501
|
+
* We could have composed them similar to:
|
|
4502
|
+
* https://github.com/paulmillr/scure-base/blob/b266c73dde977b1dd7ef40ef7a23cc15aab526b3/index.ts#L250
|
|
4503
|
+
* But it's hard because of authKey:
|
|
4504
|
+
* In salsa20, authKey changes position in salsa stream.
|
|
4505
|
+
* In chacha, authKey can't be computed inside computeTag, it modifies the counter.
|
|
4506
|
+
*/
|
|
4507
|
+
const _poly1305_aead = (xorStream) => (key, nonce, AAD) => {
|
|
4508
|
+
const tagLength = 16;
|
|
4509
|
+
return {
|
|
4510
|
+
encrypt(plaintext, output) {
|
|
4511
|
+
const plength = plaintext.length;
|
|
4512
|
+
output = getOutput(plength + tagLength, output, false);
|
|
4513
|
+
output.set(plaintext);
|
|
4514
|
+
const oPlain = output.subarray(0, -tagLength);
|
|
4515
|
+
xorStream(key, nonce, oPlain, oPlain, 1);
|
|
4516
|
+
const tag = computeTag(xorStream, key, nonce, oPlain, AAD);
|
|
4517
|
+
output.set(tag, plength); // append tag
|
|
4518
|
+
clean(tag);
|
|
4519
|
+
return output;
|
|
4520
|
+
},
|
|
4521
|
+
decrypt(ciphertext, output) {
|
|
4522
|
+
output = getOutput(ciphertext.length - tagLength, output, false);
|
|
4523
|
+
const data = ciphertext.subarray(0, -tagLength);
|
|
4524
|
+
const passedTag = ciphertext.subarray(-tagLength);
|
|
4525
|
+
const tag = computeTag(xorStream, key, nonce, data, AAD);
|
|
4526
|
+
if (!equalBytes(passedTag, tag))
|
|
4527
|
+
throw new Error('invalid tag');
|
|
4528
|
+
output.set(ciphertext.subarray(0, -tagLength));
|
|
4529
|
+
xorStream(key, nonce, output, output, 1); // start stream with i=1
|
|
4530
|
+
clean(tag);
|
|
4531
|
+
return output;
|
|
4532
|
+
},
|
|
4533
|
+
};
|
|
4534
|
+
};
|
|
4535
|
+
/**
|
|
4536
|
+
* ChaCha20-Poly1305 from RFC 8439.
|
|
4537
|
+
*
|
|
4538
|
+
* Unsafe to use random nonces under the same key, due to collision chance.
|
|
4539
|
+
* Prefer XChaCha instead.
|
|
4540
|
+
*/
|
|
4541
|
+
const chacha20poly1305 = /* @__PURE__ */ wrapCipher({ blockSize: 64, nonceLength: 12, tagLength: 16 }, _poly1305_aead(chacha20));
|
|
4542
|
+
|
|
4543
|
+
/**
|
|
4544
|
+
* HKDF (RFC 5869): extract + expand in one step.
|
|
4545
|
+
* See https://soatok.blog/2021/11/17/understanding-hkdf/.
|
|
4546
|
+
* @module
|
|
4547
|
+
*/
|
|
4548
|
+
/**
|
|
4549
|
+
* HKDF-extract from spec. Less important part. `HKDF-Extract(IKM, salt) -> PRK`
|
|
4550
|
+
* Arguments position differs from spec (IKM is first one, since it is not optional)
|
|
4551
|
+
* @param hash - hash function that would be used (e.g. sha256)
|
|
4552
|
+
* @param ikm - input keying material, the initial key
|
|
4553
|
+
* @param salt - optional salt value (a non-secret random value)
|
|
4554
|
+
*/
|
|
4555
|
+
function extract(hash, ikm, salt) {
|
|
4556
|
+
ahash(hash);
|
|
4557
|
+
// NOTE: some libraries treat zero-length array as 'not provided';
|
|
4558
|
+
// we don't, since we have undefined as 'not provided'
|
|
4559
|
+
// https://github.com/RustCrypto/KDFs/issues/15
|
|
4560
|
+
if (salt === undefined)
|
|
4561
|
+
salt = new Uint8Array(hash.outputLen);
|
|
4562
|
+
return hmac(hash, toBytes$1(salt), toBytes$1(ikm));
|
|
4563
|
+
}
|
|
4564
|
+
const HKDF_COUNTER = /* @__PURE__ */ Uint8Array.from([0]);
|
|
4565
|
+
const EMPTY_BUFFER = /* @__PURE__ */ Uint8Array.of();
|
|
4566
|
+
/**
|
|
4567
|
+
* HKDF-expand from the spec. The most important part. `HKDF-Expand(PRK, info, L) -> OKM`
|
|
4568
|
+
* @param hash - hash function that would be used (e.g. sha256)
|
|
4569
|
+
* @param prk - a pseudorandom key of at least HashLen octets (usually, the output from the extract step)
|
|
4570
|
+
* @param info - optional context and application specific information (can be a zero-length string)
|
|
4571
|
+
* @param length - length of output keying material in bytes
|
|
4572
|
+
*/
|
|
4573
|
+
function expand(hash, prk, info, length = 32) {
|
|
4574
|
+
ahash(hash);
|
|
4575
|
+
anumber$1(length);
|
|
4576
|
+
const olen = hash.outputLen;
|
|
4577
|
+
if (length > 255 * olen)
|
|
4578
|
+
throw new Error('Length should be <= 255*HashLen');
|
|
4579
|
+
const blocks = Math.ceil(length / olen);
|
|
4580
|
+
if (info === undefined)
|
|
4581
|
+
info = EMPTY_BUFFER;
|
|
4582
|
+
// first L(ength) octets of T
|
|
4583
|
+
const okm = new Uint8Array(blocks * olen);
|
|
4584
|
+
// Re-use HMAC instance between blocks
|
|
4585
|
+
const HMAC = hmac.create(hash, prk);
|
|
4586
|
+
const HMACTmp = HMAC._cloneInto();
|
|
4587
|
+
const T = new Uint8Array(HMAC.outputLen);
|
|
4588
|
+
for (let counter = 0; counter < blocks; counter++) {
|
|
4589
|
+
HKDF_COUNTER[0] = counter + 1;
|
|
4590
|
+
// T(0) = empty string (zero length)
|
|
4591
|
+
// T(N) = HMAC-Hash(PRK, T(N-1) | info | N)
|
|
4592
|
+
HMACTmp.update(counter === 0 ? EMPTY_BUFFER : T)
|
|
4593
|
+
.update(info)
|
|
4594
|
+
.update(HKDF_COUNTER)
|
|
4595
|
+
.digestInto(T);
|
|
4596
|
+
okm.set(T, olen * counter);
|
|
4597
|
+
HMAC._cloneInto(HMACTmp);
|
|
4598
|
+
}
|
|
4599
|
+
HMAC.destroy();
|
|
4600
|
+
HMACTmp.destroy();
|
|
4601
|
+
clean$1(T, HKDF_COUNTER);
|
|
4602
|
+
return okm.slice(0, length);
|
|
4603
|
+
}
|
|
4604
|
+
/**
|
|
4605
|
+
* HKDF (RFC 5869): derive keys from an initial input.
|
|
4606
|
+
* Combines hkdf_extract + hkdf_expand in one step
|
|
4607
|
+
* @param hash - hash function that would be used (e.g. sha256)
|
|
4608
|
+
* @param ikm - input keying material, the initial key
|
|
4609
|
+
* @param salt - optional salt value (a non-secret random value)
|
|
4610
|
+
* @param info - optional context and application specific information (can be a zero-length string)
|
|
4611
|
+
* @param length - length of output keying material in bytes
|
|
4612
|
+
* @example
|
|
4613
|
+
* import { hkdf } from '@noble/hashes/hkdf';
|
|
4614
|
+
* import { sha256 } from '@noble/hashes/sha2';
|
|
4615
|
+
* import { randomBytes } from '@noble/hashes/utils';
|
|
4616
|
+
* const inputKey = randomBytes(32);
|
|
4617
|
+
* const salt = randomBytes(32);
|
|
4618
|
+
* const info = 'application-key';
|
|
4619
|
+
* const hk1 = hkdf(sha256, inputKey, salt, info, 32);
|
|
4620
|
+
*/
|
|
4621
|
+
const hkdf = (hash, ikm, salt, info, length) => expand(hash, extract(hash, ikm, salt), info, length);
|
|
4622
|
+
|
|
4623
|
+
/**
|
|
4624
|
+
* NIP-44 Encryption implementation.
|
|
4625
|
+
* ChaCha20-Poly1305 AEAD encryption with HKDF key derivation.
|
|
4626
|
+
* Works in both Node.js and browser environments.
|
|
4627
|
+
* See: https://github.com/nostr-protocol/nips/blob/master/44.md
|
|
4628
|
+
*/
|
|
4629
|
+
/** NIP-44 version byte */
|
|
4630
|
+
const VERSION = 0x02;
|
|
4631
|
+
/** Nonce size for XChaCha20 (24 bytes) */
|
|
4632
|
+
const NONCE_SIZE = 24;
|
|
4633
|
+
/** MAC size for Poly1305 (16 bytes) */
|
|
4634
|
+
const MAC_SIZE = 16;
|
|
4635
|
+
/** Minimum padded length */
|
|
4636
|
+
const MIN_PADDED_LEN = 32;
|
|
4637
|
+
/** Maximum message length */
|
|
4638
|
+
const MAX_MESSAGE_LEN = 65535;
|
|
4639
|
+
/** HKDF salt for conversation key derivation */
|
|
4640
|
+
const HKDF_SALT = new TextEncoder().encode('nip44-v2');
|
|
4641
|
+
/**
|
|
4642
|
+
* Derive conversation key using ECDH + HKDF.
|
|
4643
|
+
* NIP-44 uses sorted public keys as salt for HKDF.
|
|
4644
|
+
*
|
|
4645
|
+
* @param myPrivateKey 32-byte private key
|
|
4646
|
+
* @param theirPublicKey 32-byte x-only public key
|
|
4647
|
+
* @returns 32-byte conversation key
|
|
4648
|
+
*/
|
|
4649
|
+
function deriveConversationKey(myPrivateKey, theirPublicKey) {
|
|
4650
|
+
if (myPrivateKey.length !== 32) {
|
|
4651
|
+
throw new Error('Private key must be 32 bytes');
|
|
4652
|
+
}
|
|
4653
|
+
if (theirPublicKey.length !== 32) {
|
|
4654
|
+
throw new Error('Public key must be 32 bytes');
|
|
4655
|
+
}
|
|
4656
|
+
// Get shared X coordinate via ECDH
|
|
4657
|
+
const sharedX = computeSharedX(myPrivateKey, theirPublicKey);
|
|
4658
|
+
// Get my public key
|
|
4659
|
+
const myPublicKey = secp256k1.getPublicKey(myPrivateKey, true).slice(1); // Remove prefix
|
|
4660
|
+
// Create salt from sorted public keys
|
|
4661
|
+
const salt = createSortedKeysSalt(myPublicKey, theirPublicKey);
|
|
4662
|
+
// Use HKDF to derive conversation key
|
|
4663
|
+
return hkdf(sha256, sharedX, salt, HKDF_SALT, 32);
|
|
4664
|
+
}
|
|
4665
|
+
/**
|
|
4666
|
+
* Derive conversation key from hex-encoded keys.
|
|
4667
|
+
*/
|
|
4668
|
+
function deriveConversationKeyHex(myPrivateKeyHex, theirPublicKeyHex) {
|
|
4669
|
+
const myPrivateKey = hexToBytes(myPrivateKeyHex);
|
|
4670
|
+
const theirPublicKey = hexToBytes(theirPublicKeyHex);
|
|
4671
|
+
return bytesToHex(deriveConversationKey(myPrivateKey, theirPublicKey));
|
|
4672
|
+
}
|
|
4673
|
+
/**
|
|
4674
|
+
* Compute ECDH shared X coordinate.
|
|
4675
|
+
*/
|
|
4676
|
+
function computeSharedX(myPrivateKey, theirPublicKey) {
|
|
4677
|
+
// Reconstruct full public key (add 02 prefix for even y)
|
|
4678
|
+
const fullPublicKey = new Uint8Array(33);
|
|
4679
|
+
fullPublicKey[0] = 0x02;
|
|
4680
|
+
fullPublicKey.set(theirPublicKey, 1);
|
|
4681
|
+
// Compute shared point
|
|
4682
|
+
const sharedPoint = secp256k1.getSharedSecret(myPrivateKey, fullPublicKey);
|
|
4683
|
+
// Extract X coordinate (skip 04 prefix, take first 32 bytes)
|
|
4684
|
+
return sharedPoint.slice(1, 33);
|
|
4685
|
+
}
|
|
4686
|
+
/**
|
|
4687
|
+
* Create salt from lexicographically sorted public keys.
|
|
4688
|
+
*/
|
|
4689
|
+
function createSortedKeysSalt(pk1, pk2) {
|
|
4690
|
+
const cmp = compareBytes(pk1, pk2);
|
|
4691
|
+
if (cmp <= 0) {
|
|
4692
|
+
return concatBytes(pk1, pk2);
|
|
4693
|
+
}
|
|
4694
|
+
else {
|
|
4695
|
+
return concatBytes(pk2, pk1);
|
|
4696
|
+
}
|
|
4697
|
+
}
|
|
4698
|
+
/**
|
|
4699
|
+
* Compare two byte arrays lexicographically.
|
|
4700
|
+
*/
|
|
4701
|
+
function compareBytes(a, b) {
|
|
4702
|
+
const len = Math.min(a.length, b.length);
|
|
4703
|
+
for (let i = 0; i < len; i++) {
|
|
4704
|
+
const diff = a[i] - b[i];
|
|
4705
|
+
if (diff !== 0)
|
|
4706
|
+
return diff;
|
|
4707
|
+
}
|
|
4708
|
+
return a.length - b.length;
|
|
4709
|
+
}
|
|
4710
|
+
/**
|
|
4711
|
+
* Calculate padded length according to NIP-44 spec.
|
|
4712
|
+
* Uses power-of-2 chunk padding to hide message length.
|
|
4713
|
+
*/
|
|
4714
|
+
function calcPaddedLen(unpaddedLen) {
|
|
4715
|
+
if (unpaddedLen <= 0) {
|
|
4716
|
+
throw new Error('Message too short');
|
|
4717
|
+
}
|
|
4718
|
+
if (unpaddedLen > MAX_MESSAGE_LEN) {
|
|
4719
|
+
throw new Error('Message too long');
|
|
4720
|
+
}
|
|
4721
|
+
if (unpaddedLen <= 32) {
|
|
4722
|
+
return 32;
|
|
4723
|
+
}
|
|
4724
|
+
// Find next power of 2
|
|
4725
|
+
const nextPow2 = 1 << Math.ceil(Math.log2(unpaddedLen));
|
|
4726
|
+
const chunk = Math.max(32, nextPow2 >> 3);
|
|
4727
|
+
return Math.ceil(unpaddedLen / chunk) * chunk;
|
|
4728
|
+
}
|
|
4729
|
+
/**
|
|
4730
|
+
* Pad message according to NIP-44 spec.
|
|
4731
|
+
* Format: length(2 bytes big-endian) || message || padding
|
|
4732
|
+
*/
|
|
4733
|
+
function pad(message) {
|
|
4734
|
+
const len = message.length;
|
|
4735
|
+
if (len < 1) {
|
|
4736
|
+
throw new Error('Message too short');
|
|
4737
|
+
}
|
|
4738
|
+
if (len > MAX_MESSAGE_LEN) {
|
|
4739
|
+
throw new Error('Message too long');
|
|
4740
|
+
}
|
|
4741
|
+
const paddedLen = calcPaddedLen(len);
|
|
4742
|
+
const result = new Uint8Array(2 + paddedLen);
|
|
4743
|
+
// Big-endian length prefix
|
|
4744
|
+
result[0] = (len >> 8) & 0xff;
|
|
4745
|
+
result[1] = len & 0xff;
|
|
4746
|
+
// Copy message
|
|
4747
|
+
result.set(message, 2);
|
|
4748
|
+
// Remaining bytes are already zero (padding)
|
|
4749
|
+
return result;
|
|
4750
|
+
}
|
|
4751
|
+
/**
|
|
4752
|
+
* Unpad message according to NIP-44 spec.
|
|
4753
|
+
*/
|
|
4754
|
+
function unpad(padded) {
|
|
4755
|
+
if (padded.length < 2 + MIN_PADDED_LEN) {
|
|
4756
|
+
throw new Error('Padded message too short');
|
|
4757
|
+
}
|
|
4758
|
+
// Read big-endian length prefix
|
|
4759
|
+
const len = (padded[0] << 8) | padded[1];
|
|
4760
|
+
if (len < 1 || len > MAX_MESSAGE_LEN) {
|
|
4761
|
+
throw new Error(`Invalid message length: ${len}`);
|
|
4762
|
+
}
|
|
4763
|
+
const expectedPaddedLen = calcPaddedLen(len);
|
|
4764
|
+
if (padded.length !== 2 + expectedPaddedLen) {
|
|
4765
|
+
throw new Error('Invalid padding');
|
|
4766
|
+
}
|
|
4767
|
+
return padded.slice(2, 2 + len);
|
|
4768
|
+
}
|
|
4769
|
+
/**
|
|
4770
|
+
* Encrypt a message using NIP-44.
|
|
4771
|
+
*
|
|
4772
|
+
* @param message Plaintext message
|
|
4773
|
+
* @param myPrivateKey Sender's 32-byte private key
|
|
4774
|
+
* @param theirPublicKey Recipient's 32-byte x-only public key
|
|
4775
|
+
* @returns Base64-encoded encrypted payload
|
|
4776
|
+
*/
|
|
4777
|
+
function encrypt(message, myPrivateKey, theirPublicKey) {
|
|
4778
|
+
const conversationKey = deriveConversationKey(myPrivateKey, theirPublicKey);
|
|
4779
|
+
return encryptWithKey(message, conversationKey);
|
|
4780
|
+
}
|
|
4781
|
+
/**
|
|
4782
|
+
* Encrypt a message using a pre-derived conversation key.
|
|
4783
|
+
*
|
|
4784
|
+
* @param message Plaintext message
|
|
4785
|
+
* @param conversationKey 32-byte conversation key
|
|
4786
|
+
* @returns Base64-encoded encrypted payload
|
|
4787
|
+
*/
|
|
4788
|
+
function encryptWithKey(message, conversationKey) {
|
|
4789
|
+
const encoder = new TextEncoder();
|
|
4790
|
+
const messageBytes = encoder.encode(message);
|
|
4791
|
+
if (messageBytes.length > MAX_MESSAGE_LEN) {
|
|
4792
|
+
throw new Error(`Message too long (max ${MAX_MESSAGE_LEN} bytes)`);
|
|
4793
|
+
}
|
|
4794
|
+
// Pad the message
|
|
4795
|
+
const padded = pad(messageBytes);
|
|
4796
|
+
// Generate random nonce (24 bytes for XChaCha20)
|
|
4797
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
4798
|
+
// Derive message keys using HKDF
|
|
4799
|
+
const messageKey = hkdf(sha256, conversationKey, nonce, new Uint8Array(0), 76);
|
|
4800
|
+
const chachaKey = messageKey.slice(0, 32);
|
|
4801
|
+
const chachaNonce = messageKey.slice(32, 44);
|
|
4802
|
+
// Encrypt with ChaCha20-Poly1305
|
|
4803
|
+
const cipher = chacha20poly1305(chachaKey, chachaNonce);
|
|
4804
|
+
const ciphertext = cipher.encrypt(padded);
|
|
4805
|
+
// Assemble payload: version(1) || nonce(24) || ciphertext+mac
|
|
4806
|
+
const payload = new Uint8Array(1 + NONCE_SIZE + ciphertext.length);
|
|
4807
|
+
payload[0] = VERSION;
|
|
4808
|
+
payload.set(nonce, 1);
|
|
4809
|
+
payload.set(ciphertext, 1 + NONCE_SIZE);
|
|
4810
|
+
return toBase64(payload);
|
|
4811
|
+
}
|
|
4812
|
+
/**
|
|
4813
|
+
* Decrypt a NIP-44 encrypted message.
|
|
4814
|
+
*
|
|
4815
|
+
* @param encryptedContent Base64-encoded encrypted payload
|
|
4816
|
+
* @param myPrivateKey Recipient's 32-byte private key
|
|
4817
|
+
* @param theirPublicKey Sender's 32-byte x-only public key
|
|
4818
|
+
* @returns Decrypted plaintext message
|
|
4819
|
+
*/
|
|
4820
|
+
function decrypt(encryptedContent, myPrivateKey, theirPublicKey) {
|
|
4821
|
+
const conversationKey = deriveConversationKey(myPrivateKey, theirPublicKey);
|
|
4822
|
+
return decryptWithKey(encryptedContent, conversationKey);
|
|
4823
|
+
}
|
|
4824
|
+
/**
|
|
4825
|
+
* Decrypt a message using a pre-derived conversation key.
|
|
4826
|
+
*
|
|
4827
|
+
* @param encryptedContent Base64-encoded encrypted payload
|
|
4828
|
+
* @param conversationKey 32-byte conversation key
|
|
4829
|
+
* @returns Decrypted plaintext message
|
|
4830
|
+
*/
|
|
4831
|
+
function decryptWithKey(encryptedContent, conversationKey) {
|
|
4832
|
+
const payload = fromBase64(encryptedContent);
|
|
4833
|
+
if (payload.length < 1 + NONCE_SIZE + MIN_PADDED_LEN + MAC_SIZE) {
|
|
4834
|
+
throw new Error('Payload too short');
|
|
4835
|
+
}
|
|
4836
|
+
// Check version
|
|
4837
|
+
if (payload[0] !== VERSION) {
|
|
4838
|
+
throw new Error(`Unsupported NIP-44 version: ${payload[0]}`);
|
|
4839
|
+
}
|
|
4840
|
+
// Extract components
|
|
4841
|
+
const nonce = payload.slice(1, 1 + NONCE_SIZE);
|
|
4842
|
+
const ciphertext = payload.slice(1 + NONCE_SIZE);
|
|
4843
|
+
// Derive message keys
|
|
4844
|
+
const messageKey = hkdf(sha256, conversationKey, nonce, new Uint8Array(0), 76);
|
|
4845
|
+
const chachaKey = messageKey.slice(0, 32);
|
|
4846
|
+
const chachaNonce = messageKey.slice(32, 44);
|
|
4847
|
+
// Decrypt with ChaCha20-Poly1305
|
|
4848
|
+
const cipher = chacha20poly1305(chachaKey, chachaNonce);
|
|
4849
|
+
const padded = cipher.decrypt(ciphertext);
|
|
4850
|
+
// Unpad
|
|
4851
|
+
const messageBytes = unpad(padded);
|
|
4852
|
+
const decoder = new TextDecoder();
|
|
4853
|
+
return decoder.decode(messageBytes);
|
|
4854
|
+
}
|
|
4855
|
+
/**
|
|
4856
|
+
* Encrypt a message using hex-encoded keys.
|
|
4857
|
+
*/
|
|
4858
|
+
function encryptHex(message, myPrivateKeyHex, theirPublicKeyHex) {
|
|
4859
|
+
const myPrivateKey = hexToBytes(myPrivateKeyHex);
|
|
4860
|
+
const theirPublicKey = hexToBytes(theirPublicKeyHex);
|
|
4861
|
+
return encrypt(message, myPrivateKey, theirPublicKey);
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* Decrypt a message using hex-encoded keys.
|
|
4865
|
+
*/
|
|
4866
|
+
function decryptHex(encryptedContent, myPrivateKeyHex, theirPublicKeyHex) {
|
|
4867
|
+
const myPrivateKey = hexToBytes(myPrivateKeyHex);
|
|
4868
|
+
const theirPublicKey = hexToBytes(theirPublicKeyHex);
|
|
4869
|
+
return decrypt(encryptedContent, myPrivateKey, theirPublicKey);
|
|
4870
|
+
}
|
|
4871
|
+
/**
|
|
4872
|
+
* Convert a Uint8Array to base64 string (browser and Node.js compatible).
|
|
4873
|
+
*/
|
|
4874
|
+
function toBase64(bytes) {
|
|
4875
|
+
if (typeof Buffer !== 'undefined') {
|
|
4876
|
+
return Buffer.from(bytes).toString('base64');
|
|
4877
|
+
}
|
|
4878
|
+
// Browser environment
|
|
4879
|
+
let binary = '';
|
|
4880
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
4881
|
+
binary += String.fromCharCode(bytes[i]);
|
|
4882
|
+
}
|
|
4883
|
+
return btoa(binary);
|
|
4884
|
+
}
|
|
4885
|
+
/**
|
|
4886
|
+
* Convert a base64 string to Uint8Array (browser and Node.js compatible).
|
|
4887
|
+
*/
|
|
4888
|
+
function fromBase64(base64) {
|
|
4889
|
+
if (typeof Buffer !== 'undefined') {
|
|
4890
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
4891
|
+
}
|
|
4892
|
+
// Browser environment
|
|
4893
|
+
const binary = atob(base64);
|
|
4894
|
+
const bytes = new Uint8Array(binary.length);
|
|
4895
|
+
for (let i = 0; i < binary.length; i++) {
|
|
4896
|
+
bytes[i] = binary.charCodeAt(i);
|
|
4897
|
+
}
|
|
4898
|
+
return bytes;
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
var nip44 = /*#__PURE__*/Object.freeze({
|
|
4902
|
+
__proto__: null,
|
|
4903
|
+
VERSION: VERSION,
|
|
4904
|
+
calcPaddedLen: calcPaddedLen,
|
|
4905
|
+
decrypt: decrypt,
|
|
4906
|
+
decryptHex: decryptHex,
|
|
4907
|
+
decryptWithKey: decryptWithKey,
|
|
4908
|
+
deriveConversationKey: deriveConversationKey,
|
|
4909
|
+
deriveConversationKeyHex: deriveConversationKeyHex,
|
|
3701
4910
|
encrypt: encrypt,
|
|
3702
|
-
encryptHex: encryptHex
|
|
4911
|
+
encryptHex: encryptHex,
|
|
4912
|
+
encryptWithKey: encryptWithKey,
|
|
4913
|
+
pad: pad,
|
|
4914
|
+
unpad: unpad
|
|
3703
4915
|
});
|
|
3704
4916
|
|
|
3705
4917
|
/**
|
|
@@ -3863,7 +5075,7 @@
|
|
|
3863
5075
|
*/
|
|
3864
5076
|
async encrypt(message, recipientPublicKey) {
|
|
3865
5077
|
this.ensureNotCleared();
|
|
3866
|
-
return encrypt(message, this.privateKey, recipientPublicKey);
|
|
5078
|
+
return encrypt$1(message, this.privateKey, recipientPublicKey);
|
|
3867
5079
|
}
|
|
3868
5080
|
/**
|
|
3869
5081
|
* Encrypt a message using hex-encoded recipient public key.
|
|
@@ -3874,7 +5086,7 @@
|
|
|
3874
5086
|
async encryptHex(message, recipientPublicKeyHex) {
|
|
3875
5087
|
this.ensureNotCleared();
|
|
3876
5088
|
const recipientPublicKey = hexToBytes(recipientPublicKeyHex);
|
|
3877
|
-
return encrypt(message, this.privateKey, recipientPublicKey);
|
|
5089
|
+
return encrypt$1(message, this.privateKey, recipientPublicKey);
|
|
3878
5090
|
}
|
|
3879
5091
|
/**
|
|
3880
5092
|
* Decrypt a NIP-04 encrypted message.
|
|
@@ -3884,7 +5096,7 @@
|
|
|
3884
5096
|
*/
|
|
3885
5097
|
async decrypt(encryptedContent, senderPublicKey) {
|
|
3886
5098
|
this.ensureNotCleared();
|
|
3887
|
-
return decrypt(encryptedContent, this.privateKey, senderPublicKey);
|
|
5099
|
+
return decrypt$1(encryptedContent, this.privateKey, senderPublicKey);
|
|
3888
5100
|
}
|
|
3889
5101
|
/**
|
|
3890
5102
|
* Decrypt a message using hex-encoded sender public key.
|
|
@@ -3895,7 +5107,7 @@
|
|
|
3895
5107
|
async decryptHex(encryptedContent, senderPublicKeyHex) {
|
|
3896
5108
|
this.ensureNotCleared();
|
|
3897
5109
|
const senderPublicKey = hexToBytes(senderPublicKeyHex);
|
|
3898
|
-
return decrypt(encryptedContent, this.privateKey, senderPublicKey);
|
|
5110
|
+
return decrypt$1(encryptedContent, this.privateKey, senderPublicKey);
|
|
3899
5111
|
}
|
|
3900
5112
|
/**
|
|
3901
5113
|
* Derive a shared secret using ECDH.
|
|
@@ -3906,6 +5118,62 @@
|
|
|
3906
5118
|
this.ensureNotCleared();
|
|
3907
5119
|
return deriveSharedSecret(this.privateKey, theirPublicKey);
|
|
3908
5120
|
}
|
|
5121
|
+
// ============================================================================
|
|
5122
|
+
// NIP-44 Encryption (XChaCha20-Poly1305)
|
|
5123
|
+
// ============================================================================
|
|
5124
|
+
/**
|
|
5125
|
+
* Encrypt a message using NIP-44 encryption.
|
|
5126
|
+
* Uses XChaCha20-Poly1305 with HKDF key derivation.
|
|
5127
|
+
* @param message Message to encrypt
|
|
5128
|
+
* @param recipientPublicKey 32-byte x-only public key of recipient
|
|
5129
|
+
* @returns Base64-encoded encrypted content
|
|
5130
|
+
*/
|
|
5131
|
+
encryptNip44(message, recipientPublicKey) {
|
|
5132
|
+
this.ensureNotCleared();
|
|
5133
|
+
return encrypt(message, this.privateKey, recipientPublicKey);
|
|
5134
|
+
}
|
|
5135
|
+
/**
|
|
5136
|
+
* Encrypt a message using NIP-44 with hex-encoded recipient public key.
|
|
5137
|
+
* @param message Message to encrypt
|
|
5138
|
+
* @param recipientPublicKeyHex Hex-encoded recipient public key
|
|
5139
|
+
* @returns Base64-encoded encrypted content
|
|
5140
|
+
*/
|
|
5141
|
+
encryptNip44Hex(message, recipientPublicKeyHex) {
|
|
5142
|
+
this.ensureNotCleared();
|
|
5143
|
+
const recipientPublicKey = hexToBytes(recipientPublicKeyHex);
|
|
5144
|
+
return encrypt(message, this.privateKey, recipientPublicKey);
|
|
5145
|
+
}
|
|
5146
|
+
/**
|
|
5147
|
+
* Decrypt a NIP-44 encrypted message.
|
|
5148
|
+
* @param encryptedContent Base64-encoded encrypted content
|
|
5149
|
+
* @param senderPublicKey 32-byte x-only public key of sender
|
|
5150
|
+
* @returns Decrypted message
|
|
5151
|
+
*/
|
|
5152
|
+
decryptNip44(encryptedContent, senderPublicKey) {
|
|
5153
|
+
this.ensureNotCleared();
|
|
5154
|
+
return decrypt(encryptedContent, this.privateKey, senderPublicKey);
|
|
5155
|
+
}
|
|
5156
|
+
/**
|
|
5157
|
+
* Decrypt a NIP-44 message using hex-encoded sender public key.
|
|
5158
|
+
* @param encryptedContent Base64-encoded encrypted content
|
|
5159
|
+
* @param senderPublicKeyHex Hex-encoded sender public key
|
|
5160
|
+
* @returns Decrypted message
|
|
5161
|
+
*/
|
|
5162
|
+
decryptNip44Hex(encryptedContent, senderPublicKeyHex) {
|
|
5163
|
+
this.ensureNotCleared();
|
|
5164
|
+
const senderPublicKey = hexToBytes(senderPublicKeyHex);
|
|
5165
|
+
return decrypt(encryptedContent, this.privateKey, senderPublicKey);
|
|
5166
|
+
}
|
|
5167
|
+
/**
|
|
5168
|
+
* Derive NIP-44 conversation key with another party.
|
|
5169
|
+
* Uses ECDH + HKDF with sorted public keys as salt.
|
|
5170
|
+
* @param theirPublicKey 32-byte x-only public key
|
|
5171
|
+
* @returns 32-byte conversation key
|
|
5172
|
+
*/
|
|
5173
|
+
deriveConversationKey(theirPublicKey) {
|
|
5174
|
+
this.ensureNotCleared();
|
|
5175
|
+
return deriveConversationKey(this.privateKey, theirPublicKey);
|
|
5176
|
+
}
|
|
3909
5177
|
/**
|
|
3910
5178
|
* Check if a public key matches this key manager's public key.
|
|
3911
5179
|
* @param publicKeyHex Hex-encoded public key to check
|
|
@@ -4311,6 +5579,12 @@
|
|
|
4311
5579
|
const ENCRYPTED_DM = 4;
|
|
4312
5580
|
/** NIP-09: Event deletion */
|
|
4313
5581
|
const DELETION = 5;
|
|
5582
|
+
/** NIP-17: Seal (signed, encrypted rumor) */
|
|
5583
|
+
const SEAL = 13;
|
|
5584
|
+
/** NIP-17: Private chat message (rumor - unsigned inner event) */
|
|
5585
|
+
const CHAT_MESSAGE = 14;
|
|
5586
|
+
/** NIP-17: Read receipt (rumor kind) */
|
|
5587
|
+
const READ_RECEIPT = 15;
|
|
4314
5588
|
/** NIP-25: Reactions (likes, etc.) */
|
|
4315
5589
|
const REACTION = 7;
|
|
4316
5590
|
/** NIP-59: Gift wrap for private events */
|
|
@@ -4383,6 +5657,12 @@
|
|
|
4383
5657
|
return 'Encrypted DM';
|
|
4384
5658
|
case DELETION:
|
|
4385
5659
|
return 'Deletion';
|
|
5660
|
+
case SEAL:
|
|
5661
|
+
return 'Seal';
|
|
5662
|
+
case CHAT_MESSAGE:
|
|
5663
|
+
return 'Chat Message';
|
|
5664
|
+
case READ_RECEIPT:
|
|
5665
|
+
return 'Read Receipt';
|
|
4386
5666
|
case REACTION:
|
|
4387
5667
|
return 'Reaction';
|
|
4388
5668
|
case GIFT_WRAP:
|
|
@@ -4420,6 +5700,7 @@
|
|
|
4420
5700
|
AGENT_LOCATION: AGENT_LOCATION,
|
|
4421
5701
|
AGENT_PROFILE: AGENT_PROFILE,
|
|
4422
5702
|
APP_DATA: APP_DATA,
|
|
5703
|
+
CHAT_MESSAGE: CHAT_MESSAGE,
|
|
4423
5704
|
CONTACTS: CONTACTS,
|
|
4424
5705
|
DELETION: DELETION,
|
|
4425
5706
|
ENCRYPTED_DM: ENCRYPTED_DM,
|
|
@@ -4428,8 +5709,10 @@
|
|
|
4428
5709
|
PAYMENT_REQUEST: PAYMENT_REQUEST,
|
|
4429
5710
|
PROFILE: PROFILE,
|
|
4430
5711
|
REACTION: REACTION,
|
|
5712
|
+
READ_RECEIPT: READ_RECEIPT,
|
|
4431
5713
|
RECOMMEND_RELAY: RECOMMEND_RELAY,
|
|
4432
5714
|
RELAY_LIST: RELAY_LIST,
|
|
5715
|
+
SEAL: SEAL,
|
|
4433
5716
|
TEXT_NOTE: TEXT_NOTE,
|
|
4434
5717
|
TOKEN_TRANSFER: TOKEN_TRANSFER,
|
|
4435
5718
|
getName: getName,
|
|
@@ -4476,6 +5759,235 @@
|
|
|
4476
5759
|
return String(event.data);
|
|
4477
5760
|
}
|
|
4478
5761
|
|
|
5762
|
+
/**
|
|
5763
|
+
* NIP-17 Private Direct Messages Protocol.
|
|
5764
|
+
* Implements gift-wrapping for sender anonymity using NIP-44 encryption.
|
|
5765
|
+
*
|
|
5766
|
+
* Message flow:
|
|
5767
|
+
* 1. Create Rumor (kind 14, unsigned) with actual message content
|
|
5768
|
+
* 2. Create Seal (kind 13, signed by sender) encrypting the rumor
|
|
5769
|
+
* 3. Create Gift Wrap (kind 1059, signed by random ephemeral key) encrypting the seal
|
|
5770
|
+
*
|
|
5771
|
+
* Only the recipient can decrypt and verify the true sender.
|
|
5772
|
+
*/
|
|
5773
|
+
// Randomization window for timestamps (+/- 2 days in seconds)
|
|
5774
|
+
const TIMESTAMP_RANDOMIZATION = 2 * 24 * 60 * 60;
|
|
5775
|
+
/**
|
|
5776
|
+
* Create a gift-wrapped private message.
|
|
5777
|
+
*
|
|
5778
|
+
* @param senderKeys Sender's key manager
|
|
5779
|
+
* @param recipientPubkeyHex Recipient's public key (hex)
|
|
5780
|
+
* @param content Message content
|
|
5781
|
+
* @param options Optional message options (reply-to, etc.)
|
|
5782
|
+
* @returns Gift-wrapped event (kind 1059)
|
|
5783
|
+
*/
|
|
5784
|
+
function createGiftWrap(senderKeys, recipientPubkeyHex, content, options) {
|
|
5785
|
+
// 1. Create Rumor (kind 14, unsigned)
|
|
5786
|
+
const rumor = createRumor(senderKeys.getPublicKeyHex(), recipientPubkeyHex, content, CHAT_MESSAGE, options?.replyToEventId);
|
|
5787
|
+
// 2. Create Seal (kind 13, signed by sender, encrypts rumor)
|
|
5788
|
+
const seal = createSeal(senderKeys, recipientPubkeyHex, rumor);
|
|
5789
|
+
// 3. Create Gift Wrap (kind 1059, signed by ephemeral key, encrypts seal)
|
|
5790
|
+
return wrapSeal(seal, recipientPubkeyHex);
|
|
5791
|
+
}
|
|
5792
|
+
/**
|
|
5793
|
+
* Create a gift-wrapped read receipt.
|
|
5794
|
+
*
|
|
5795
|
+
* @param senderKeys Sender's key manager
|
|
5796
|
+
* @param recipientPubkeyHex Recipient (original sender) public key
|
|
5797
|
+
* @param messageEventId Event ID of the message being acknowledged
|
|
5798
|
+
* @returns Gift-wrapped read receipt event
|
|
5799
|
+
*/
|
|
5800
|
+
function createReadReceipt(senderKeys, recipientPubkeyHex, messageEventId) {
|
|
5801
|
+
// Create rumor with kind 15 (read receipt)
|
|
5802
|
+
const tags = [
|
|
5803
|
+
['p', recipientPubkeyHex],
|
|
5804
|
+
['e', messageEventId],
|
|
5805
|
+
];
|
|
5806
|
+
// Use actual timestamp for rumor (privacy via outer layers)
|
|
5807
|
+
const actualTimestamp = Math.floor(Date.now() / 1000);
|
|
5808
|
+
const rumor = {
|
|
5809
|
+
id: '', // Will be computed
|
|
5810
|
+
pubkey: senderKeys.getPublicKeyHex(),
|
|
5811
|
+
created_at: actualTimestamp,
|
|
5812
|
+
kind: READ_RECEIPT,
|
|
5813
|
+
tags,
|
|
5814
|
+
content: '', // Read receipts have empty content
|
|
5815
|
+
};
|
|
5816
|
+
// Compute the rumor ID
|
|
5817
|
+
rumor.id = computeRumorId(rumor);
|
|
5818
|
+
const seal = createSeal(senderKeys, recipientPubkeyHex, rumor);
|
|
5819
|
+
return wrapSeal(seal, recipientPubkeyHex);
|
|
5820
|
+
}
|
|
5821
|
+
/**
|
|
5822
|
+
* Unwrap a gift-wrapped message.
|
|
5823
|
+
*
|
|
5824
|
+
* @param giftWrap Gift wrap event (kind 1059)
|
|
5825
|
+
* @param recipientKeys Recipient's key manager
|
|
5826
|
+
* @returns Parsed private message
|
|
5827
|
+
*/
|
|
5828
|
+
function unwrap(giftWrap, recipientKeys) {
|
|
5829
|
+
if (giftWrap.kind !== GIFT_WRAP) {
|
|
5830
|
+
throw new Error(`Event is not a gift wrap (kind ${giftWrap.kind})`);
|
|
5831
|
+
}
|
|
5832
|
+
// Get ephemeral sender's pubkey from gift wrap
|
|
5833
|
+
const ephemeralPubkey = giftWrap.pubkey;
|
|
5834
|
+
const ephemeralPubkeyBytes = hexToBytes(ephemeralPubkey);
|
|
5835
|
+
// Decrypt seal from gift wrap content
|
|
5836
|
+
const sealJson = decrypt(giftWrap.content, recipientKeys.getPrivateKey(), ephemeralPubkeyBytes);
|
|
5837
|
+
const sealData = JSON.parse(sealJson);
|
|
5838
|
+
if (sealData.kind !== SEAL) {
|
|
5839
|
+
throw new Error(`Inner event is not a seal (kind ${sealData.kind})`);
|
|
5840
|
+
}
|
|
5841
|
+
// Verify seal signature
|
|
5842
|
+
const sealPubkey = sealData.pubkey;
|
|
5843
|
+
const sealIdBytes = hexToBytes(sealData.id);
|
|
5844
|
+
const sigBytes = hexToBytes(sealData.sig);
|
|
5845
|
+
const pubkeyBytes = hexToBytes(sealPubkey);
|
|
5846
|
+
if (!verify(sigBytes, sealIdBytes, pubkeyBytes)) {
|
|
5847
|
+
throw new Error('Seal signature verification failed');
|
|
5848
|
+
}
|
|
5849
|
+
// Decrypt rumor from seal content
|
|
5850
|
+
const rumorJson = decrypt(sealData.content, recipientKeys.getPrivateKey(), pubkeyBytes);
|
|
5851
|
+
const rumor = JSON.parse(rumorJson);
|
|
5852
|
+
// Extract reply-to event ID if present
|
|
5853
|
+
const replyToEventId = getTagValue(rumor.tags, 'e');
|
|
5854
|
+
return {
|
|
5855
|
+
eventId: giftWrap.id,
|
|
5856
|
+
senderPubkey: sealPubkey,
|
|
5857
|
+
recipientPubkey: recipientKeys.getPublicKeyHex(),
|
|
5858
|
+
content: rumor.content,
|
|
5859
|
+
timestamp: rumor.created_at,
|
|
5860
|
+
kind: rumor.kind,
|
|
5861
|
+
replyToEventId,
|
|
5862
|
+
};
|
|
5863
|
+
}
|
|
5864
|
+
// ========== Helper Functions ==========
|
|
5865
|
+
/**
|
|
5866
|
+
* Create an unsigned rumor (kind 14 or 15).
|
|
5867
|
+
* Note: Rumor uses actual timestamp for correct message ordering.
|
|
5868
|
+
* Only seal and gift wrap use randomized timestamps for privacy.
|
|
5869
|
+
*/
|
|
5870
|
+
function createRumor(senderPubkey, recipientPubkey, content, kind, replyToEventId) {
|
|
5871
|
+
const tags = [['p', recipientPubkey]];
|
|
5872
|
+
if (replyToEventId) {
|
|
5873
|
+
tags.push(['e', replyToEventId, '', 'reply']);
|
|
5874
|
+
}
|
|
5875
|
+
// Use actual timestamp for rumor (inner message) - needed for correct ordering
|
|
5876
|
+
// Privacy is provided by randomized timestamps on seal and gift wrap layers
|
|
5877
|
+
const actualTimestamp = Math.floor(Date.now() / 1000);
|
|
5878
|
+
const rumor = {
|
|
5879
|
+
id: '', // Will be computed
|
|
5880
|
+
pubkey: senderPubkey,
|
|
5881
|
+
created_at: actualTimestamp,
|
|
5882
|
+
kind,
|
|
5883
|
+
tags,
|
|
5884
|
+
content,
|
|
5885
|
+
};
|
|
5886
|
+
// Compute the rumor ID
|
|
5887
|
+
rumor.id = computeRumorId(rumor);
|
|
5888
|
+
return rumor;
|
|
5889
|
+
}
|
|
5890
|
+
/**
|
|
5891
|
+
* Compute the rumor ID from serialized data.
|
|
5892
|
+
* ID = SHA-256([0, pubkey, created_at, kind, tags, content])
|
|
5893
|
+
*/
|
|
5894
|
+
function computeRumorId(rumor) {
|
|
5895
|
+
const serialized = JSON.stringify([
|
|
5896
|
+
0,
|
|
5897
|
+
rumor.pubkey,
|
|
5898
|
+
rumor.created_at,
|
|
5899
|
+
rumor.kind,
|
|
5900
|
+
rumor.tags,
|
|
5901
|
+
rumor.content,
|
|
5902
|
+
]);
|
|
5903
|
+
const hash = sha256(new TextEncoder().encode(serialized));
|
|
5904
|
+
return bytesToHex(hash);
|
|
5905
|
+
}
|
|
5906
|
+
/**
|
|
5907
|
+
* Create a seal (kind 13) that encrypts a rumor.
|
|
5908
|
+
*/
|
|
5909
|
+
function createSeal(senderKeys, recipientPubkeyHex, rumor) {
|
|
5910
|
+
const rumorJson = JSON.stringify(rumor);
|
|
5911
|
+
// Encrypt rumor with NIP-44
|
|
5912
|
+
const recipientPubkey = hexToBytes(recipientPubkeyHex);
|
|
5913
|
+
const encryptedRumor = encrypt(rumorJson, senderKeys.getPrivateKey(), recipientPubkey);
|
|
5914
|
+
// Create seal data
|
|
5915
|
+
const pubkey = senderKeys.getPublicKeyHex();
|
|
5916
|
+
const created_at = randomizeTimestamp();
|
|
5917
|
+
const kind = SEAL;
|
|
5918
|
+
const tags = []; // Seals have no tags
|
|
5919
|
+
const content = encryptedRumor;
|
|
5920
|
+
// Calculate ID
|
|
5921
|
+
const sealId = Event.calculateId(pubkey, created_at, kind, tags, content);
|
|
5922
|
+
// Sign
|
|
5923
|
+
const sealIdBytes = hexToBytes(sealId);
|
|
5924
|
+
const sig = senderKeys.signHex(sealIdBytes);
|
|
5925
|
+
return new Event({
|
|
5926
|
+
id: sealId,
|
|
5927
|
+
pubkey,
|
|
5928
|
+
created_at,
|
|
5929
|
+
kind,
|
|
5930
|
+
tags,
|
|
5931
|
+
content,
|
|
5932
|
+
sig,
|
|
5933
|
+
});
|
|
5934
|
+
}
|
|
5935
|
+
/**
|
|
5936
|
+
* Wrap a seal in a gift wrap (kind 1059) using an ephemeral key.
|
|
5937
|
+
*/
|
|
5938
|
+
function wrapSeal(seal, recipientPubkeyHex) {
|
|
5939
|
+
// Generate ephemeral key for the gift wrap
|
|
5940
|
+
const ephemeralKeys = NostrKeyManager.generate();
|
|
5941
|
+
const sealJson = JSON.stringify(seal.toJSON());
|
|
5942
|
+
// Encrypt seal with NIP-44 using ephemeral key
|
|
5943
|
+
const recipientPubkey = hexToBytes(recipientPubkeyHex);
|
|
5944
|
+
const encryptedSeal = encrypt(sealJson, ephemeralKeys.getPrivateKey(), recipientPubkey);
|
|
5945
|
+
// Create gift wrap data
|
|
5946
|
+
const pubkey = ephemeralKeys.getPublicKeyHex();
|
|
5947
|
+
const created_at = randomizeTimestamp();
|
|
5948
|
+
const kind = GIFT_WRAP;
|
|
5949
|
+
const tags = [['p', recipientPubkeyHex]];
|
|
5950
|
+
const content = encryptedSeal;
|
|
5951
|
+
// Calculate ID
|
|
5952
|
+
const giftWrapId = Event.calculateId(pubkey, created_at, kind, tags, content);
|
|
5953
|
+
// Sign with ephemeral key
|
|
5954
|
+
const giftWrapIdBytes = hexToBytes(giftWrapId);
|
|
5955
|
+
const sig = ephemeralKeys.signHex(giftWrapIdBytes);
|
|
5956
|
+
// Clear ephemeral key from memory
|
|
5957
|
+
ephemeralKeys.clear();
|
|
5958
|
+
return new Event({
|
|
5959
|
+
id: giftWrapId,
|
|
5960
|
+
pubkey,
|
|
5961
|
+
created_at,
|
|
5962
|
+
kind,
|
|
5963
|
+
tags,
|
|
5964
|
+
content,
|
|
5965
|
+
sig,
|
|
5966
|
+
});
|
|
5967
|
+
}
|
|
5968
|
+
/**
|
|
5969
|
+
* Generate a randomized timestamp for privacy (+/- 2 days).
|
|
5970
|
+
*/
|
|
5971
|
+
function randomizeTimestamp() {
|
|
5972
|
+
const now = Math.floor(Date.now() / 1000);
|
|
5973
|
+
const randomOffset = Math.floor(Math.random() * 2 * TIMESTAMP_RANDOMIZATION) - TIMESTAMP_RANDOMIZATION;
|
|
5974
|
+
return now + randomOffset;
|
|
5975
|
+
}
|
|
5976
|
+
/**
|
|
5977
|
+
* Get the first value of a tag by name from a tags array.
|
|
5978
|
+
*/
|
|
5979
|
+
function getTagValue(tags, tagName) {
|
|
5980
|
+
const tag = tags.find((t) => t[0] === tagName);
|
|
5981
|
+
return tag?.[1];
|
|
5982
|
+
}
|
|
5983
|
+
|
|
5984
|
+
var nip17 = /*#__PURE__*/Object.freeze({
|
|
5985
|
+
__proto__: null,
|
|
5986
|
+
createGiftWrap: createGiftWrap,
|
|
5987
|
+
createReadReceipt: createReadReceipt,
|
|
5988
|
+
unwrap: unwrap
|
|
5989
|
+
});
|
|
5990
|
+
|
|
4479
5991
|
/**
|
|
4480
5992
|
* NostrClient - Main entry point for Nostr protocol operations.
|
|
4481
5993
|
* Handles relay connections, event publishing, and subscriptions.
|
|
@@ -4963,6 +6475,51 @@
|
|
|
4963
6475
|
const event = Event.create(this.keyManager, data);
|
|
4964
6476
|
return this.publishEvent(event);
|
|
4965
6477
|
}
|
|
6478
|
+
// ========== NIP-17 Private Messages ==========
|
|
6479
|
+
/**
|
|
6480
|
+
* Send a private message using NIP-17 gift-wrapping.
|
|
6481
|
+
* @param recipientPubkeyHex Recipient's public key (hex)
|
|
6482
|
+
* @param message Message content
|
|
6483
|
+
* @param options Optional message options (reply-to, etc.)
|
|
6484
|
+
* @returns Promise that resolves with the gift wrap event ID
|
|
6485
|
+
*/
|
|
6486
|
+
async sendPrivateMessage(recipientPubkeyHex, message, options) {
|
|
6487
|
+
const giftWrap = createGiftWrap(this.keyManager, recipientPubkeyHex, message, options);
|
|
6488
|
+
return this.publishEvent(giftWrap);
|
|
6489
|
+
}
|
|
6490
|
+
/**
|
|
6491
|
+
* Send a private message to a recipient identified by their nametag.
|
|
6492
|
+
* Resolves the nametag to a pubkey automatically.
|
|
6493
|
+
* @param recipientNametag Recipient's nametag (Unicity ID)
|
|
6494
|
+
* @param message Message content
|
|
6495
|
+
* @param options Optional message options (reply-to, etc.)
|
|
6496
|
+
* @returns Promise that resolves with the gift wrap event ID
|
|
6497
|
+
*/
|
|
6498
|
+
async sendPrivateMessageToNametag(recipientNametag, message, options) {
|
|
6499
|
+
const pubkey = await this.queryPubkeyByNametag(recipientNametag);
|
|
6500
|
+
if (!pubkey) {
|
|
6501
|
+
throw new Error(`Nametag not found: ${recipientNametag}`);
|
|
6502
|
+
}
|
|
6503
|
+
return this.sendPrivateMessage(pubkey, message, options);
|
|
6504
|
+
}
|
|
6505
|
+
/**
|
|
6506
|
+
* Send a read receipt for a message using NIP-17 gift-wrapping.
|
|
6507
|
+
* @param recipientPubkeyHex Recipient (original sender) public key
|
|
6508
|
+
* @param messageEventId Event ID of the message being acknowledged
|
|
6509
|
+
* @returns Promise that resolves with the gift wrap event ID
|
|
6510
|
+
*/
|
|
6511
|
+
async sendReadReceipt(recipientPubkeyHex, messageEventId) {
|
|
6512
|
+
const giftWrap = createReadReceipt(this.keyManager, recipientPubkeyHex, messageEventId);
|
|
6513
|
+
return this.publishEvent(giftWrap);
|
|
6514
|
+
}
|
|
6515
|
+
/**
|
|
6516
|
+
* Unwrap a gift-wrapped private message.
|
|
6517
|
+
* @param giftWrap Gift wrap event (kind 1059)
|
|
6518
|
+
* @returns Parsed private message
|
|
6519
|
+
*/
|
|
6520
|
+
unwrapPrivateMessage(giftWrap) {
|
|
6521
|
+
return unwrap(giftWrap, this.keyManager);
|
|
6522
|
+
}
|
|
4966
6523
|
}
|
|
4967
6524
|
|
|
4968
6525
|
/**
|
|
@@ -7974,7 +9531,7 @@
|
|
|
7974
9531
|
return false;
|
|
7975
9532
|
}
|
|
7976
9533
|
// Count non-digit characters (excluding common phone number chars)
|
|
7977
|
-
const cleanedLength = str.replace(/[\s
|
|
9534
|
+
const cleanedLength = str.replace(/[\s\-().]/g, '').length;
|
|
7978
9535
|
const digitRatio = digitCount / cleanedLength;
|
|
7979
9536
|
return digitRatio > 0.5;
|
|
7980
9537
|
}
|
|
@@ -8431,7 +9988,9 @@
|
|
|
8431
9988
|
*/
|
|
8432
9989
|
function generateRequestId() {
|
|
8433
9990
|
const bytes = new Uint8Array(4);
|
|
9991
|
+
// eslint-disable-next-line no-undef
|
|
8434
9992
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
9993
|
+
// eslint-disable-next-line no-undef
|
|
8435
9994
|
crypto.getRandomValues(bytes);
|
|
8436
9995
|
}
|
|
8437
9996
|
else {
|
|
@@ -8653,10 +10212,27 @@
|
|
|
8653
10212
|
parsePaymentRequest: parsePaymentRequest
|
|
8654
10213
|
});
|
|
8655
10214
|
|
|
10215
|
+
/**
|
|
10216
|
+
* NIP-17 Messaging Types - Private Direct Messages
|
|
10217
|
+
*/
|
|
10218
|
+
/**
|
|
10219
|
+
* Check if a message is a chat message (kind 14).
|
|
10220
|
+
*/
|
|
10221
|
+
function isChatMessage(message) {
|
|
10222
|
+
return message.kind === 14;
|
|
10223
|
+
}
|
|
10224
|
+
/**
|
|
10225
|
+
* Check if a message is a read receipt (kind 15).
|
|
10226
|
+
*/
|
|
10227
|
+
function isReadReceipt(message) {
|
|
10228
|
+
return message.kind === 15;
|
|
10229
|
+
}
|
|
10230
|
+
|
|
8656
10231
|
exports.AGENT_LOCATION = AGENT_LOCATION;
|
|
8657
10232
|
exports.AGENT_PROFILE = AGENT_PROFILE;
|
|
8658
10233
|
exports.APP_DATA = APP_DATA;
|
|
8659
10234
|
exports.Bech32 = bech32;
|
|
10235
|
+
exports.CHAT_MESSAGE = CHAT_MESSAGE;
|
|
8660
10236
|
exports.CLOSED = CLOSED;
|
|
8661
10237
|
exports.CLOSING = CLOSING;
|
|
8662
10238
|
exports.CONNECTING = CONNECTING;
|
|
@@ -8671,6 +10247,8 @@
|
|
|
8671
10247
|
exports.FilterBuilder = FilterBuilder;
|
|
8672
10248
|
exports.GIFT_WRAP = GIFT_WRAP;
|
|
8673
10249
|
exports.NIP04 = nip04;
|
|
10250
|
+
exports.NIP17 = nip17;
|
|
10251
|
+
exports.NIP44 = nip44;
|
|
8674
10252
|
exports.NametagBinding = NametagBinding;
|
|
8675
10253
|
exports.NametagUtils = NametagUtils;
|
|
8676
10254
|
exports.NostrClient = NostrClient;
|
|
@@ -8680,38 +10258,38 @@
|
|
|
8680
10258
|
exports.PROFILE = PROFILE;
|
|
8681
10259
|
exports.PaymentRequestProtocol = PaymentRequestProtocol;
|
|
8682
10260
|
exports.REACTION = REACTION;
|
|
10261
|
+
exports.READ_RECEIPT = READ_RECEIPT;
|
|
8683
10262
|
exports.RECOMMEND_RELAY = RECOMMEND_RELAY;
|
|
8684
10263
|
exports.RELAY_LIST = RELAY_LIST;
|
|
10264
|
+
exports.SEAL = SEAL;
|
|
8685
10265
|
exports.SchnorrSigner = schnorr;
|
|
8686
10266
|
exports.TEXT_NOTE = TEXT_NOTE;
|
|
8687
10267
|
exports.TOKEN_TRANSFER = TOKEN_TRANSFER;
|
|
8688
10268
|
exports.TokenTransferProtocol = TokenTransferProtocol;
|
|
8689
10269
|
exports.areSameNametag = areSameNametag;
|
|
8690
10270
|
exports.createBindingEvent = createBindingEvent;
|
|
10271
|
+
exports.createGiftWrap = createGiftWrap;
|
|
8691
10272
|
exports.createNametagToPubkeyFilter = createNametagToPubkeyFilter;
|
|
8692
10273
|
exports.createPubkeyToNametagFilter = createPubkeyToNametagFilter;
|
|
10274
|
+
exports.createReadReceipt = createReadReceipt;
|
|
8693
10275
|
exports.createWebSocket = createWebSocket;
|
|
8694
10276
|
exports.decode = decode;
|
|
8695
10277
|
exports.decodeNpub = decodeNpub;
|
|
8696
10278
|
exports.decodeNsec = decodeNsec;
|
|
8697
|
-
exports.decrypt = decrypt;
|
|
8698
|
-
exports.decryptHex = decryptHex;
|
|
8699
|
-
exports.deriveSharedSecret = deriveSharedSecret;
|
|
8700
|
-
exports.deriveSharedSecretHex = deriveSharedSecretHex;
|
|
8701
10279
|
exports.encode = encode;
|
|
8702
10280
|
exports.encodeNpub = encodeNpub;
|
|
8703
10281
|
exports.encodeNsec = encodeNsec;
|
|
8704
|
-
exports.encrypt = encrypt;
|
|
8705
|
-
exports.encryptHex = encryptHex;
|
|
8706
10282
|
exports.extractMessageData = extractMessageData;
|
|
8707
10283
|
exports.formatForDisplay = formatForDisplay;
|
|
8708
10284
|
exports.getName = getName;
|
|
8709
10285
|
exports.getPublicKey = getPublicKey;
|
|
8710
10286
|
exports.getPublicKeyHex = getPublicKeyHex;
|
|
8711
10287
|
exports.hashNametag = hashNametag;
|
|
10288
|
+
exports.isChatMessage = isChatMessage;
|
|
8712
10289
|
exports.isEphemeral = isEphemeral;
|
|
8713
10290
|
exports.isParameterizedReplaceable = isParameterizedReplaceable;
|
|
8714
10291
|
exports.isPhoneNumber = isPhoneNumber;
|
|
10292
|
+
exports.isReadReceipt = isReadReceipt;
|
|
8715
10293
|
exports.isReplaceable = isReplaceable;
|
|
8716
10294
|
exports.isValidBindingEvent = isValidBindingEvent;
|
|
8717
10295
|
exports.normalizeNametag = normalizeNametag;
|
|
@@ -8719,6 +10297,7 @@
|
|
|
8719
10297
|
exports.parseNametagHashFromEvent = parseNametagHashFromEvent;
|
|
8720
10298
|
exports.sign = sign;
|
|
8721
10299
|
exports.signHex = signHex;
|
|
10300
|
+
exports.unwrap = unwrap;
|
|
8722
10301
|
exports.verify = verify;
|
|
8723
10302
|
exports.verifyHex = verifyHex;
|
|
8724
10303
|
|