@wishknish/knishio-client-ts 0.9.8 → 1.1.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 +13 -0
- package/dist/index.cjs +1500 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +188 -48
- package/dist/index.d.ts +188 -48
- package/dist/index.iife.js +1494 -156
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +1492 -165
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/AuthToken.ts +47 -25
- package/src/KnishIOClient.ts +40 -12
- package/src/core/Molecule.ts +17 -7
- package/src/core/Wallet.ts +161 -32
- package/src/exception/SecretStorageException.ts +9 -0
- package/src/index.ts +17 -3
- package/src/libraries/GraphQLClient.ts +2 -2
- package/src/schemas/index.ts +2 -1
- package/src/storage/FileStorageBackend.ts +176 -0
- package/src/storage/MemorySecretStorageProvider.ts +75 -3
- package/src/storage/NonExtractableKeySecretStorageProvider.ts +547 -0
- package/src/storage/WebAuthnPrfSecretStorageProvider.ts +679 -0
- package/src/storage/WebCryptoSecretStorageProvider.ts +105 -134
- package/src/storage/WebStorageBackend.ts +102 -0
- package/src/storage/index.ts +26 -2
- package/src/storage/secretEnvelope.ts +200 -0
- package/src/types/crypto.ts +9 -4
- package/src/types/storage.ts +22 -2
- package/src/validation/schemas.ts +2 -1
package/src/core/Wallet.ts
CHANGED
|
@@ -52,8 +52,17 @@ import { generateBundleHash, generateSecret, shake256, generateBatchId } from '@
|
|
|
52
52
|
import WalletCredentialException from '@/exception/WalletCredentialException'
|
|
53
53
|
import { isBundleHash } from '@/types'
|
|
54
54
|
import TokenUnit from '@/core/TokenUnit'
|
|
55
|
-
// Post-quantum cryptography for ML-
|
|
56
|
-
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js'
|
|
55
|
+
// Post-quantum cryptography for ML-KEM key encapsulation
|
|
56
|
+
import { ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem.js'
|
|
57
|
+
|
|
58
|
+
const ML_KEM_PARAMS = {
|
|
59
|
+
1024: { kem: ml_kem1024, pkBytes: 1568, skBytes: 3168, ctBytes: 1568 },
|
|
60
|
+
768: { kem: ml_kem768, pkBytes: 1184, skBytes: 2400, ctBytes: 1088 }
|
|
61
|
+
} as const
|
|
62
|
+
const DEFAULT_ML_KEM_PARAMETER_SET = 1024
|
|
63
|
+
|
|
64
|
+
type MlKemParams = (typeof ML_KEM_PARAMS)[1024 | 768]
|
|
65
|
+
type MlKemKeypair = { pubkey: string; privkey: Uint8Array; params: MlKemParams }
|
|
57
66
|
|
|
58
67
|
/**
|
|
59
68
|
* Wallet class - Identity and key management for KnishIO DLT
|
|
@@ -73,6 +82,7 @@ export default class Wallet {
|
|
|
73
82
|
public tokenUnits: any[]
|
|
74
83
|
public tradeRates: Record<string, any>
|
|
75
84
|
public molecules: Record<string, any>
|
|
85
|
+
public mlKemParameterSet: 1024 | 768
|
|
76
86
|
|
|
77
87
|
// Token metadata (populated from query responses)
|
|
78
88
|
public tokenName?: string
|
|
@@ -88,7 +98,8 @@ export default class Wallet {
|
|
|
88
98
|
address = null,
|
|
89
99
|
position = null,
|
|
90
100
|
batchId = null,
|
|
91
|
-
characters = null
|
|
101
|
+
characters = null,
|
|
102
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
92
103
|
}: {
|
|
93
104
|
secret?: string | null
|
|
94
105
|
bundle?: string | null
|
|
@@ -97,7 +108,13 @@ export default class Wallet {
|
|
|
97
108
|
position?: string | null
|
|
98
109
|
batchId?: string | null
|
|
99
110
|
characters?: string | null
|
|
111
|
+
mlKemParameterSet?: 1024 | 768
|
|
100
112
|
} = {}) {
|
|
113
|
+
const paramSetNum = Number(mlKemParameterSet) as 1024 | 768
|
|
114
|
+
if (!ML_KEM_PARAMS[paramSetNum]) {
|
|
115
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${mlKemParameterSet}; expected 1024 or 768.`)
|
|
116
|
+
}
|
|
117
|
+
this.mlKemParameterSet = paramSetNum
|
|
101
118
|
this.token = token
|
|
102
119
|
this.balance = '0'
|
|
103
120
|
this.molecules = {}
|
|
@@ -133,7 +150,7 @@ export default class Wallet {
|
|
|
133
150
|
// Set characters
|
|
134
151
|
this.characters = this.characters || 'BASE64'
|
|
135
152
|
|
|
136
|
-
// Initialize ML-
|
|
153
|
+
// Initialize ML-KEM keys (matches JavaScript SDK)
|
|
137
154
|
this.initializeMLKEM()
|
|
138
155
|
}
|
|
139
156
|
}
|
|
@@ -147,13 +164,15 @@ export default class Wallet {
|
|
|
147
164
|
bundle = null,
|
|
148
165
|
token = 'USER',
|
|
149
166
|
batchId = null,
|
|
150
|
-
characters = null
|
|
167
|
+
characters = null,
|
|
168
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
151
169
|
}: {
|
|
152
170
|
secret?: string | null
|
|
153
171
|
bundle?: string | null
|
|
154
172
|
token?: string
|
|
155
173
|
batchId?: string | null
|
|
156
174
|
characters?: string | null
|
|
175
|
+
mlKemParameterSet?: 1024 | 768
|
|
157
176
|
}): Wallet {
|
|
158
177
|
let position: string | null = null
|
|
159
178
|
|
|
@@ -176,7 +195,8 @@ export default class Wallet {
|
|
|
176
195
|
token,
|
|
177
196
|
position,
|
|
178
197
|
batchId,
|
|
179
|
-
characters
|
|
198
|
+
characters,
|
|
199
|
+
mlKemParameterSet
|
|
180
200
|
})
|
|
181
201
|
}
|
|
182
202
|
|
|
@@ -408,26 +428,90 @@ export default class Wallet {
|
|
|
408
428
|
}
|
|
409
429
|
|
|
410
430
|
// =============================================================================
|
|
411
|
-
// POST-QUANTUM CRYPTOGRAPHY - ML-
|
|
431
|
+
// POST-QUANTUM CRYPTOGRAPHY - ML-KEM INTEGRATION
|
|
412
432
|
// =============================================================================
|
|
413
433
|
|
|
414
434
|
/**
|
|
415
|
-
*
|
|
435
|
+
* Derive an ML-KEM keypair for an arbitrary parameter set from the wallet's key seed,
|
|
436
|
+
* without mutating the wallet. The 64-byte `d‖z` seed takes no parameter-set input — only
|
|
437
|
+
* the final `keygen` call differs — so one KnishIO wallet owns both an ML-KEM-768 and an
|
|
438
|
+
* ML-KEM-1024 identity and either can be reconstructed on demand.
|
|
439
|
+
*
|
|
440
|
+
* Returns `null` when the wallet holds no key — a secret-less wallet, which is what a molecule
|
|
441
|
+
* deserializer builds for validation context. `generateSecret(null, …)` does NOT throw, so
|
|
442
|
+
* without this the wallet would derive a plausible-looking identity from a bogus seed and fail
|
|
443
|
+
* three layers down at AES-GCM instead of at the missing key. The guard lives here rather than
|
|
444
|
+
* at each call site so a new caller cannot miss it.
|
|
445
|
+
*
|
|
446
|
+
* @param parameterSet - 1024 or 768
|
|
416
447
|
*/
|
|
417
|
-
|
|
448
|
+
private deriveMlKemKeypair(parameterSet: 1024 | 768): MlKemKeypair | null {
|
|
449
|
+
const params = ML_KEM_PARAMS[parameterSet]
|
|
450
|
+
if (!params) {
|
|
451
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`)
|
|
452
|
+
}
|
|
453
|
+
if (!this.key) {
|
|
454
|
+
return null
|
|
455
|
+
}
|
|
418
456
|
// Generate a 64-byte (512-bit) seed from the Knish.IO private key
|
|
419
457
|
// Use deterministic approach: generateSecret(key, 128) → 128 hex chars = 64 bytes
|
|
420
|
-
const seedHex = generateSecret(this.key
|
|
421
|
-
|
|
422
|
-
// Convert the hex string to a Uint8Array
|
|
458
|
+
const seedHex = generateSecret(this.key, 128) // 128 hex chars = 64 bytes, matches JS SDK
|
|
459
|
+
|
|
460
|
+
// Convert the hex string to a Uint8Array
|
|
423
461
|
const seed = new Uint8Array(64)
|
|
424
462
|
for (let i = 0; i < 64; i++) {
|
|
425
463
|
seed[i] = parseInt(seedHex.substr(i * 2, 2), 16)
|
|
426
464
|
}
|
|
427
|
-
|
|
428
|
-
const { publicKey, secretKey } =
|
|
429
|
-
|
|
430
|
-
|
|
465
|
+
|
|
466
|
+
const { publicKey, secretKey } = params.kem.keygen(seed)
|
|
467
|
+
return {
|
|
468
|
+
pubkey: this.serializeKey(publicKey),
|
|
469
|
+
privkey: secretKey,
|
|
470
|
+
params
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* ML-KEM parameter set implied by a serialized public key's raw byte length. FIPS 203's key
|
|
476
|
+
* lengths are disjoint (1568 bytes → ML-KEM-1024, 1184 bytes → ML-KEM-768), so a stored peer
|
|
477
|
+
* key recovers the parameter set of the session it belongs to without a wire-format change.
|
|
478
|
+
* Used by AuthToken.restore to resolve a snapshot that predates the field.
|
|
479
|
+
*
|
|
480
|
+
* @param pubkey - Base64-serialized ML-KEM public key
|
|
481
|
+
* @return 1024, 768, or null when the length matches neither
|
|
482
|
+
*/
|
|
483
|
+
static mlKemParameterSetFromPubkey(pubkey: string | null | undefined): 1024 | 768 | null {
|
|
484
|
+
if (!pubkey) {
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
487
|
+
let byteLength: number
|
|
488
|
+
try {
|
|
489
|
+
byteLength = typeof Buffer !== 'undefined'
|
|
490
|
+
? Buffer.from(pubkey, 'base64').length
|
|
491
|
+
: atob(pubkey).length
|
|
492
|
+
} catch {
|
|
493
|
+
return null
|
|
494
|
+
}
|
|
495
|
+
if (byteLength === ML_KEM_PARAMS[1024].pkBytes) {
|
|
496
|
+
return 1024
|
|
497
|
+
}
|
|
498
|
+
if (byteLength === ML_KEM_PARAMS[768].pkBytes) {
|
|
499
|
+
return 768
|
|
500
|
+
}
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Initializes the ML-KEM key pair (matches JavaScript SDK exactly). Only ever reached from the
|
|
506
|
+
* constructor's `secret` branch, so the derivation cannot come back empty here.
|
|
507
|
+
*/
|
|
508
|
+
initializeMLKEM(): void {
|
|
509
|
+
const derived = this.deriveMlKemKeypair(this.mlKemParameterSet)
|
|
510
|
+
if (!derived) {
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
this.pubkey = derived.pubkey
|
|
514
|
+
this.privkey = derived.privkey // Note: We're keeping privkey as UInt8Array for security
|
|
431
515
|
}
|
|
432
516
|
|
|
433
517
|
|
|
@@ -439,19 +523,20 @@ export default class Wallet {
|
|
|
439
523
|
const messageString = JSON.stringify(message)
|
|
440
524
|
const messageUint8 = new TextEncoder().encode(messageString)
|
|
441
525
|
const deserializedPubkey = this.deserializeKey(recipientPubkey)
|
|
442
|
-
// ML-KEM
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
|
|
526
|
+
// ML-KEM public keys are exactly the configured parameter set's length — 1568 bytes for
|
|
527
|
+
// ML-KEM-1024, 1184 bytes for ML-KEM-768. A wrong-length key here almost always means the
|
|
528
|
+
// node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator
|
|
529
|
+
// predating the PQ-transport build). Fail with an actionable message rather than @noble's
|
|
530
|
+
// cryptic `"publicKey" expected Uint8Array of length N, got length=M` assertion.
|
|
531
|
+
const params = ML_KEM_PARAMS[this.mlKemParameterSet]
|
|
532
|
+
if (deserializedPubkey.length !== params.pkBytes) {
|
|
448
533
|
throw new Error(
|
|
449
534
|
`KnishIO: cannot ML-KEM-encrypt — recipient public key is ${deserializedPubkey.length} bytes, ` +
|
|
450
|
-
`expected ${
|
|
451
|
-
|
|
535
|
+
`expected ${params.pkBytes} (ML-KEM-${this.mlKemParameterSet}). The peer is not running ML-KEM-${this.mlKemParameterSet}; ` +
|
|
536
|
+
'upgrade the peer, or step this client back to the other parameter set.'
|
|
452
537
|
)
|
|
453
538
|
}
|
|
454
|
-
const { cipherText, sharedSecret } =
|
|
539
|
+
const { cipherText, sharedSecret } = params.kem.encapsulate(deserializedPubkey)
|
|
455
540
|
const encryptedMessage = await this.encryptWithSharedSecret(messageUint8, sharedSecret)
|
|
456
541
|
return {
|
|
457
542
|
cipherText: this.serializeKey(cipherText),
|
|
@@ -465,15 +550,46 @@ export default class Wallet {
|
|
|
465
550
|
}
|
|
466
551
|
|
|
467
552
|
/**
|
|
468
|
-
* ML-
|
|
553
|
+
* ML-KEM decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string (no JSON.parse).
|
|
469
554
|
* Shared by {@link decryptMessage} (which JSON.parses the result) and the PQ CipherHash transport
|
|
470
|
-
* ({@link
|
|
555
|
+
* ({@link decryptMyMessageML}, which needs the raw response JSON text). PQ-transport Phase E.
|
|
471
556
|
*/
|
|
472
557
|
async _mlkemDecryptToString(encryptedData: { cipherText: string; encryptedMessage: string }): Promise<string | null> {
|
|
473
558
|
const { cipherText, encryptedMessage } = encryptedData
|
|
559
|
+
const configuredParams = ML_KEM_PARAMS[this.mlKemParameterSet]
|
|
560
|
+
const otherSet: 1024 | 768 = this.mlKemParameterSet === 1024 ? 768 : 1024
|
|
561
|
+
const deserializedCipherText = this.deserializeKey(cipherText)
|
|
562
|
+
|
|
563
|
+
// Inbound is PERMISSIVE: a ciphertext at either parameter set decrypts, provided it is addressed
|
|
564
|
+
// to one of THIS wallet's own ML-KEM identities. The 64-byte seed is parameter-set-independent,
|
|
565
|
+
// so the other identity is derived on demand and its private key is released with this call's
|
|
566
|
+
// scope — never cached on the wallet. Outbound encapsulation stays STRICT (see encryptMessage);
|
|
567
|
+
// reading a 768 record we own downgrades nothing, but encapsulating at 768 would.
|
|
568
|
+
let params: MlKemParams = configuredParams
|
|
569
|
+
let decapsPrivkey: Uint8Array = this.privkey
|
|
570
|
+
if (deserializedCipherText.length !== configuredParams.ctBytes) {
|
|
571
|
+
if (deserializedCipherText.length !== ML_KEM_PARAMS[otherSet].ctBytes) {
|
|
572
|
+
console.error(
|
|
573
|
+
`Wallet::decryptMessage() - Ciphertext length mismatch: got ${deserializedCipherText.length}, expected ${configuredParams.ctBytes}`
|
|
574
|
+
)
|
|
575
|
+
return null
|
|
576
|
+
}
|
|
577
|
+
// `null` here means the wallet holds no key to derive from (a secret-less validation
|
|
578
|
+
// wallet); preserve the existing failure observable rather than decapsulating with nothing.
|
|
579
|
+
const derived = this.deriveMlKemKeypair(otherSet)
|
|
580
|
+
if (!derived) {
|
|
581
|
+
console.error(
|
|
582
|
+
`Wallet::decryptMessage() - cannot derive the ML-KEM-${otherSet} identity: wallet has no key`
|
|
583
|
+
)
|
|
584
|
+
return null
|
|
585
|
+
}
|
|
586
|
+
params = derived.params
|
|
587
|
+
decapsPrivkey = derived.privkey
|
|
588
|
+
}
|
|
589
|
+
|
|
474
590
|
let sharedSecret
|
|
475
591
|
try {
|
|
476
|
-
sharedSecret =
|
|
592
|
+
sharedSecret = params.kem.decapsulate(deserializedCipherText, decapsPrivkey)
|
|
477
593
|
} catch (e) {
|
|
478
594
|
console.error('Wallet::decryptMessage() - Decapsulation failed', e)
|
|
479
595
|
console.info('Wallet::decryptMessage() - my public key', this.pubkey)
|
|
@@ -525,11 +641,11 @@ export default class Wallet {
|
|
|
525
641
|
}
|
|
526
642
|
|
|
527
643
|
/**
|
|
528
|
-
* Post-quantum (ML-
|
|
644
|
+
* Post-quantum (ML-KEM) CipherHash request envelope: a stringified single-recipient map
|
|
529
645
|
* `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued, via
|
|
530
646
|
* {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ-transport Phase E.
|
|
531
647
|
*/
|
|
532
|
-
async
|
|
648
|
+
async encryptStringML(message: any, recipientPubkey: string): Promise<string> {
|
|
533
649
|
const envelope = await this.encryptMessage(message, recipientPubkey)
|
|
534
650
|
return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope })
|
|
535
651
|
}
|
|
@@ -538,9 +654,22 @@ export default class Wallet {
|
|
|
538
654
|
* Decrypt a CipherHash response map addressed to THIS wallet's ML-KEM pubkey
|
|
539
655
|
* (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
|
|
540
656
|
* it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
|
|
657
|
+
*
|
|
658
|
+
* A pre-bump peer addressed its envelope to `hashShare(our_768_pubkey)`, which a wallet
|
|
659
|
+
* configured at ML-KEM-1024 would never find — so the other identity's share is tried too.
|
|
660
|
+
* Without this, the permissive length dispatch in {@link _mlkemDecryptToString} is
|
|
661
|
+
* unreachable on the transport path.
|
|
541
662
|
*/
|
|
542
|
-
async
|
|
543
|
-
|
|
663
|
+
async decryptMyMessageML(map: Record<string, { cipherText: string; encryptedMessage: string }>): Promise<string | null> {
|
|
664
|
+
let envelope = map[this.hashShare(this.pubkey)]
|
|
665
|
+
if (!envelope) {
|
|
666
|
+
// A secret-less wallet derives nothing, so the lookup is simply skipped.
|
|
667
|
+
const otherSet: 1024 | 768 = this.mlKemParameterSet === 1024 ? 768 : 1024
|
|
668
|
+
const other = this.deriveMlKemKeypair(otherSet)
|
|
669
|
+
if (other) {
|
|
670
|
+
envelope = map[this.hashShare(other.pubkey)]
|
|
671
|
+
}
|
|
672
|
+
}
|
|
544
673
|
if (!envelope) {
|
|
545
674
|
return null
|
|
546
675
|
}
|
|
@@ -94,4 +94,13 @@ export default class SecretStorageException extends BaseException {
|
|
|
94
94
|
}
|
|
95
95
|
)
|
|
96
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validation error for storage options or parameters
|
|
100
|
+
*/
|
|
101
|
+
static validationError(message: string): SecretStorageException {
|
|
102
|
+
return new SecretStorageException(message, {
|
|
103
|
+
code: 'VALIDATION_ERROR'
|
|
104
|
+
})
|
|
105
|
+
}
|
|
97
106
|
}
|
package/src/index.ts
CHANGED
|
@@ -275,10 +275,23 @@ export {
|
|
|
275
275
|
export {
|
|
276
276
|
MemorySecretStorageProvider,
|
|
277
277
|
WebCryptoSecretStorageProvider,
|
|
278
|
+
WebAuthnPrfSecretStorageProvider,
|
|
279
|
+
NonExtractableKeySecretStorageProvider,
|
|
280
|
+
IndexedDbKeyStore,
|
|
281
|
+
MemoryKeyStore,
|
|
278
282
|
MemoryStorageBackend,
|
|
283
|
+
FileStorageBackend,
|
|
284
|
+
WebStorageBackend,
|
|
279
285
|
createDefaultSecretStorage,
|
|
286
|
+
sealEnvelope,
|
|
287
|
+
openEnvelope,
|
|
288
|
+
SECRET_KEY_PREFIX,
|
|
289
|
+
RECOVERY_KEY_PREFIX,
|
|
280
290
|
type IStorageBackend,
|
|
281
|
-
type
|
|
291
|
+
type IKeyStore,
|
|
292
|
+
type CreateSecretStorageOptions,
|
|
293
|
+
type WebAuthnPrfSecretStorageOptions,
|
|
294
|
+
type NonExtractableKeyStorageOptions
|
|
282
295
|
} from './storage'
|
|
283
296
|
|
|
284
297
|
export {
|
|
@@ -291,6 +304,7 @@ export {
|
|
|
291
304
|
export type {
|
|
292
305
|
SecretStorageMetadata,
|
|
293
306
|
EncryptedSecretPayload,
|
|
307
|
+
StorageOptions,
|
|
294
308
|
ISecretStorageProvider
|
|
295
309
|
} from './types/storage'
|
|
296
310
|
|
|
@@ -389,7 +403,7 @@ export {
|
|
|
389
403
|
// MUST equal package.json's "version". The CI `version consistency` job enforces this via
|
|
390
404
|
// .github/scripts/check-version.sh, which parses this exact line — keep the literal form
|
|
391
405
|
// `export const SDK_VERSION = '<semver>'` intact so the gate can read it.
|
|
392
|
-
export const SDK_VERSION = '
|
|
406
|
+
export const SDK_VERSION = '1.1.0'
|
|
393
407
|
export const SDK_NAME = 'KnishIO-Client-TS'
|
|
394
408
|
export const COMPATIBLE_SERVER_VERSIONS = [4, 5]
|
|
395
409
|
|
|
@@ -402,7 +416,7 @@ export const SDK_INFO = {
|
|
|
402
416
|
description: 'TypeScript SDK for Knish.IO post-blockchain distributed ledger',
|
|
403
417
|
compatibleServerVersions: COMPATIBLE_SERVER_VERSIONS,
|
|
404
418
|
features: [
|
|
405
|
-
'Post-quantum cryptography (XMSS, ML-
|
|
419
|
+
'Post-quantum cryptography (XMSS, ML-KEM-1024)',
|
|
406
420
|
'Cross-platform compatibility',
|
|
407
421
|
'Type-safe APIs',
|
|
408
422
|
'DAG-based transaction processing',
|
|
@@ -204,7 +204,7 @@ export default class GraphQLClient implements IGraphQLClient {
|
|
|
204
204
|
let requestInit = init
|
|
205
205
|
|
|
206
206
|
if (wallet && serverPubkey && init && typeof init.body === 'string' && this.shouldEncrypt(init.body)) {
|
|
207
|
-
const hashVar = await wallet.
|
|
207
|
+
const hashVar = await wallet.encryptStringML(init.body, serverPubkey)
|
|
208
208
|
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) }
|
|
209
209
|
encryptedRequest = true
|
|
210
210
|
}
|
|
@@ -228,7 +228,7 @@ export default class GraphQLClient implements IGraphQLClient {
|
|
|
228
228
|
// Plaintext (e.g. a validator-side error response) — pass through unchanged.
|
|
229
229
|
return new Response(text, init2)
|
|
230
230
|
}
|
|
231
|
-
const decrypted = await wallet!.
|
|
231
|
+
const decrypted = await wallet!.decryptMyMessageML(JSON.parse(hash))
|
|
232
232
|
return new Response(decrypted != null ? decrypted : text, init2)
|
|
233
233
|
}
|
|
234
234
|
|
package/src/schemas/index.ts
CHANGED
|
@@ -208,7 +208,8 @@ export const KnishIOClientConfigSchema = z.object({
|
|
|
208
208
|
serverSdkVersion: z.number().int().min(1).optional(),
|
|
209
209
|
logging: z.boolean().optional(),
|
|
210
210
|
defaultRequestPolicy: z.enum(['cache-first', 'cache-only', 'network-only', 'cache-and-network']).nullable().optional(),
|
|
211
|
-
secretStorage: z.unknown().optional()
|
|
211
|
+
secretStorage: z.unknown().optional(),
|
|
212
|
+
mlKemParameterSet: z.union([z.literal(1024), z.literal(768)]).optional()
|
|
212
213
|
}).strict()
|
|
213
214
|
|
|
214
215
|
// =============================================================================
|
|
@@ -0,0 +1,176 @@
|
|
|
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-TS for information.
|
|
45
|
+
|
|
46
|
+
License: https://github.com/WishKnish/KnishIO-Client-TS/blob/master/LICENSE
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { IStorageBackend } from './WebCryptoSecretStorageProvider'
|
|
50
|
+
import SecretStorageException from '@/exception/SecretStorageException'
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Node-only persistent file storage backend.
|
|
54
|
+
* Stores key-value entries in a JSON file with restrictive permissions (0o600).
|
|
55
|
+
* Uses temporary file writing followed by atomic rename to prevent corruption.
|
|
56
|
+
*/
|
|
57
|
+
export default class FileStorageBackend implements IStorageBackend {
|
|
58
|
+
readonly filePath: string
|
|
59
|
+
private store: Map<string, string> = new Map()
|
|
60
|
+
private loaded = false
|
|
61
|
+
|
|
62
|
+
constructor(filePath: string) {
|
|
63
|
+
if (!filePath) {
|
|
64
|
+
throw new SecretStorageException('Storage file path cannot be empty')
|
|
65
|
+
}
|
|
66
|
+
this.filePath = filePath
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async getFs() {
|
|
70
|
+
try {
|
|
71
|
+
// Platform-specific: node:fs and node:path do not exist in browser runtimes
|
|
72
|
+
// and cannot be statically imported without breaking browser bundles.
|
|
73
|
+
const fs = await import('node:fs/promises')
|
|
74
|
+
const path = await import('node:path')
|
|
75
|
+
return { fs, path }
|
|
76
|
+
} catch {
|
|
77
|
+
throw SecretStorageException.unavailable(
|
|
78
|
+
'file-storage',
|
|
79
|
+
'FileStorageBackend is only supported in Node.js environments with node:fs access'
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async ensureLoaded(): Promise<Map<string, string>> {
|
|
85
|
+
if (this.loaded) {
|
|
86
|
+
return this.store
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { fs } = await this.getFs()
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const content = await fs.readFile(this.filePath, 'utf8')
|
|
93
|
+
let parsed: Record<string, unknown>
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(content)
|
|
96
|
+
} catch {
|
|
97
|
+
throw SecretStorageException.decryptionFailed('Corrupted storage file format')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (parsed && typeof parsed === 'object') {
|
|
101
|
+
this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]))
|
|
102
|
+
}
|
|
103
|
+
} catch (err: unknown) {
|
|
104
|
+
if (err instanceof SecretStorageException) {
|
|
105
|
+
throw err
|
|
106
|
+
}
|
|
107
|
+
// If file does not exist (ENOENT), treat as empty map
|
|
108
|
+
const nodeErr = err as { code?: string }
|
|
109
|
+
if (nodeErr?.code !== 'ENOENT') {
|
|
110
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
111
|
+
throw new SecretStorageException(`Failed to read storage file: ${msg}`)
|
|
112
|
+
}
|
|
113
|
+
this.store = new Map()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.loaded = true
|
|
117
|
+
return this.store
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async persist(): Promise<void> {
|
|
121
|
+
const { fs, path } = await this.getFs()
|
|
122
|
+
|
|
123
|
+
const dir = path.dirname(this.filePath)
|
|
124
|
+
if (dir && dir !== '.') {
|
|
125
|
+
await fs.mkdir(dir, { recursive: true })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`
|
|
129
|
+
const data = JSON.stringify(Object.fromEntries(this.store), null, 2)
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
await fs.writeFile(tmpPath, data, { mode: 0o600, encoding: 'utf8' })
|
|
133
|
+
if (typeof process !== 'undefined' && process.platform !== 'win32') {
|
|
134
|
+
try {
|
|
135
|
+
await fs.chmod(tmpPath, 0o600)
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore chmod errors if file system does not support it
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
await fs.rename(tmpPath, this.filePath)
|
|
141
|
+
} catch (err: unknown) {
|
|
142
|
+
try {
|
|
143
|
+
await fs.unlink(tmpPath)
|
|
144
|
+
} catch {
|
|
145
|
+
// Ignore unlink cleanup error
|
|
146
|
+
}
|
|
147
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
148
|
+
throw new SecretStorageException(`Failed to persist storage file: ${msg}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getItem(key: string): Promise<string | null> {
|
|
153
|
+
await this.ensureLoaded()
|
|
154
|
+
return this.store.get(key) ?? null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
158
|
+
await this.ensureLoaded()
|
|
159
|
+
this.store.set(key, value)
|
|
160
|
+
await this.persist()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async removeItem(key: string): Promise<boolean> {
|
|
164
|
+
await this.ensureLoaded()
|
|
165
|
+
const existed = this.store.delete(key)
|
|
166
|
+
if (existed) {
|
|
167
|
+
await this.persist()
|
|
168
|
+
}
|
|
169
|
+
return existed
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async keys(): Promise<string[]> {
|
|
173
|
+
await this.ensureLoaded()
|
|
174
|
+
return Array.from(this.store.keys())
|
|
175
|
+
}
|
|
176
|
+
}
|