@wishknish/knishio-client-js 0.9.4 → 1.0.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/dist/client.cjs.js +38 -40
- package/dist/client.cjs.js.map +1 -1
- package/dist/client.es.mjs +5955 -9256
- package/dist/client.es.mjs.map +1 -1
- package/dist/client.iife.js +38 -40
- package/dist/client.iife.js.map +1 -1
- package/package.json +10 -11
- package/src/AuthToken.js +25 -3
- package/src/KnishIOClient.js +54 -15
- package/src/Molecule.js +15 -7
- package/src/Wallet.js +137 -30
- package/src/libraries/urql/UrqlClientWrapper.js +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wishknish/knishio-client-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"productName": "Knish.IO Javascript SDK Client",
|
|
6
6
|
"description": "JavaScript implementation of the Knish.IO SDK to consume Knish.IO GraphQL APIs.",
|
|
@@ -48,16 +48,16 @@
|
|
|
48
48
|
"quantum-safe",
|
|
49
49
|
"sdk",
|
|
50
50
|
"javascript",
|
|
51
|
-
"ml-
|
|
51
|
+
"ml-kem1024",
|
|
52
52
|
"cryptography",
|
|
53
53
|
"crystals-kyber",
|
|
54
54
|
"fips-202",
|
|
55
55
|
"fips-203"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@noble/post-quantum": "^0.
|
|
59
|
-
"@thumbmarkjs/thumbmarkjs": "^
|
|
60
|
-
"@urql/core": "^
|
|
58
|
+
"@noble/post-quantum": "^0.7.1",
|
|
59
|
+
"@thumbmarkjs/thumbmarkjs": "^1.11.0",
|
|
60
|
+
"@urql/core": "^6.0.3",
|
|
61
61
|
"graphql": "^16.12.0",
|
|
62
62
|
"graphql-ws": "^6.0.7",
|
|
63
63
|
"isomorphic-fetch": "^3.0.0",
|
|
@@ -66,17 +66,13 @@
|
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"@jest/globals": "^30.4.1",
|
|
69
|
-
"@rollup/plugin-babel": "^6.1.0",
|
|
70
|
-
"@rollup/plugin-commonjs": "^28.0.9",
|
|
71
|
-
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
72
69
|
"@swc/core": "^1.15.43",
|
|
73
70
|
"@swc/jest": "^0.2.39",
|
|
74
71
|
"buffer": "^6.0.3",
|
|
75
72
|
"eslint": "^9.39.0",
|
|
76
73
|
"jest": "^30.4.2",
|
|
77
74
|
"neostandard": "^0.13.0",
|
|
78
|
-
"
|
|
79
|
-
"vite": "^7.3.5"
|
|
75
|
+
"vite": "^8.2.2"
|
|
80
76
|
},
|
|
81
77
|
"browserslist": [
|
|
82
78
|
"> 1%",
|
|
@@ -96,5 +92,8 @@
|
|
|
96
92
|
"/src",
|
|
97
93
|
"/dist"
|
|
98
94
|
],
|
|
99
|
-
"packageManager": "yarn@4.15.0"
|
|
95
|
+
"packageManager": "yarn@4.15.0",
|
|
96
|
+
"engines": {
|
|
97
|
+
"node": ">=20.0.0"
|
|
98
|
+
}
|
|
100
99
|
}
|
package/src/AuthToken.js
CHANGED
|
@@ -83,6 +83,26 @@ export default class AuthToken {
|
|
|
83
83
|
return authToken
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* ML-KEM parameter set a restored session must use, resolved in three tiers:
|
|
88
|
+
* an explicit snapshot field, then the stored validator key's length, then ML-KEM-768.
|
|
89
|
+
*
|
|
90
|
+
* The final tier is deliberately NOT the constructor default. A snapshot with neither an
|
|
91
|
+
* explicit field nor a recognisable key can only have come from a pre-bump build, and every
|
|
92
|
+
* pre-bump build was 768-only — defaulting to 1024 would make the restored wallet advertise
|
|
93
|
+
* a public key the validator never recorded for that token.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} snapshot
|
|
96
|
+
* @return {number}
|
|
97
|
+
*/
|
|
98
|
+
static resolveMlKemParameterSet (snapshot) {
|
|
99
|
+
const explicit = snapshot.wallet && snapshot.wallet.mlKemParameterSet
|
|
100
|
+
if (explicit) {
|
|
101
|
+
return Number(explicit)
|
|
102
|
+
}
|
|
103
|
+
return Wallet.mlKemParameterSetFromPubkey(snapshot.pubkey) || 768
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
/**
|
|
87
107
|
*
|
|
88
108
|
* @param {object} snapshot
|
|
@@ -94,7 +114,8 @@ export default class AuthToken {
|
|
|
94
114
|
secret,
|
|
95
115
|
token: 'AUTH',
|
|
96
116
|
position: snapshot.wallet.position,
|
|
97
|
-
characters: snapshot.wallet.characters
|
|
117
|
+
characters: snapshot.wallet.characters,
|
|
118
|
+
mlKemParameterSet: AuthToken.resolveMlKemParameterSet(snapshot)
|
|
98
119
|
})
|
|
99
120
|
return AuthToken.create({
|
|
100
121
|
token: snapshot.token,
|
|
@@ -122,7 +143,7 @@ export default class AuthToken {
|
|
|
122
143
|
|
|
123
144
|
/**
|
|
124
145
|
*
|
|
125
|
-
* @return {{wallet: {characters, position}, encrypt, expiresAt, token, pubkey}}
|
|
146
|
+
* @return {{wallet: {characters, position, mlKemParameterSet}, encrypt, expiresAt, token, pubkey}}
|
|
126
147
|
*/
|
|
127
148
|
getSnapshot () {
|
|
128
149
|
return {
|
|
@@ -132,7 +153,8 @@ export default class AuthToken {
|
|
|
132
153
|
encrypt: this.$__encrypt,
|
|
133
154
|
wallet: {
|
|
134
155
|
position: this.$__wallet.position,
|
|
135
|
-
characters: this.$__wallet.characters
|
|
156
|
+
characters: this.$__wallet.characters,
|
|
157
|
+
mlKemParameterSet: this.$__wallet.mlKemParameterSet
|
|
136
158
|
}
|
|
137
159
|
}
|
|
138
160
|
}
|
package/src/KnishIOClient.js
CHANGED
|
@@ -129,7 +129,8 @@ export default class KnishIOClient {
|
|
|
129
129
|
serverSdkVersion = 3,
|
|
130
130
|
logging = false,
|
|
131
131
|
defaultRequestPolicy = null,
|
|
132
|
-
secretStorage = null
|
|
132
|
+
secretStorage = null,
|
|
133
|
+
mlKemParameterSet = 1024
|
|
133
134
|
}) {
|
|
134
135
|
this.initialize({
|
|
135
136
|
uri,
|
|
@@ -139,7 +140,8 @@ export default class KnishIOClient {
|
|
|
139
140
|
serverSdkVersion,
|
|
140
141
|
logging,
|
|
141
142
|
defaultRequestPolicy,
|
|
142
|
-
secretStorage
|
|
143
|
+
secretStorage,
|
|
144
|
+
mlKemParameterSet
|
|
143
145
|
})
|
|
144
146
|
}
|
|
145
147
|
|
|
@@ -161,7 +163,8 @@ export default class KnishIOClient {
|
|
|
161
163
|
serverSdkVersion = 3,
|
|
162
164
|
logging = false,
|
|
163
165
|
defaultRequestPolicy = null,
|
|
164
|
-
secretStorage = null
|
|
166
|
+
secretStorage = null,
|
|
167
|
+
mlKemParameterSet = 1024
|
|
165
168
|
}) {
|
|
166
169
|
this.reset()
|
|
167
170
|
|
|
@@ -171,6 +174,7 @@ export default class KnishIOClient {
|
|
|
171
174
|
// policy. A long-lived server/sync client set to 'network-only' never serves
|
|
172
175
|
// a stale cache-first read; browser/SPA consumers leave this null (cache-first).
|
|
173
176
|
this.$__defaultRequestPolicy = defaultRequestPolicy
|
|
177
|
+
this.setMlKemParameterSet(mlKemParameterSet)
|
|
174
178
|
this.$__authTokenObjects = {}
|
|
175
179
|
this.$__authInProcess = false
|
|
176
180
|
this.abortControllers = new Map()
|
|
@@ -202,6 +206,30 @@ export default class KnishIOClient {
|
|
|
202
206
|
this.$__serverSdkVersion = serverSdkVersion
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Get active ML-KEM parameter set (1024 default or 768 step-back)
|
|
211
|
+
*
|
|
212
|
+
* @return {number}
|
|
213
|
+
*/
|
|
214
|
+
getMlKemParameterSet () {
|
|
215
|
+
return this.$__mlKemParameterSet || 1024
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Set active ML-KEM parameter set (1024 default or 768 step-back)
|
|
220
|
+
*
|
|
221
|
+
* @param {number|string} parameterSet
|
|
222
|
+
* @return {KnishIOClient}
|
|
223
|
+
*/
|
|
224
|
+
setMlKemParameterSet (parameterSet) {
|
|
225
|
+
const paramNum = Number(parameterSet)
|
|
226
|
+
if (![1024, 768].includes(paramNum)) {
|
|
227
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`)
|
|
228
|
+
}
|
|
229
|
+
this.$__mlKemParameterSet = paramNum
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
205
233
|
/**
|
|
206
234
|
* Get random uri from specified this.$__uris
|
|
207
235
|
*
|
|
@@ -509,7 +537,8 @@ export default class KnishIOClient {
|
|
|
509
537
|
|
|
510
538
|
if (!sourceWallet) {
|
|
511
539
|
sourceWallet = new Wallet({
|
|
512
|
-
secret: this.getSecret()
|
|
540
|
+
secret: this.getSecret(),
|
|
541
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
513
542
|
})
|
|
514
543
|
} else {
|
|
515
544
|
sourceWallet.key = Wallet.generateKey({
|
|
@@ -598,7 +627,8 @@ export default class KnishIOClient {
|
|
|
598
627
|
bundle,
|
|
599
628
|
token: 'USER',
|
|
600
629
|
batchId: sourceWallet.batchId,
|
|
601
|
-
characters: sourceWallet.characters
|
|
630
|
+
characters: sourceWallet.characters,
|
|
631
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
602
632
|
})
|
|
603
633
|
|
|
604
634
|
return new Molecule({
|
|
@@ -608,7 +638,8 @@ export default class KnishIOClient {
|
|
|
608
638
|
remainderWallet: this.getRemainderWallet(),
|
|
609
639
|
cellSlug: this.getCellSlug(),
|
|
610
640
|
version: this.getServerSdkVersion(),
|
|
611
|
-
continuIdPosition
|
|
641
|
+
continuIdPosition,
|
|
642
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
612
643
|
})
|
|
613
644
|
}
|
|
614
645
|
|
|
@@ -1237,7 +1268,8 @@ export default class KnishIOClient {
|
|
|
1237
1268
|
}) {
|
|
1238
1269
|
const newWallet = new Wallet({
|
|
1239
1270
|
secret: this.getSecret(),
|
|
1240
|
-
token
|
|
1271
|
+
token,
|
|
1272
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
1241
1273
|
})
|
|
1242
1274
|
|
|
1243
1275
|
/**
|
|
@@ -1369,7 +1401,8 @@ export default class KnishIOClient {
|
|
|
1369
1401
|
secret: this.getSecret(),
|
|
1370
1402
|
bundle: this.getBundle(),
|
|
1371
1403
|
token,
|
|
1372
|
-
batchId
|
|
1404
|
+
batchId,
|
|
1405
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
1373
1406
|
})
|
|
1374
1407
|
|
|
1375
1408
|
/**
|
|
@@ -1783,7 +1816,8 @@ export default class KnishIOClient {
|
|
|
1783
1816
|
} else {
|
|
1784
1817
|
to = Wallet.create({
|
|
1785
1818
|
secret: to,
|
|
1786
|
-
token
|
|
1819
|
+
token,
|
|
1820
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
1787
1821
|
})
|
|
1788
1822
|
}
|
|
1789
1823
|
}
|
|
@@ -1935,7 +1969,8 @@ export default class KnishIOClient {
|
|
|
1935
1969
|
// Attempt to get the recipient's wallet, if not provided
|
|
1936
1970
|
const recipientWallet = Wallet.create({
|
|
1937
1971
|
bundle: bundleHash,
|
|
1938
|
-
token
|
|
1972
|
+
token,
|
|
1973
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
1939
1974
|
})
|
|
1940
1975
|
|
|
1941
1976
|
// Compute the batch ID for the recipient
|
|
@@ -2035,7 +2070,8 @@ export default class KnishIOClient {
|
|
|
2035
2070
|
const recipientWallets = recipients.map(recipient => {
|
|
2036
2071
|
const recipientWallet = Wallet.create({
|
|
2037
2072
|
bundle: recipient.bundleHash,
|
|
2038
|
-
token
|
|
2073
|
+
token,
|
|
2074
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
2039
2075
|
})
|
|
2040
2076
|
|
|
2041
2077
|
// Compute the batch ID for the recipient (typically used by stackable tokens)
|
|
@@ -2334,7 +2370,8 @@ export default class KnishIOClient {
|
|
|
2334
2370
|
// Generate new recipient wallet if only recipient secret has been passed
|
|
2335
2371
|
const recipientWallet = Wallet.create({
|
|
2336
2372
|
bundle: bundleHash,
|
|
2337
|
-
token: tokenSlug
|
|
2373
|
+
token: tokenSlug,
|
|
2374
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
2338
2375
|
})
|
|
2339
2376
|
|
|
2340
2377
|
// Set batch ID
|
|
@@ -2390,7 +2427,8 @@ export default class KnishIOClient {
|
|
|
2390
2427
|
// Create a wallet for encryption
|
|
2391
2428
|
const wallet = new Wallet({
|
|
2392
2429
|
secret: generateSecret(await this.getFingerprint()),
|
|
2393
|
-
token: 'AUTH'
|
|
2430
|
+
token: 'AUTH',
|
|
2431
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
2394
2432
|
})
|
|
2395
2433
|
|
|
2396
2434
|
/**
|
|
@@ -2443,7 +2481,8 @@ export default class KnishIOClient {
|
|
|
2443
2481
|
// Generate a signing wallet
|
|
2444
2482
|
const wallet = new Wallet({
|
|
2445
2483
|
secret,
|
|
2446
|
-
token: 'AUTH'
|
|
2484
|
+
token: 'AUTH',
|
|
2485
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
2447
2486
|
})
|
|
2448
2487
|
|
|
2449
2488
|
// Create a wallet with a signing wallet
|
|
@@ -2460,7 +2499,7 @@ export default class KnishIOClient {
|
|
|
2460
2499
|
molecule
|
|
2461
2500
|
})
|
|
2462
2501
|
|
|
2463
|
-
// PQ-transport Phase E (cycle 163): convey the AUTH source wallet's ML-
|
|
2502
|
+
// PQ-transport Phase E (cycle 163): convey the AUTH source wallet's ML-KEM public key as a
|
|
2464
2503
|
// SIGNED `walletPubkey` meta on the U-atom (fillMolecule → initAuthorization → sign), so the
|
|
2465
2504
|
// validator can encrypt CipherHash responses back to THIS wallet (the one that decrypts them).
|
|
2466
2505
|
// Signed → tamper-proof. Only when present (PQ-capable wallet).
|
package/src/Molecule.js
CHANGED
|
@@ -87,8 +87,10 @@ export default class Molecule {
|
|
|
87
87
|
remainderWallet = null,
|
|
88
88
|
cellSlug = null,
|
|
89
89
|
version = null,
|
|
90
|
-
continuIdPosition = null
|
|
90
|
+
continuIdPosition = null,
|
|
91
|
+
mlKemParameterSet = null
|
|
91
92
|
}) {
|
|
93
|
+
this.mlKemParameterSet = mlKemParameterSet || (sourceWallet && sourceWallet.mlKemParameterSet) || 1024
|
|
92
94
|
this.status = null
|
|
93
95
|
this.molecularHash = null
|
|
94
96
|
this.createdAt = String(+new Date())
|
|
@@ -110,7 +112,8 @@ export default class Molecule {
|
|
|
110
112
|
bundle,
|
|
111
113
|
token: sourceWallet.token,
|
|
112
114
|
batchId: sourceWallet.batchId,
|
|
113
|
-
characters: sourceWallet.characters
|
|
115
|
+
characters: sourceWallet.characters,
|
|
116
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
114
117
|
})
|
|
115
118
|
}
|
|
116
119
|
}
|
|
@@ -343,7 +346,8 @@ export default class Molecule {
|
|
|
343
346
|
if (!this.remainderWallet || this.remainderWallet.token !== 'USER') {
|
|
344
347
|
this.remainderWallet = Wallet.create({
|
|
345
348
|
secret: this.secret,
|
|
346
|
-
bundle: this.bundle
|
|
349
|
+
bundle: this.bundle,
|
|
350
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
347
351
|
})
|
|
348
352
|
}
|
|
349
353
|
|
|
@@ -479,7 +483,8 @@ export default class Molecule {
|
|
|
479
483
|
// Create burn address wallet (null bundle = token destruction)
|
|
480
484
|
const burnWallet = new Wallet({
|
|
481
485
|
bundle: '0000000000000000000000000000000000000000000000000000000000000000',
|
|
482
|
-
token: this.sourceWallet.token
|
|
486
|
+
token: this.sourceWallet.token,
|
|
487
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
483
488
|
})
|
|
484
489
|
|
|
485
490
|
// V-atom 1: Debit full balance from source
|
|
@@ -685,7 +690,8 @@ export default class Molecule {
|
|
|
685
690
|
secret: this.secret,
|
|
686
691
|
bundle: this.bundle,
|
|
687
692
|
token: this.sourceWallet.token,
|
|
688
|
-
batchId: this.sourceWallet.batchId
|
|
693
|
+
batchId: this.sourceWallet.batchId,
|
|
694
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
689
695
|
})
|
|
690
696
|
bufferWallet.tradeRates = tradeRates
|
|
691
697
|
|
|
@@ -1329,7 +1335,8 @@ export default class Molecule {
|
|
|
1329
1335
|
position: data.sourceWallet.position,
|
|
1330
1336
|
bundle: data.sourceWallet.bundle,
|
|
1331
1337
|
batchId: data.sourceWallet.batchId,
|
|
1332
|
-
characters: data.sourceWallet.characters
|
|
1338
|
+
characters: data.sourceWallet.characters,
|
|
1339
|
+
mlKemParameterSet: molecule.mlKemParameterSet
|
|
1333
1340
|
})
|
|
1334
1341
|
|
|
1335
1342
|
// Set additional properties for validation context
|
|
@@ -1351,7 +1358,8 @@ export default class Molecule {
|
|
|
1351
1358
|
position: data.remainderWallet.position,
|
|
1352
1359
|
bundle: data.remainderWallet.bundle,
|
|
1353
1360
|
batchId: data.remainderWallet.batchId,
|
|
1354
|
-
characters: data.remainderWallet.characters
|
|
1361
|
+
characters: data.remainderWallet.characters,
|
|
1362
|
+
mlKemParameterSet: molecule.mlKemParameterSet
|
|
1355
1363
|
})
|
|
1356
1364
|
|
|
1357
1365
|
// Set additional properties for validation context
|
package/src/Wallet.js
CHANGED
|
@@ -59,7 +59,13 @@ import {
|
|
|
59
59
|
} from './libraries/crypto.js'
|
|
60
60
|
import TokenUnit from './TokenUnit.js'
|
|
61
61
|
import WalletCredentialException from './exception/WalletCredentialException.js'
|
|
62
|
-
import { ml_kem768 as MlKEM768 } from '@noble/post-quantum/ml-kem.js'
|
|
62
|
+
import { ml_kem768 as MlKEM768, ml_kem1024 as MlKEM1024 } from '@noble/post-quantum/ml-kem.js'
|
|
63
|
+
|
|
64
|
+
const ML_KEM_PARAMS = {
|
|
65
|
+
1024: { kem: MlKEM1024, pkBytes: 1568, skBytes: 3168, ctBytes: 1568 },
|
|
66
|
+
768: { kem: MlKEM768, pkBytes: 1184, skBytes: 2400, ctBytes: 1088 }
|
|
67
|
+
}
|
|
68
|
+
const DEFAULT_ML_KEM_PARAMETER_SET = 1024
|
|
63
69
|
|
|
64
70
|
/**
|
|
65
71
|
* Wallet class represents the set of public and private
|
|
@@ -84,8 +90,14 @@ export default class Wallet {
|
|
|
84
90
|
address = null,
|
|
85
91
|
position = null,
|
|
86
92
|
batchId = null,
|
|
87
|
-
characters = null
|
|
93
|
+
characters = null,
|
|
94
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
88
95
|
}) {
|
|
96
|
+
const paramSetNum = Number(mlKemParameterSet)
|
|
97
|
+
if (!ML_KEM_PARAMS[paramSetNum]) {
|
|
98
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${mlKemParameterSet}; expected 1024 or 768.`)
|
|
99
|
+
}
|
|
100
|
+
this.mlKemParameterSet = paramSetNum
|
|
89
101
|
this.token = token
|
|
90
102
|
this.balance = '0'
|
|
91
103
|
this.molecules = {}
|
|
@@ -141,7 +153,8 @@ export default class Wallet {
|
|
|
141
153
|
bundle = null,
|
|
142
154
|
token,
|
|
143
155
|
batchId = null,
|
|
144
|
-
characters = null
|
|
156
|
+
characters = null,
|
|
157
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
145
158
|
}) {
|
|
146
159
|
let position = null
|
|
147
160
|
|
|
@@ -163,7 +176,8 @@ export default class Wallet {
|
|
|
163
176
|
token,
|
|
164
177
|
position,
|
|
165
178
|
batchId,
|
|
166
|
-
characters
|
|
179
|
+
characters,
|
|
180
|
+
mlKemParameterSet
|
|
167
181
|
})
|
|
168
182
|
}
|
|
169
183
|
|
|
@@ -280,23 +294,49 @@ export default class Wallet {
|
|
|
280
294
|
}
|
|
281
295
|
|
|
282
296
|
/**
|
|
283
|
-
*
|
|
297
|
+
* Derive an ML-KEM keypair for an arbitrary parameter set from the wallet's key seed.
|
|
298
|
+
*
|
|
299
|
+
* Returns `null` when the wallet holds no key — a secret-less wallet, which is what
|
|
300
|
+
* {@link Molecule.fromJSON} builds for validation context. `generateSecret(null, …)` does NOT
|
|
301
|
+
* throw, so without this the wallet would derive a plausible-looking identity from a bogus seed
|
|
302
|
+
* and fail three layers down at AES-GCM instead of at the missing key. The guard lives here
|
|
303
|
+
* rather than at each call site so a new caller cannot miss it.
|
|
304
|
+
*
|
|
305
|
+
* @param {number} parameterSet - 1024 or 768
|
|
306
|
+
* @return {{pubkey: string, privkey: Uint8Array, params: object}|null}
|
|
284
307
|
*/
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
308
|
+
_deriveMlKemKeypair (parameterSet) {
|
|
309
|
+
const params = ML_KEM_PARAMS[parameterSet]
|
|
310
|
+
if (!params) {
|
|
311
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`)
|
|
312
|
+
}
|
|
313
|
+
if (!this.key) {
|
|
314
|
+
return null
|
|
315
|
+
}
|
|
288
316
|
const seedHex = generateSecret(this.key, 128) // 128 hex chars = 64 bytes
|
|
289
|
-
|
|
290
|
-
// Convert the hex string to a Uint8Array
|
|
291
317
|
const seed = new Uint8Array(64)
|
|
292
318
|
for (let i = 0; i < 64; i++) {
|
|
293
319
|
seed[i] = parseInt(seedHex.substr(i * 2, 2), 16)
|
|
294
320
|
}
|
|
321
|
+
const { publicKey, secretKey } = params.kem.keygen(seed)
|
|
322
|
+
return {
|
|
323
|
+
pubkey: this.serializeKey(publicKey),
|
|
324
|
+
privkey: secretKey,
|
|
325
|
+
params
|
|
326
|
+
}
|
|
327
|
+
}
|
|
295
328
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Initializes the ML-KEM key pair. Only ever reached from the constructor's `secret` branch,
|
|
331
|
+
* so the derivation cannot come back empty here.
|
|
332
|
+
*/
|
|
333
|
+
initializeMLKEM () {
|
|
334
|
+
const derived = this._deriveMlKemKeypair(this.mlKemParameterSet)
|
|
335
|
+
if (!derived) {
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
this.pubkey = derived.pubkey
|
|
339
|
+
this.privkey = derived.privkey
|
|
300
340
|
}
|
|
301
341
|
|
|
302
342
|
serializeKey (key) {
|
|
@@ -321,6 +361,35 @@ export default class Wallet {
|
|
|
321
361
|
return new Uint8Array(binaryString.length).map((_, i) => binaryString.charCodeAt(i))
|
|
322
362
|
}
|
|
323
363
|
|
|
364
|
+
/**
|
|
365
|
+
* ML-KEM parameter set implied by a serialized public key's raw byte length. FIPS 203's
|
|
366
|
+
* key lengths are disjoint (1568 bytes → ML-KEM-1024, 1184 bytes → ML-KEM-768), so a stored
|
|
367
|
+
* peer key recovers the parameter set of the session it belongs to without a wire-format
|
|
368
|
+
* change. Used by {@link AuthToken.restore} to resolve a snapshot that predates the field.
|
|
369
|
+
*
|
|
370
|
+
* @param {string|null} pubkey - Base64-serialized ML-KEM public key
|
|
371
|
+
* @return {number|null} 1024, 768, or null when the length matches neither
|
|
372
|
+
*/
|
|
373
|
+
static mlKemParameterSetFromPubkey (pubkey) {
|
|
374
|
+
if (!pubkey) {
|
|
375
|
+
return null
|
|
376
|
+
}
|
|
377
|
+
let byteLength
|
|
378
|
+
try {
|
|
379
|
+
byteLength = typeof Buffer !== 'undefined'
|
|
380
|
+
? Buffer.from(pubkey, 'base64').length
|
|
381
|
+
: atob(pubkey).length
|
|
382
|
+
} catch (e) {
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
for (const [set, params] of Object.entries(ML_KEM_PARAMS)) {
|
|
386
|
+
if (params.pkBytes === byteLength) {
|
|
387
|
+
return Number(set)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null
|
|
391
|
+
}
|
|
392
|
+
|
|
324
393
|
/**
|
|
325
394
|
* Returns balance as a Number for arithmetic operations.
|
|
326
395
|
* WARNING: Precision loss for values > 2^53.
|
|
@@ -499,19 +568,19 @@ export default class Wallet {
|
|
|
499
568
|
const messageString = JSON.stringify(message)
|
|
500
569
|
const messageUint8 = new TextEncoder().encode(messageString)
|
|
501
570
|
const deserializedPubkey = this.deserializeKey(recipientPubkey)
|
|
502
|
-
// ML-KEM
|
|
503
|
-
// node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator
|
|
504
|
-
// the PQ-transport build). Fail with an actionable message rather than the crypto lib's cryptic
|
|
505
|
-
// `"publicKey" expected Uint8Array of length
|
|
506
|
-
const
|
|
507
|
-
if (deserializedPubkey.length !==
|
|
571
|
+
// ML-KEM public keys are exactly 1568 bytes (ML-KEM-1024) or 1184 bytes (ML-KEM-768). A wrong-length key here
|
|
572
|
+
// almost always means the node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator
|
|
573
|
+
// predating the PQ-transport build). Fail with an actionable message rather than the crypto lib's cryptic
|
|
574
|
+
// `"publicKey" expected Uint8Array of length N, got length=M` assertion.
|
|
575
|
+
const params = ML_KEM_PARAMS[this.mlKemParameterSet]
|
|
576
|
+
if (deserializedPubkey.length !== params.pkBytes) {
|
|
508
577
|
throw new Error(
|
|
509
578
|
`KnishIO: cannot ML-KEM-encrypt — recipient public key is ${deserializedPubkey.length} bytes, ` +
|
|
510
|
-
`expected ${
|
|
511
|
-
'
|
|
579
|
+
`expected ${params.pkBytes} (ML-KEM-${this.mlKemParameterSet}). The peer is not running ML-KEM-${this.mlKemParameterSet}; ` +
|
|
580
|
+
'upgrade the peer, or step this client back to the other parameter set.'
|
|
512
581
|
)
|
|
513
582
|
}
|
|
514
|
-
const { cipherText, sharedSecret } =
|
|
583
|
+
const { cipherText, sharedSecret } = params.kem.encapsulate(deserializedPubkey)
|
|
515
584
|
const encryptedMessage = await this.encryptWithSharedSecret(messageUint8, sharedSecret)
|
|
516
585
|
return {
|
|
517
586
|
cipherText: this.serializeKey(cipherText),
|
|
@@ -525,17 +594,46 @@ export default class Wallet {
|
|
|
525
594
|
}
|
|
526
595
|
|
|
527
596
|
/**
|
|
528
|
-
* ML-
|
|
597
|
+
* ML-KEM decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string
|
|
529
598
|
* (no JSON.parse). Shared by {@link decryptMessage} (which JSON.parses the result)
|
|
530
|
-
* and the PQ CipherHash transport ({@link
|
|
599
|
+
* and the PQ CipherHash transport ({@link decryptMyMessageML}, which needs the raw
|
|
531
600
|
* response JSON text). PQ-transport Phase E (cycle 163).
|
|
532
601
|
*/
|
|
533
602
|
async _mlkemDecryptToString (encryptedData) {
|
|
534
603
|
const { cipherText, encryptedMessage } = encryptedData
|
|
535
604
|
|
|
605
|
+
const deserializedCipherText = this.deserializeKey(cipherText)
|
|
606
|
+
const configuredParams = ML_KEM_PARAMS[this.mlKemParameterSet]
|
|
607
|
+
const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024
|
|
608
|
+
|
|
609
|
+
// Inbound is PERMISSIVE: a ciphertext at either parameter set decrypts, provided it is addressed
|
|
610
|
+
// to one of THIS wallet's own ML-KEM identities. The 64-byte seed is parameter-set-independent,
|
|
611
|
+
// so the other identity is derived on demand and its private key is released with this call's
|
|
612
|
+
// scope — never cached on the wallet. Outbound encapsulation stays STRICT (see encryptMessage);
|
|
613
|
+
// reading a 768 record we own downgrades nothing, but encapsulating at 768 would.
|
|
614
|
+
let decapsParams = configuredParams
|
|
615
|
+
let decapsPrivkey = this.privkey
|
|
616
|
+
if (deserializedCipherText.length !== configuredParams.ctBytes) {
|
|
617
|
+
if (deserializedCipherText.length !== ML_KEM_PARAMS[otherSet].ctBytes) {
|
|
618
|
+
console.error(
|
|
619
|
+
`Wallet::decryptMessage() - Ciphertext length mismatch: got ${deserializedCipherText.length}, expected ${configuredParams.ctBytes}`
|
|
620
|
+
)
|
|
621
|
+
return null
|
|
622
|
+
}
|
|
623
|
+
// `null` here means the wallet holds no key to derive from (a secret-less validation
|
|
624
|
+
// wallet); preserve the existing failure observable rather than decapsulating with nothing.
|
|
625
|
+
const derived = this._deriveMlKemKeypair(otherSet)
|
|
626
|
+
if (!derived) {
|
|
627
|
+
console.error(`Wallet::decryptMessage() - cannot derive the ML-KEM-${otherSet} identity: wallet has no key`)
|
|
628
|
+
return null
|
|
629
|
+
}
|
|
630
|
+
decapsParams = derived.params
|
|
631
|
+
decapsPrivkey = derived.privkey
|
|
632
|
+
}
|
|
633
|
+
|
|
536
634
|
let sharedSecret
|
|
537
635
|
try {
|
|
538
|
-
sharedSecret =
|
|
636
|
+
sharedSecret = decapsParams.kem.decapsulate(deserializedCipherText, decapsPrivkey)
|
|
539
637
|
} catch (e) {
|
|
540
638
|
console.error('Wallet::decryptMessage() - Decapsulation failed', e)
|
|
541
639
|
console.info('Wallet::decryptMessage() - my public key', this.pubkey)
|
|
@@ -587,11 +685,11 @@ export default class Wallet {
|
|
|
587
685
|
}
|
|
588
686
|
|
|
589
687
|
/**
|
|
590
|
-
* Post-quantum (ML-
|
|
688
|
+
* Post-quantum (ML-KEM) `CipherHash` request envelope: a stringified single-recipient
|
|
591
689
|
* map `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued,
|
|
592
690
|
* via {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ Phase E.
|
|
593
691
|
*/
|
|
594
|
-
async
|
|
692
|
+
async encryptStringML (message, recipientPubkey) {
|
|
595
693
|
const envelope = await this.encryptMessage(message, recipientPubkey)
|
|
596
694
|
return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope })
|
|
597
695
|
}
|
|
@@ -601,8 +699,17 @@ export default class Wallet {
|
|
|
601
699
|
* (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
|
|
602
700
|
* it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
|
|
603
701
|
*/
|
|
604
|
-
async
|
|
605
|
-
|
|
702
|
+
async decryptMyMessageML (map) {
|
|
703
|
+
let envelope = map[this.hashShare(this.pubkey)]
|
|
704
|
+
if (!envelope) {
|
|
705
|
+
// Inbound permissive: try the hash share of the on-demand derived other-set pubkey.
|
|
706
|
+
// A secret-less wallet derives nothing, so the lookup is simply skipped.
|
|
707
|
+
const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024
|
|
708
|
+
const other = this._deriveMlKemKeypair(otherSet)
|
|
709
|
+
if (other) {
|
|
710
|
+
envelope = map[this.hashShare(other.pubkey)]
|
|
711
|
+
}
|
|
712
|
+
}
|
|
606
713
|
if (!envelope) {
|
|
607
714
|
return null
|
|
608
715
|
}
|
|
@@ -63,6 +63,11 @@ class UrqlClientWrapper {
|
|
|
63
63
|
return createClient({
|
|
64
64
|
url: serverUri,
|
|
65
65
|
exchanges,
|
|
66
|
+
// urql 5 had no default and always POSTed; urql 6 defaults to 'within-url-limit', which
|
|
67
|
+
// URL-encodes short queries and sends NO body. That would silently disable the CipherHash
|
|
68
|
+
// envelope below — cipherFetch's `typeof init.body === 'string'` guard fails on a GET, so
|
|
69
|
+
// the query would leave as plaintext URL parameters with no error. Pin POST explicitly.
|
|
70
|
+
preferGetMethod: false,
|
|
66
71
|
// PQ-transport Phase E: when encryption is on, route fetch through the CipherHash
|
|
67
72
|
// wrapper (encrypt the request body to the validator's ML-KEM pubkey, decrypt the
|
|
68
73
|
// response). Undefined → urql uses the global fetch (plaintext).
|
|
@@ -114,7 +119,7 @@ class UrqlClientWrapper {
|
|
|
114
119
|
let requestInit = init
|
|
115
120
|
|
|
116
121
|
if (wallet && serverPubkey && init && typeof init.body === 'string' && this.shouldEncrypt(init.body)) {
|
|
117
|
-
const hashVar = await wallet.
|
|
122
|
+
const hashVar = await wallet.encryptStringML(init.body, serverPubkey)
|
|
118
123
|
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) }
|
|
119
124
|
encryptedRequest = true
|
|
120
125
|
}
|
|
@@ -138,7 +143,7 @@ class UrqlClientWrapper {
|
|
|
138
143
|
// Plaintext (e.g. a validator-side error response) — pass through unchanged.
|
|
139
144
|
return new Response(text, init2)
|
|
140
145
|
}
|
|
141
|
-
const decrypted = await wallet.
|
|
146
|
+
const decrypted = await wallet.decryptMyMessageML(JSON.parse(hash))
|
|
142
147
|
return new Response(decrypted != null ? decrypted : text, init2)
|
|
143
148
|
}
|
|
144
149
|
|