@bsv/sdk 1.9.31 → 1.10.2
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/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/Peer.js +68 -48
- package/dist/cjs/src/auth/Peer.js.map +1 -1
- package/dist/cjs/src/identity/IdentityClient.js +124 -20
- package/dist/cjs/src/identity/IdentityClient.js.map +1 -1
- package/dist/cjs/src/primitives/BigNumber.js +28 -54
- package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
- package/dist/cjs/src/primitives/ECDSA.js +36 -1
- package/dist/cjs/src/primitives/ECDSA.js.map +1 -1
- package/dist/cjs/src/primitives/ReductionContext.js +35 -46
- package/dist/cjs/src/primitives/ReductionContext.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/Peer.js +68 -48
- package/dist/esm/src/auth/Peer.js.map +1 -1
- package/dist/esm/src/identity/IdentityClient.js +124 -20
- package/dist/esm/src/identity/IdentityClient.js.map +1 -1
- package/dist/esm/src/primitives/BigNumber.js +28 -54
- package/dist/esm/src/primitives/BigNumber.js.map +1 -1
- package/dist/esm/src/primitives/ECDSA.js +36 -1
- package/dist/esm/src/primitives/ECDSA.js.map +1 -1
- package/dist/esm/src/primitives/ReductionContext.js +35 -46
- package/dist/esm/src/primitives/ReductionContext.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/Peer.d.ts.map +1 -1
- package/dist/types/src/auth/types.d.ts +2 -0
- package/dist/types/src/auth/types.d.ts.map +1 -1
- package/dist/types/src/identity/IdentityClient.d.ts +8 -0
- package/dist/types/src/identity/IdentityClient.d.ts.map +1 -1
- package/dist/types/src/primitives/BigNumber.d.ts +8 -0
- package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
- package/dist/types/src/primitives/ECDSA.d.ts +24 -0
- package/dist/types/src/primitives/ECDSA.d.ts.map +1 -1
- package/dist/types/src/primitives/ReductionContext.d.ts +9 -0
- package/dist/types/src/primitives/ReductionContext.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/index.md +15 -1
- package/docs/reference/auth.md +2 -0
- package/docs/reference/messages.md +0 -24
- package/docs/reference/primitives.md +91 -31
- package/package.json +1 -1
- package/src/auth/Peer.ts +122 -57
- package/src/auth/__tests/Peer.test.ts +166 -257
- package/src/auth/types.ts +2 -0
- package/src/identity/IdentityClient.ts +153 -29
- package/src/identity/__tests/IdentityClient.test.ts +289 -1
- package/src/primitives/BigNumber.ts +27 -31
- package/src/primitives/ECDSA.ts +41 -2
- package/src/primitives/ReductionContext.ts +44 -48
- package/src/primitives/__tests/ECDSA.test.ts +16 -0
package/docs/index.md
CHANGED
|
@@ -59,6 +59,7 @@ Finally, you can deep dive into the details of the interface and types in the re
|
|
|
59
59
|
- [Storage](./reference/storage.md)
|
|
60
60
|
- [KV Store](./reference/kvstore.md)
|
|
61
61
|
- [Messages](./reference/messages.md)
|
|
62
|
+
- Please note [*Security Considerations*](#security-considerations-for-encrypted-messages).
|
|
62
63
|
- [TOTP](./reference/totp.md)
|
|
63
64
|
- [Compatibility](./reference/compat.md)
|
|
64
65
|
|
|
@@ -74,4 +75,17 @@ Finally, you can deep dive into the details of the interface and types in the re
|
|
|
74
75
|
|
|
75
76
|
## Performance Reports
|
|
76
77
|
|
|
77
|
-
- [Benchmarks](./performance.md)
|
|
78
|
+
- [Benchmarks](./performance.md)
|
|
79
|
+
|
|
80
|
+
## Security Considerations for Encrypted Messages
|
|
81
|
+
|
|
82
|
+
The encrypted message protocol implemented in this SDK derives per-message encryption keys deterministically from the parties’ long-term keys and a caller-supplied invoice number (BRC-42 style derivation).
|
|
83
|
+
|
|
84
|
+
This construction does not provide the guarantees of a standard authenticated key exchange (AKE). In particular:
|
|
85
|
+
|
|
86
|
+
No forward secrecy: Compromise of a long-term private key compromises all past and future messages derived from it.
|
|
87
|
+
No replay protection: Messages encrypted under the same invoice number and key pair can be replayed.
|
|
88
|
+
Potential identity misbinding: Public keys alone do not guarantee peer identity without additional authentication or identity verification.
|
|
89
|
+
This protocol is intended for lightweight, deterministic messaging between parties that already trust each other’s long-term public keys. It SHOULD NOT be used for high-security or high-value communications without additional protocol-layer protections.
|
|
90
|
+
|
|
91
|
+
Applications requiring strong authentication, replay protection, or forward secrecy should use a formally analyzed protocol such as X3DH, Noise, or SIGMA.
|
package/docs/reference/auth.md
CHANGED
|
@@ -2,30 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Variables](#variables)
|
|
4
4
|
|
|
5
|
-
## Security Considerations for Encrypted Messages
|
|
6
|
-
|
|
7
|
-
The encrypted message protocol implemented in this SDK derives per-message
|
|
8
|
-
encryption keys deterministically from the parties’ long-term keys and a
|
|
9
|
-
caller-supplied invoice number (BRC-42 style derivation).
|
|
10
|
-
|
|
11
|
-
This construction does **not** provide the guarantees of a standard
|
|
12
|
-
authenticated key exchange (AKE). In particular:
|
|
13
|
-
|
|
14
|
-
- **No forward secrecy**: Compromise of a long-term private key compromises
|
|
15
|
-
all past and future messages derived from it.
|
|
16
|
-
- **No replay protection**: Messages encrypted under the same invoice number
|
|
17
|
-
and key pair can be replayed.
|
|
18
|
-
- **Potential identity misbinding**: Public keys alone do not guarantee peer
|
|
19
|
-
identity without additional authentication or identity verification.
|
|
20
|
-
|
|
21
|
-
This protocol is intended for lightweight, deterministic messaging between
|
|
22
|
-
parties that already trust each other’s long-term public keys. It SHOULD NOT
|
|
23
|
-
be used for high-security or high-value communications without additional
|
|
24
|
-
protocol-layer protections.
|
|
25
|
-
|
|
26
|
-
Applications requiring strong authentication, replay protection, or forward
|
|
27
|
-
secrecy should use a formally analyzed protocol such as X3DH, Noise, or SIGMA.
|
|
28
|
-
|
|
29
5
|
## Interfaces
|
|
30
6
|
|
|
31
7
|
## Classes
|
|
@@ -299,6 +299,13 @@ console.log(BigNumber.wordSize); // output: 26
|
|
|
299
299
|
Compute the multiplicative inverse of the current BigNumber in the modulus field specified by `p`.
|
|
300
300
|
The multiplicative inverse is a number which when multiplied with the current BigNumber gives '1' in the modulus field.
|
|
301
301
|
|
|
302
|
+
SECURITY NOTE:
|
|
303
|
+
This implementation avoids variable-time extended Euclidean algorithms
|
|
304
|
+
to reduce timing side-channel leakage. However, JavaScript BigInt arithmetic
|
|
305
|
+
does not provide constant-time guarantees. This implementation is suitable
|
|
306
|
+
for browser and single-tenant environments but is not hardened against
|
|
307
|
+
high-resolution timing attacks in shared CPU contexts.
|
|
308
|
+
|
|
302
309
|
```ts
|
|
303
310
|
_invmp(p: BigNumber): BigNumber
|
|
304
311
|
```
|
|
@@ -1786,6 +1793,7 @@ export default class Point extends BasePoint {
|
|
|
1786
1793
|
getX(): BigNumber
|
|
1787
1794
|
getY(): BigNumber
|
|
1788
1795
|
mul(k: BigNumber | number | number[] | string): Point
|
|
1796
|
+
mulCT(k: BigNumber | number | number[] | string): Point
|
|
1789
1797
|
mulAdd(k1: BigNumber, p2: Point, k2: BigNumber): Point
|
|
1790
1798
|
jmulAdd(k1: BigNumber, p2: Point, k2: BigNumber): JPoint
|
|
1791
1799
|
eq(p: Point): boolean
|
|
@@ -2508,6 +2516,32 @@ Returns
|
|
|
2508
2516
|
|
|
2509
2517
|
#### Method deriveChild
|
|
2510
2518
|
|
|
2519
|
+
SECURITY NOTE – DETERMINISTIC CHILD KEY DERIVATION
|
|
2520
|
+
|
|
2521
|
+
This method derives child private keys deterministically from the caller’s
|
|
2522
|
+
long-term private key, the counterparty’s public key, and a caller-supplied
|
|
2523
|
+
invoice number using HMAC over an ECDH shared secret (BRC-42 style derivation).
|
|
2524
|
+
|
|
2525
|
+
This construction does NOT implement a formally authenticated key exchange
|
|
2526
|
+
(AKE) and does NOT provide the following security properties:
|
|
2527
|
+
|
|
2528
|
+
- Forward secrecy: Compromise of a long-term private key compromises all
|
|
2529
|
+
past and future child keys derived from it.
|
|
2530
|
+
- Replay protection: Child keys are deterministic for a given invoice
|
|
2531
|
+
number and key pair; previously observed messages can be replayed.
|
|
2532
|
+
- Explicit authentication / identity binding: Possession of a public key
|
|
2533
|
+
alone does not guarantee the intended peer identity, enabling potential
|
|
2534
|
+
identity misbinding attacks if higher-level identity verification is absent.
|
|
2535
|
+
|
|
2536
|
+
This derivation is intended for lightweight, deterministic key hierarchies
|
|
2537
|
+
where both parties already possess and trust each other’s long-term public
|
|
2538
|
+
keys. It SHOULD NOT be used as a drop-in replacement for a standard
|
|
2539
|
+
authenticated key exchange (e.g. X3DH, Noise, or SIGMA) in high-security or
|
|
2540
|
+
high-value contexts.
|
|
2541
|
+
|
|
2542
|
+
Any future protocol providing forward secrecy, replay protection, or strong
|
|
2543
|
+
peer authentication will require a versioned, breaking change.
|
|
2544
|
+
|
|
2511
2545
|
Derives a child key with BRC-42.
|
|
2512
2546
|
|
|
2513
2547
|
```ts
|
|
@@ -3309,6 +3343,14 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
3309
3343
|
---
|
|
3310
3344
|
### Class: ReductionContext
|
|
3311
3345
|
|
|
3346
|
+
SECURITY NOTE:
|
|
3347
|
+
This reduction context avoids obvious variable-time constructs (such as
|
|
3348
|
+
sliding-window exponentiation and conditional modular reduction) to reduce
|
|
3349
|
+
timing side-channel leakage. However, JavaScript BigInt arithmetic does not
|
|
3350
|
+
provide constant-time guarantees. These mitigations improve resistance to
|
|
3351
|
+
coarse timing attacks but do not make the implementation suitable for
|
|
3352
|
+
hostile multi-tenant or shared-CPU environments.
|
|
3353
|
+
|
|
3312
3354
|
A base reduction engine that provides several arithmetic operations over
|
|
3313
3355
|
big numbers under a modulus context. It's particularly suitable for
|
|
3314
3356
|
calculations required in cryptography algorithms and encoding schemas.
|
|
@@ -4961,14 +5003,14 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
4961
5003
|
|
|
4962
5004
|
| | |
|
|
4963
5005
|
| --- | --- |
|
|
4964
|
-
| [AES](#function-aes) | [
|
|
4965
|
-
| [AESGCM](#function-aesgcm) | [
|
|
4966
|
-
| [AESGCMDecrypt](#function-aesgcmdecrypt) | [
|
|
4967
|
-
| [assertValidHex](#function-assertvalidhex) | [
|
|
4968
|
-
| [base64ToArray](#function-base64toarray) | [
|
|
5006
|
+
| [AES](#function-aes) | [normalizeHex](#function-normalizehex) |
|
|
5007
|
+
| [AESGCM](#function-aesgcm) | [pbkdf2](#function-pbkdf2) |
|
|
5008
|
+
| [AESGCMDecrypt](#function-aesgcmdecrypt) | [realHtonl](#function-realhtonl) |
|
|
5009
|
+
| [assertValidHex](#function-assertvalidhex) | [red](#function-red) |
|
|
5010
|
+
| [base64ToArray](#function-base64toarray) | [swapBytes32](#function-swapbytes32) |
|
|
5011
|
+
| [constantTimeEquals](#function-constanttimeequals) | [toArray](#function-toarray) |
|
|
4969
5012
|
| [ghash](#function-ghash) | [toBase64](#function-tobase64) |
|
|
4970
5013
|
| [htonl](#function-htonl) | [verifyNotNull](#function-verifynotnull) |
|
|
4971
|
-
| [normalizeHex](#function-normalizehex) | |
|
|
4972
5014
|
|
|
4973
5015
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
4974
5016
|
|
|
@@ -5067,6 +5109,15 @@ export function base64ToArray(msg: string): number[]
|
|
|
5067
5109
|
|
|
5068
5110
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
5069
5111
|
|
|
5112
|
+
---
|
|
5113
|
+
### Function: constantTimeEquals
|
|
5114
|
+
|
|
5115
|
+
```ts
|
|
5116
|
+
export function constantTimeEquals(a: Uint8Array | number[], b: Uint8Array | number[]): boolean
|
|
5117
|
+
```
|
|
5118
|
+
|
|
5119
|
+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
5120
|
+
|
|
5070
5121
|
---
|
|
5071
5122
|
### Function: ghash
|
|
5072
5123
|
|
|
@@ -5736,7 +5787,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
5736
5787
|
```ts
|
|
5737
5788
|
incrementLeastSignificantThirtyTwoBits = function (block: Bytes): Bytes {
|
|
5738
5789
|
const result = block.slice();
|
|
5739
|
-
for (let i = 15; i
|
|
5790
|
+
for (let i = 15; i > 11; i--) {
|
|
5740
5791
|
result[i] = (result[i] + 1) & 255;
|
|
5741
5792
|
if (result[i] !== 0) {
|
|
5742
5793
|
break;
|
|
@@ -5914,16 +5965,18 @@ multiply = function (block0: Bytes, block1: Bytes): Bytes {
|
|
|
5914
5965
|
const v = block1.slice();
|
|
5915
5966
|
const z = createZeroBlock(16);
|
|
5916
5967
|
for (let i = 0; i < 16; i++) {
|
|
5968
|
+
const b = block0[i];
|
|
5917
5969
|
for (let j = 7; j >= 0; j--) {
|
|
5918
|
-
|
|
5919
|
-
|
|
5970
|
+
const bit = (b >> j) & 1;
|
|
5971
|
+
const mask = -bit & 255;
|
|
5972
|
+
for (let k = 0; k < 16; k++) {
|
|
5973
|
+
z[k] ^= v[k] & mask;
|
|
5920
5974
|
}
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
rightShift(v);
|
|
5975
|
+
const lsb = v[15] & 1;
|
|
5976
|
+
const rmask = -lsb & 255;
|
|
5977
|
+
rightShift(v);
|
|
5978
|
+
for (let k = 0; k < 16; k++) {
|
|
5979
|
+
v[k] ^= R[k] & rmask;
|
|
5927
5980
|
}
|
|
5928
5981
|
}
|
|
5929
5982
|
}
|
|
@@ -6100,9 +6153,13 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
6100
6153
|
|
|
6101
6154
|
```ts
|
|
6102
6155
|
sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: BigNumber | ((iter: number) => BigNumber)): Signature => {
|
|
6156
|
+
const nBitLength = curve.n.bitLength();
|
|
6157
|
+
if (msg.bitLength() > nBitLength) {
|
|
6158
|
+
throw new Error(`ECDSA message is too large: expected <= ${nBitLength} bits. Callers must hash messages before signing.`);
|
|
6159
|
+
}
|
|
6103
6160
|
msg = truncateToN(msg);
|
|
6104
|
-
const msgBig =
|
|
6105
|
-
const keyBig =
|
|
6161
|
+
const msgBig = bnToBigInt(msg);
|
|
6162
|
+
const keyBig = bnToBigInt(key);
|
|
6106
6163
|
const bkey = key.toArray("be", bytes);
|
|
6107
6164
|
const nonce = msg.toArray("be", bytes);
|
|
6108
6165
|
const drbg = new DRBG(bkey, nonce);
|
|
@@ -6112,26 +6169,24 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
|
|
|
6112
6169
|
: BigNumber.isBN(customK)
|
|
6113
6170
|
? customK
|
|
6114
6171
|
: new BigNumber(drbg.generate(bytes), 16);
|
|
6115
|
-
if (kBN == null)
|
|
6172
|
+
if (kBN == null) {
|
|
6116
6173
|
throw new Error("k is undefined");
|
|
6174
|
+
}
|
|
6117
6175
|
kBN = truncateToN(kBN, true);
|
|
6118
6176
|
if (kBN.cmpn(1) < 0 || kBN.cmp(ns1) > 0) {
|
|
6119
6177
|
if (BigNumber.isBN(customK)) {
|
|
6120
|
-
throw new Error("Invalid fixed custom K value (must be >1 and <N
|
|
6178
|
+
throw new Error("Invalid fixed custom K value (must be >1 and <N-1)");
|
|
6121
6179
|
}
|
|
6122
6180
|
continue;
|
|
6123
6181
|
}
|
|
6124
|
-
const
|
|
6125
|
-
|
|
6126
|
-
if (R.Z === 0n) {
|
|
6182
|
+
const R = curve.g.mulCT(kBN);
|
|
6183
|
+
if (R.isInfinity()) {
|
|
6127
6184
|
if (BigNumber.isBN(customK)) {
|
|
6128
6185
|
throw new Error("Invalid fixed custom K value (k\u00B7G at infinity)");
|
|
6129
6186
|
}
|
|
6130
6187
|
continue;
|
|
6131
6188
|
}
|
|
6132
|
-
const
|
|
6133
|
-
const zInv2 = biModMul(zInv, zInv);
|
|
6134
|
-
const xAff = biModMul(R.X, zInv2);
|
|
6189
|
+
const xAff = BigInt("0x" + R.getX().toString(16));
|
|
6135
6190
|
const rBig = modN(xAff);
|
|
6136
6191
|
if (rBig === 0n) {
|
|
6137
6192
|
if (BigNumber.isBN(customK)) {
|
|
@@ -6139,6 +6194,7 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
|
|
|
6139
6194
|
}
|
|
6140
6195
|
continue;
|
|
6141
6196
|
}
|
|
6197
|
+
const kBig = BigInt("0x" + kBN.toString(16));
|
|
6142
6198
|
const kInv = modInvN(kBig);
|
|
6143
6199
|
const rTimesKey = modMulN(rBig, keyBig);
|
|
6144
6200
|
const sum = modN(msgBig + rTimesKey);
|
|
@@ -6159,7 +6215,7 @@ sign = (msg: BigNumber, key: BigNumber, forceLowS: boolean = false, customK?: Bi
|
|
|
6159
6215
|
}
|
|
6160
6216
|
```
|
|
6161
6217
|
|
|
6162
|
-
See also: [BigNumber](./primitives.md#class-bignumber), [DRBG](./primitives.md#class-drbg), [
|
|
6218
|
+
See also: [BigNumber](./primitives.md#class-bignumber), [DRBG](./primitives.md#class-drbg), [N_BIGINT](./primitives.md#variable-n_bigint), [Signature](./primitives.md#class-signature), [modInvN](./primitives.md#variable-modinvn), [modMulN](./primitives.md#variable-modmuln), [modN](./primitives.md#variable-modn), [toArray](./primitives.md#variable-toarray)
|
|
6163
6219
|
|
|
6164
6220
|
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
|
|
6165
6221
|
|
|
@@ -6350,17 +6406,21 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
|
|
|
6350
6406
|
|
|
6351
6407
|
```ts
|
|
6352
6408
|
verify = (msg: BigNumber, sig: Signature, key: Point): boolean => {
|
|
6353
|
-
const
|
|
6409
|
+
const nBitLength = curve.n.bitLength();
|
|
6410
|
+
if (msg.bitLength() > nBitLength) {
|
|
6411
|
+
return false;
|
|
6412
|
+
}
|
|
6413
|
+
const hash = bnToBigInt(msg);
|
|
6354
6414
|
if ((key.x == null) || (key.y == null)) {
|
|
6355
6415
|
throw new Error("Invalid public key: missing coordinates.");
|
|
6356
6416
|
}
|
|
6357
6417
|
const publicKey = {
|
|
6358
|
-
x:
|
|
6359
|
-
y:
|
|
6418
|
+
x: bnToBigInt(key.x),
|
|
6419
|
+
y: bnToBigInt(key.y)
|
|
6360
6420
|
};
|
|
6361
6421
|
const signature = {
|
|
6362
|
-
r:
|
|
6363
|
-
s:
|
|
6422
|
+
r: bnToBigInt(sig.r),
|
|
6423
|
+
s: bnToBigInt(sig.s)
|
|
6364
6424
|
};
|
|
6365
6425
|
const { r, s } = signature;
|
|
6366
6426
|
const z = hash;
|
package/package.json
CHANGED
package/src/auth/Peer.ts
CHANGED
|
@@ -127,7 +127,17 @@ export class Peer {
|
|
|
127
127
|
|
|
128
128
|
const peerSession = await this.getAuthenticatedSession(identityKey, maxWaitTime)
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
if (peerSession.peerIdentityKey == null) {
|
|
131
|
+
throw new Error('Peer identity is not established')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (peerSession.certificatesRequired === true &&
|
|
135
|
+
peerSession.certificatesValidated !== true) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
'Cannot send general message before certificate validation is complete'
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
const requestNonce = Utils.toBase64(Random(32))
|
|
132
142
|
const { signature } = await this.wallet.createSignature({
|
|
133
143
|
data: message,
|
|
@@ -339,15 +349,19 @@ export class Peer {
|
|
|
339
349
|
identityKey?: string,
|
|
340
350
|
maxWaitTime = 10000
|
|
341
351
|
): Promise<string> {
|
|
342
|
-
const sessionNonce = await createNonce(this.wallet, undefined, this.originator)
|
|
352
|
+
const sessionNonce = await createNonce(this.wallet, undefined, this.originator)
|
|
343
353
|
|
|
344
|
-
// Create the preliminary session (not yet authenticated)
|
|
345
354
|
const now = Date.now()
|
|
355
|
+
const certificatesRequired =
|
|
356
|
+
this.certificatesToRequest.certifiers.length > 0
|
|
357
|
+
|
|
346
358
|
this.sessionManager.addSession({
|
|
347
359
|
isAuthenticated: false,
|
|
348
360
|
sessionNonce,
|
|
349
361
|
peerIdentityKey: identityKey,
|
|
350
|
-
lastUpdate: now
|
|
362
|
+
lastUpdate: now,
|
|
363
|
+
certificatesRequired,
|
|
364
|
+
certificatesValidated: !certificatesRequired
|
|
351
365
|
})
|
|
352
366
|
|
|
353
367
|
const initialRequest: AuthMessage = {
|
|
@@ -448,28 +462,33 @@ export class Peer {
|
|
|
448
462
|
)
|
|
449
463
|
}
|
|
450
464
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
message.
|
|
471
|
-
|
|
472
|
-
|
|
465
|
+
try {
|
|
466
|
+
switch (message.messageType) {
|
|
467
|
+
case 'initialRequest':
|
|
468
|
+
await this.processInitialRequest(message)
|
|
469
|
+
break
|
|
470
|
+
case 'initialResponse':
|
|
471
|
+
await this.processInitialResponse(message)
|
|
472
|
+
break
|
|
473
|
+
case 'certificateRequest':
|
|
474
|
+
await this.processCertificateRequest(message)
|
|
475
|
+
break
|
|
476
|
+
case 'certificateResponse':
|
|
477
|
+
await this.processCertificateResponse(message)
|
|
478
|
+
break
|
|
479
|
+
case 'general':
|
|
480
|
+
await this.processGeneralMessage(message)
|
|
481
|
+
break
|
|
482
|
+
default:
|
|
483
|
+
throw new Error(
|
|
484
|
+
`Unknown message type of ${String(message.messageType)} from ${String(
|
|
485
|
+
message.identityKey
|
|
486
|
+
)}`
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
// Swallow protocol violations so transport does not crash the process
|
|
491
|
+
// (Message is intentionally rejected)
|
|
473
492
|
}
|
|
474
493
|
}
|
|
475
494
|
|
|
@@ -487,33 +506,35 @@ export class Peer {
|
|
|
487
506
|
throw new Error('Missing required fields in initialRequest message.')
|
|
488
507
|
}
|
|
489
508
|
|
|
490
|
-
// Create a new sessionNonce for our side
|
|
491
509
|
const sessionNonce = await createNonce(this.wallet, undefined, this.originator)
|
|
492
510
|
const now = Date.now()
|
|
493
511
|
|
|
494
|
-
|
|
512
|
+
const certificatesRequired =
|
|
513
|
+
Array.isArray(this.certificatesToRequest?.certifiers) &&
|
|
514
|
+
this.certificatesToRequest.certifiers.length > 0
|
|
515
|
+
|
|
495
516
|
this.sessionManager.addSession({
|
|
496
517
|
isAuthenticated: true,
|
|
497
518
|
sessionNonce,
|
|
498
519
|
peerNonce: message.initialNonce,
|
|
499
520
|
peerIdentityKey: message.identityKey,
|
|
500
|
-
lastUpdate: now
|
|
521
|
+
lastUpdate: now,
|
|
522
|
+
certificatesRequired,
|
|
523
|
+
certificatesValidated: !certificatesRequired
|
|
501
524
|
})
|
|
502
525
|
|
|
503
|
-
// Possibly handle the peer's requested certs
|
|
504
526
|
let certificatesToInclude: VerifiableCertificate[] | undefined
|
|
527
|
+
|
|
528
|
+
// Handle THEIR certificate request (if any)
|
|
505
529
|
if (
|
|
506
|
-
(message.requestedCertificates
|
|
507
|
-
Array.isArray(message.requestedCertificates.certifiers) &&
|
|
530
|
+
Array.isArray(message.requestedCertificates?.certifiers) &&
|
|
508
531
|
message.requestedCertificates.certifiers.length > 0
|
|
509
532
|
) {
|
|
510
533
|
if (this.onCertificateRequestReceivedCallbacks.size > 0) {
|
|
511
|
-
// Let the application handle it
|
|
512
534
|
this.onCertificateRequestReceivedCallbacks.forEach(cb => {
|
|
513
535
|
cb(message.identityKey, message.requestedCertificates as RequestedCertificateSet)
|
|
514
536
|
})
|
|
515
537
|
} else {
|
|
516
|
-
// Attempt to find automatically
|
|
517
538
|
certificatesToInclude = await getVerifiableCertificates(
|
|
518
539
|
this.wallet,
|
|
519
540
|
message.requestedCertificates,
|
|
@@ -523,7 +544,6 @@ export class Peer {
|
|
|
523
544
|
}
|
|
524
545
|
}
|
|
525
546
|
|
|
526
|
-
// Create signature
|
|
527
547
|
const { signature } = await this.wallet.createSignature({
|
|
528
548
|
data: Peer.base64ToBytes(message.initialNonce + sessionNonce),
|
|
529
549
|
protocolID: [2, 'auth message signature'],
|
|
@@ -542,12 +562,10 @@ export class Peer {
|
|
|
542
562
|
signature
|
|
543
563
|
}
|
|
544
564
|
|
|
545
|
-
// If we haven't interacted with a peer yet, store this identity as "lastInteracted"
|
|
546
565
|
if (this.lastInteractedWithPeer === undefined) {
|
|
547
566
|
this.lastInteractedWithPeer = message.identityKey
|
|
548
567
|
}
|
|
549
568
|
|
|
550
|
-
// Send the response
|
|
551
569
|
await this.transport.send(initialResponseMessage)
|
|
552
570
|
}
|
|
553
571
|
|
|
@@ -559,23 +577,27 @@ export class Peer {
|
|
|
559
577
|
* @throws Will throw an error if nonce or signature verification fails.
|
|
560
578
|
*/
|
|
561
579
|
private async processInitialResponse (message: AuthMessage): Promise<void> {
|
|
562
|
-
const validNonce = await verifyNonce(
|
|
580
|
+
const validNonce = await verifyNonce(
|
|
581
|
+
message.yourNonce as string,
|
|
582
|
+
this.wallet,
|
|
583
|
+
undefined,
|
|
584
|
+
this.originator
|
|
585
|
+
)
|
|
563
586
|
if (!validNonce) {
|
|
564
587
|
throw new Error(
|
|
565
588
|
`Initial response nonce verification failed from peer: ${message.identityKey}`
|
|
566
589
|
)
|
|
567
590
|
}
|
|
568
591
|
|
|
569
|
-
// This is the session we previously created by calling initiateHandshake
|
|
570
592
|
const peerSession = this.sessionManager.getSession(message.yourNonce as string)
|
|
571
593
|
if (peerSession == null) {
|
|
572
594
|
throw new Error(`Peer session not found for peer: ${message.identityKey}`)
|
|
573
595
|
}
|
|
574
596
|
|
|
575
|
-
// Validate message signature
|
|
576
597
|
const dataToVerify = Peer.base64ToBytes(
|
|
577
598
|
(peerSession.sessionNonce ?? '') + (message.initialNonce ?? '')
|
|
578
599
|
)
|
|
600
|
+
|
|
579
601
|
const { valid } = await this.wallet.verifySignature({
|
|
580
602
|
data: dataToVerify,
|
|
581
603
|
signature: message.signature as number[],
|
|
@@ -583,55 +605,74 @@ export class Peer {
|
|
|
583
605
|
keyID: `${peerSession.sessionNonce ?? ''} ${message.initialNonce ?? ''}`,
|
|
584
606
|
counterparty: message.identityKey
|
|
585
607
|
}, this.originator)
|
|
608
|
+
|
|
586
609
|
if (!valid) {
|
|
587
610
|
throw new Error(
|
|
588
611
|
`Unable to verify initial response signature for peer: ${message.identityKey}`
|
|
589
612
|
)
|
|
590
613
|
}
|
|
591
614
|
|
|
592
|
-
//
|
|
615
|
+
// --- Transport authentication complete ---
|
|
593
616
|
peerSession.peerNonce = message.initialNonce
|
|
594
617
|
peerSession.peerIdentityKey = message.identityKey
|
|
595
618
|
peerSession.isAuthenticated = true
|
|
619
|
+
|
|
620
|
+
peerSession.certificatesRequired =
|
|
621
|
+
Array.isArray(this.certificatesToRequest?.certifiers) &&
|
|
622
|
+
this.certificatesToRequest.certifiers.length > 0
|
|
623
|
+
|
|
624
|
+
// IMPORTANT: validation defaults to false if certs are required
|
|
625
|
+
peerSession.certificatesValidated = !peerSession.certificatesRequired
|
|
626
|
+
|
|
596
627
|
peerSession.lastUpdate = Date.now()
|
|
597
628
|
this.sessionManager.updateSession(peerSession)
|
|
598
629
|
|
|
599
|
-
//
|
|
630
|
+
// --- Validate certificates if provided ---
|
|
600
631
|
if (
|
|
601
|
-
|
|
602
|
-
message.certificates
|
|
632
|
+
peerSession.certificatesRequired &&
|
|
633
|
+
Array.isArray(message.certificates) &&
|
|
634
|
+
message.certificates.length > 0
|
|
603
635
|
) {
|
|
604
|
-
await validateCertificates(
|
|
636
|
+
await validateCertificates(
|
|
637
|
+
this.wallet,
|
|
638
|
+
message,
|
|
639
|
+
this.certificatesToRequest,
|
|
640
|
+
this.originator
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
peerSession.certificatesValidated = true
|
|
644
|
+
peerSession.lastUpdate = Date.now()
|
|
645
|
+
this.sessionManager.updateSession(peerSession)
|
|
605
646
|
|
|
606
|
-
// Notify listeners
|
|
607
647
|
this.onCertificatesReceivedCallbacks.forEach(cb =>
|
|
608
648
|
cb(message.identityKey, message.certificates as VerifiableCertificate[])
|
|
609
649
|
)
|
|
610
650
|
}
|
|
611
651
|
|
|
612
|
-
// Update
|
|
652
|
+
// Update last-interacted peer
|
|
613
653
|
this.lastInteractedWithPeer = message.identityKey
|
|
614
654
|
|
|
615
|
-
//
|
|
655
|
+
// Release handshake waiters (even if certs still pending)
|
|
616
656
|
this.onInitialResponseReceivedCallbacks.forEach(entry => {
|
|
617
657
|
if (entry.sessionNonce === peerSession.sessionNonce) {
|
|
618
658
|
entry.callback(peerSession.sessionNonce)
|
|
619
659
|
}
|
|
620
660
|
})
|
|
621
661
|
|
|
622
|
-
//
|
|
662
|
+
// --- Peer may request certificates from us ---
|
|
623
663
|
if (
|
|
624
|
-
|
|
664
|
+
message.requestedCertificates != null &&
|
|
625
665
|
Array.isArray(message.requestedCertificates.certifiers) &&
|
|
626
666
|
message.requestedCertificates.certifiers.length > 0
|
|
627
667
|
) {
|
|
628
668
|
if (this.onCertificateRequestReceivedCallbacks.size > 0) {
|
|
629
|
-
// Let the application handle it
|
|
630
669
|
this.onCertificateRequestReceivedCallbacks.forEach(cb => {
|
|
631
|
-
cb(
|
|
670
|
+
cb(
|
|
671
|
+
message.identityKey,
|
|
672
|
+
message.requestedCertificates as RequestedCertificateSet
|
|
673
|
+
)
|
|
632
674
|
})
|
|
633
675
|
} else {
|
|
634
|
-
// Attempt auto
|
|
635
676
|
const verifiableCertificates = await getVerifiableCertificates(
|
|
636
677
|
this.wallet,
|
|
637
678
|
message.requestedCertificates,
|
|
@@ -789,13 +830,14 @@ export class Peer {
|
|
|
789
830
|
this.originator
|
|
790
831
|
)
|
|
791
832
|
|
|
833
|
+
peerSession.certificatesValidated = true
|
|
834
|
+
peerSession.lastUpdate = Date.now()
|
|
835
|
+
this.sessionManager.updateSession(peerSession)
|
|
836
|
+
|
|
792
837
|
// Notify any listeners
|
|
793
838
|
this.onCertificatesReceivedCallbacks.forEach(cb => {
|
|
794
839
|
cb(message.identityKey, message.certificates ?? [])
|
|
795
840
|
})
|
|
796
|
-
|
|
797
|
-
peerSession.lastUpdate = Date.now()
|
|
798
|
-
this.sessionManager.updateSession(peerSession)
|
|
799
841
|
}
|
|
800
842
|
|
|
801
843
|
/**
|
|
@@ -806,7 +848,13 @@ export class Peer {
|
|
|
806
848
|
* @throws Will throw an error if nonce or signature verification fails.
|
|
807
849
|
*/
|
|
808
850
|
private async processGeneralMessage (message: AuthMessage): Promise<void> {
|
|
809
|
-
const validNonce = await verifyNonce(
|
|
851
|
+
const validNonce = await verifyNonce(
|
|
852
|
+
message.yourNonce as string,
|
|
853
|
+
this.wallet,
|
|
854
|
+
undefined,
|
|
855
|
+
this.originator
|
|
856
|
+
)
|
|
857
|
+
|
|
810
858
|
if (!validNonce) {
|
|
811
859
|
throw new Error(
|
|
812
860
|
`Unable to verify nonce for general message from: ${message.identityKey}`
|
|
@@ -818,6 +866,17 @@ export class Peer {
|
|
|
818
866
|
throw new Error(`Session not found for nonce: ${message.yourNonce as string}`)
|
|
819
867
|
}
|
|
820
868
|
|
|
869
|
+
const certificatesRequired = peerSession.certificatesRequired === true
|
|
870
|
+
const certificatesValidated = peerSession.certificatesValidated === true
|
|
871
|
+
|
|
872
|
+
if (certificatesRequired && !certificatesValidated) {
|
|
873
|
+
throw new Error(
|
|
874
|
+
`Received general message before certificate validation from peer ${
|
|
875
|
+
peerSession.peerIdentityKey ?? 'unknown'
|
|
876
|
+
}`
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
|
|
821
880
|
const { valid } = await this.wallet.verifySignature({
|
|
822
881
|
data: message.payload,
|
|
823
882
|
signature: message.signature as number[],
|
|
@@ -825,6 +884,7 @@ export class Peer {
|
|
|
825
884
|
keyID: `${message.nonce ?? ''} ${peerSession.sessionNonce ?? ''}`,
|
|
826
885
|
counterparty: peerSession.peerIdentityKey
|
|
827
886
|
}, this.originator)
|
|
887
|
+
|
|
828
888
|
if (!valid) {
|
|
829
889
|
throw new Error(
|
|
830
890
|
`Invalid signature in generalMessage from ${peerSession.peerIdentityKey as string}`
|
|
@@ -848,10 +908,12 @@ export class Peer {
|
|
|
848
908
|
if (this.identityPublicKey != null) {
|
|
849
909
|
return this.identityPublicKey
|
|
850
910
|
}
|
|
911
|
+
|
|
851
912
|
const { publicKey } = await this.wallet.getPublicKey(
|
|
852
913
|
{ identityKey: true },
|
|
853
914
|
this.originator
|
|
854
915
|
)
|
|
916
|
+
|
|
855
917
|
this.identityPublicKey = publicKey
|
|
856
918
|
return publicKey
|
|
857
919
|
}
|
|
@@ -860,9 +922,11 @@ export class Peer {
|
|
|
860
922
|
if (BufferCtor != null) {
|
|
861
923
|
return Array.from(BufferCtor.from(data, 'utf8'))
|
|
862
924
|
}
|
|
925
|
+
|
|
863
926
|
if (typeof TextEncoder !== 'undefined') {
|
|
864
927
|
return Array.from(new TextEncoder().encode(data))
|
|
865
928
|
}
|
|
929
|
+
|
|
866
930
|
return Utils.toArray(data, 'utf8')
|
|
867
931
|
}
|
|
868
932
|
|
|
@@ -870,6 +934,7 @@ export class Peer {
|
|
|
870
934
|
if (BufferCtor != null) {
|
|
871
935
|
return Array.from(BufferCtor.from(data, 'base64'))
|
|
872
936
|
}
|
|
937
|
+
|
|
873
938
|
return Utils.toArray(data, 'base64')
|
|
874
939
|
}
|
|
875
940
|
}
|