@wishknish/knishio-client-js 0.9.3 → 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 -8770
- 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 +121 -18
- package/src/Molecule.js +16 -8
- package/src/Wallet.js +137 -30
- package/src/exception/SecretStorageException.js +95 -0
- package/src/exception/index.js +2 -0
- package/src/index.js +15 -0
- package/src/libraries/secureMemory.js +125 -0
- package/src/libraries/urql/UrqlClientWrapper.js +7 -2
- package/src/storage/MemorySecretStorageProvider.js +170 -0
- package/src/storage/WebCryptoSecretStorageProvider.js +416 -0
- package/src/storage/index.js +79 -0
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
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
(
|
|
3
|
+
(/(
|
|
4
|
+
(//(
|
|
5
|
+
(///(
|
|
6
|
+
(/////(
|
|
7
|
+
(//////( )
|
|
8
|
+
(////////( (/)
|
|
9
|
+
(////////( (///)
|
|
10
|
+
(//////////( (////)
|
|
11
|
+
(//////////( (//////)
|
|
12
|
+
(////////////( (///////)
|
|
13
|
+
(/////////////( (/////////)
|
|
14
|
+
(//////////////( (///////////)
|
|
15
|
+
(///////////////( (/////////////)
|
|
16
|
+
(////////////////( (//////////////)
|
|
17
|
+
((((((((((((((((((( (((((((((((((((
|
|
18
|
+
((((((((((((((((((( ((((((((((((((
|
|
19
|
+
((((((((((((((((((( ((((((((((((((
|
|
20
|
+
(((((((((((((((((((( (((((((((((((
|
|
21
|
+
(((((((((((((((((((( ((((((((((((
|
|
22
|
+
((((((((((((((((((( ((((((((((((
|
|
23
|
+
((((((((((((((((((( ((((((((((
|
|
24
|
+
((((((((((((((((((/ (((((((((
|
|
25
|
+
(((((((((((((((((( ((((((((
|
|
26
|
+
((((((((((((((((( (((((((
|
|
27
|
+
(((((((((((((((((( (((((
|
|
28
|
+
################# ##
|
|
29
|
+
################ #
|
|
30
|
+
################# ##
|
|
31
|
+
%################ ###
|
|
32
|
+
###############( ####
|
|
33
|
+
############### ####
|
|
34
|
+
############### ######
|
|
35
|
+
%#############( (#######
|
|
36
|
+
%############# #########
|
|
37
|
+
############( ##########
|
|
38
|
+
########### #############
|
|
39
|
+
######### ##############
|
|
40
|
+
%######
|
|
41
|
+
|
|
42
|
+
Powered by Knish.IO: Connecting a Decentralized World
|
|
43
|
+
|
|
44
|
+
Please visit https://github.com/WishKnish/KnishIO-Client-JS for information.
|
|
45
|
+
|
|
46
|
+
License: https://github.com/WishKnish/KnishIO-Client-JS/blob/master/LICENSE
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import BaseException from './BaseException.js'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Exception thrown when secret storage or hardware envelope encryption fails
|
|
53
|
+
*/
|
|
54
|
+
export default class SecretStorageException extends BaseException {
|
|
55
|
+
/**
|
|
56
|
+
* @param {string} message
|
|
57
|
+
* @param {string|null} fileName
|
|
58
|
+
* @param {number|null} lineNumber
|
|
59
|
+
*/
|
|
60
|
+
constructor (message = 'Secret storage operation failed', fileName = null, lineNumber = null) {
|
|
61
|
+
super(message, fileName, lineNumber)
|
|
62
|
+
this.name = 'SecretStorageException'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Factory method: secret not found for bundle
|
|
67
|
+
*
|
|
68
|
+
* @param {string} bundleHash
|
|
69
|
+
* @returns {SecretStorageException}
|
|
70
|
+
*/
|
|
71
|
+
static notFound (bundleHash) {
|
|
72
|
+
return new SecretStorageException(`Secret not found for bundle: ${bundleHash}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Factory method: decryption failed
|
|
77
|
+
*
|
|
78
|
+
* @param {string} [reason]
|
|
79
|
+
* @returns {SecretStorageException}
|
|
80
|
+
*/
|
|
81
|
+
static decryptionFailed (reason = 'Invalid passphrase or corrupted ciphertext') {
|
|
82
|
+
return new SecretStorageException(`Failed to decrypt master secret: ${reason}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Factory method: provider unavailable
|
|
87
|
+
*
|
|
88
|
+
* @param {string} provider
|
|
89
|
+
* @param {string} [reason]
|
|
90
|
+
* @returns {SecretStorageException}
|
|
91
|
+
*/
|
|
92
|
+
static unavailable (provider, reason = 'Hardware or API not accessible') {
|
|
93
|
+
return new SecretStorageException(`Secret storage provider '${provider}' is unavailable: ${reason}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
package/src/exception/index.js
CHANGED
|
@@ -70,6 +70,7 @@ import TransferUnbalancedException from './TransferUnbalancedException.js'
|
|
|
70
70
|
import UnauthenticatedException from './UnauthenticatedException.js'
|
|
71
71
|
import WalletShadowException from './WalletShadowException.js'
|
|
72
72
|
import WrongTokenTypeException from './WrongTokenTypeException.js'
|
|
73
|
+
import SecretStorageException from './SecretStorageException.js'
|
|
73
74
|
|
|
74
75
|
export {
|
|
75
76
|
AtomIndexException,
|
|
@@ -96,5 +97,6 @@ export {
|
|
|
96
97
|
TransferUnbalancedException,
|
|
97
98
|
UnauthenticatedException,
|
|
98
99
|
WalletShadowException,
|
|
100
|
+
SecretStorageException,
|
|
99
101
|
WrongTokenTypeException
|
|
100
102
|
}
|
package/src/index.js
CHANGED
|
@@ -207,6 +207,7 @@ export {
|
|
|
207
207
|
TransferUnbalancedException,
|
|
208
208
|
UnauthenticatedException,
|
|
209
209
|
WalletShadowException,
|
|
210
|
+
SecretStorageException,
|
|
210
211
|
WrongTokenTypeException
|
|
211
212
|
} from './exception/index.js'
|
|
212
213
|
|
|
@@ -325,3 +326,17 @@ export {
|
|
|
325
326
|
diff,
|
|
326
327
|
intersect
|
|
327
328
|
}
|
|
329
|
+
|
|
330
|
+
export {
|
|
331
|
+
MemorySecretStorageProvider,
|
|
332
|
+
WebCryptoSecretStorageProvider,
|
|
333
|
+
MemoryStorageBackend,
|
|
334
|
+
createDefaultSecretStorage
|
|
335
|
+
} from './storage/index.js'
|
|
336
|
+
|
|
337
|
+
export {
|
|
338
|
+
zeroizeBytes,
|
|
339
|
+
withSecureBytes,
|
|
340
|
+
withSecureString,
|
|
341
|
+
constantTimeCompare
|
|
342
|
+
} from './libraries/secureMemory.js'
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
(
|
|
3
|
+
(/(
|
|
4
|
+
(//(
|
|
5
|
+
(///(
|
|
6
|
+
(/////(
|
|
7
|
+
(//////( )
|
|
8
|
+
(////////( (/)
|
|
9
|
+
(////////( (///)
|
|
10
|
+
(//////////( (////)
|
|
11
|
+
(//////////( (//////)
|
|
12
|
+
(////////////( (///////)
|
|
13
|
+
(/////////////( (/////////)
|
|
14
|
+
(//////////////( (///////////)
|
|
15
|
+
(///////////////( (/////////////)
|
|
16
|
+
(////////////////( (//////////////)
|
|
17
|
+
((((((((((((((((((( (((((((((((((((
|
|
18
|
+
((((((((((((((((((( ((((((((((((((
|
|
19
|
+
((((((((((((((((((( ((((((((((((((
|
|
20
|
+
(((((((((((((((((((( (((((((((((((
|
|
21
|
+
(((((((((((((((((((( ((((((((((((
|
|
22
|
+
((((((((((((((((((( ((((((((((((
|
|
23
|
+
((((((((((((((((((( ((((((((((
|
|
24
|
+
((((((((((((((((((/ (((((((((
|
|
25
|
+
(((((((((((((((((( ((((((((
|
|
26
|
+
((((((((((((((((( (((((((
|
|
27
|
+
(((((((((((((((((( (((((
|
|
28
|
+
################# ##
|
|
29
|
+
################ #
|
|
30
|
+
################# ##
|
|
31
|
+
%################ ###
|
|
32
|
+
###############( ####
|
|
33
|
+
############### ####
|
|
34
|
+
############### ######
|
|
35
|
+
%#############( (#######
|
|
36
|
+
%############# #########
|
|
37
|
+
############( ##########
|
|
38
|
+
########### #############
|
|
39
|
+
######### ##############
|
|
40
|
+
%######
|
|
41
|
+
|
|
42
|
+
Powered by Knish.IO: Connecting a Decentralized World
|
|
43
|
+
|
|
44
|
+
Please visit https://github.com/WishKnish/KnishIO-Client-JS for information.
|
|
45
|
+
|
|
46
|
+
License: https://github.com/WishKnish/KnishIO-Client-JS/blob/master/LICENSE
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Memory hygiene and zeroization utilities for sensitive cryptographic material
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
const textEncoder = new TextEncoder()
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Overwrite byte array contents with zeros
|
|
57
|
+
*
|
|
58
|
+
* @param {Uint8Array|number[]} buffer
|
|
59
|
+
*/
|
|
60
|
+
export function zeroizeBytes (buffer) {
|
|
61
|
+
if (buffer instanceof Uint8Array) {
|
|
62
|
+
buffer.fill(0)
|
|
63
|
+
} else if (Array.isArray(buffer)) {
|
|
64
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
65
|
+
buffer[i] = 0
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute a callback with a byte buffer and guarantee zeroization upon completion
|
|
72
|
+
*
|
|
73
|
+
* @template T
|
|
74
|
+
* @param {Uint8Array} bytes
|
|
75
|
+
* @param {(bytes: Uint8Array) => Promise<T>|T} fn
|
|
76
|
+
* @returns {Promise<T>}
|
|
77
|
+
*/
|
|
78
|
+
export async function withSecureBytes (bytes, fn) {
|
|
79
|
+
try {
|
|
80
|
+
return await fn(bytes)
|
|
81
|
+
} finally {
|
|
82
|
+
zeroizeBytes(bytes)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Execute a callback with a secret string, ensuring temporary byte buffers are cleared
|
|
88
|
+
*
|
|
89
|
+
* @template T
|
|
90
|
+
* @param {string} secret
|
|
91
|
+
* @param {(cleanSecret: string) => Promise<T>|T} fn
|
|
92
|
+
* @returns {Promise<T>}
|
|
93
|
+
*/
|
|
94
|
+
export async function withSecureString (secret, fn) {
|
|
95
|
+
const bytes = textEncoder.encode(secret)
|
|
96
|
+
try {
|
|
97
|
+
return await fn(secret)
|
|
98
|
+
} finally {
|
|
99
|
+
zeroizeBytes(bytes)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Constant-time comparison of two byte arrays or strings to prevent timing attacks
|
|
105
|
+
*
|
|
106
|
+
* @param {Uint8Array|string} a
|
|
107
|
+
* @param {Uint8Array|string} b
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
export function constantTimeCompare (a, b) {
|
|
111
|
+
const bytesA = typeof a === 'string' ? textEncoder.encode(a) : a
|
|
112
|
+
const bytesB = typeof b === 'string' ? textEncoder.encode(b) : b
|
|
113
|
+
|
|
114
|
+
let result = bytesA.length === bytesB.length ? 0 : 1
|
|
115
|
+
const len = Math.min(bytesA.length, bytesB.length)
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < len; i++) {
|
|
118
|
+
result |= (bytesA[i] ?? 0) ^ (bytesB[i] ?? 0)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof a === 'string') zeroizeBytes(bytesA)
|
|
122
|
+
if (typeof b === 'string') zeroizeBytes(bytesB)
|
|
123
|
+
|
|
124
|
+
return result === 0
|
|
125
|
+
}
|
|
@@ -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
|
|