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