@wishknish/knishio-client-ts 0.9.7 → 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.
@@ -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-KEM768 key encapsulation
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-KEM768 keys (matches JavaScript SDK)
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-KEM768 INTEGRATION
431
+ // POST-QUANTUM CRYPTOGRAPHY - ML-KEM INTEGRATION
412
432
  // =============================================================================
413
433
 
414
434
  /**
415
- * Initializes the ML-KEM key pair (matches JavaScript SDK exactly)
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
- initializeMLKEM(): void {
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!, 128) // 128 hex chars = 64 bytes, matches JS SDK
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 } = ml_kem768.keygen(seed)
429
- this.pubkey = this.serializeKey(publicKey)
430
- this.privkey = secretKey // Note: We're keeping privkey as UInt8Array for security
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-768 public keys are exactly 1184 bytes. A wrong-length key here almost always means the
443
- // node did not advertise an ML-KEM public key in its auth `key` field (e.g. a validator predating
444
- // the PQ-transport build). Fail with an actionable message rather than @noble's cryptic
445
- // `"publicKey" expected Uint8Array of length 1184, got length=N` assertion.
446
- const ML_KEM_768_PUBLIC_KEY_BYTES = 1184
447
- if (deserializedPubkey.length !== ML_KEM_768_PUBLIC_KEY_BYTES) {
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 ${ML_KEM_768_PUBLIC_KEY_BYTES} (ML-KEM-768). The node likely did not advertise an ML-KEM ` +
451
- `public key (upgrade the validator to a PQ-transport build), or authenticate with { encrypt: false }.`
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 } = ml_kem768.encapsulate(deserializedPubkey)
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-KEM768 decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string (no JSON.parse).
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 decryptMyMessageML768}, which needs the raw response JSON text). PQ-transport Phase E.
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 = ml_kem768.decapsulate(this.deserializeKey(cipherText), this.privkey)
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-KEM768) CipherHash request envelope: a stringified single-recipient map
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 encryptStringML768(message: any, recipientPubkey: string): Promise<string> {
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 decryptMyMessageML768(map: Record<string, { cipherText: string; encryptedMessage: string }>): Promise<string | null> {
543
- const envelope = map[this.hashShare(this.pubkey)]
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
  }
package/src/index.ts CHANGED
@@ -389,7 +389,7 @@ export {
389
389
  // MUST equal package.json's "version". The CI `version consistency` job enforces this via
390
390
  // .github/scripts/check-version.sh, which parses this exact line — keep the literal form
391
391
  // `export const SDK_VERSION = '<semver>'` intact so the gate can read it.
392
- export const SDK_VERSION = '0.9.7'
392
+ export const SDK_VERSION = '1.0.0'
393
393
  export const SDK_NAME = 'KnishIO-Client-TS'
394
394
  export const COMPATIBLE_SERVER_VERSIONS = [4, 5]
395
395
 
@@ -402,7 +402,7 @@ export const SDK_INFO = {
402
402
  description: 'TypeScript SDK for Knish.IO post-blockchain distributed ledger',
403
403
  compatibleServerVersions: COMPATIBLE_SERVER_VERSIONS,
404
404
  features: [
405
- 'Post-quantum cryptography (XMSS, ML-KEM768)',
405
+ 'Post-quantum cryptography (XMSS, ML-KEM-1024)',
406
406
  'Cross-platform compatibility',
407
407
  'Type-safe APIs',
408
408
  '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.encryptStringML768(init.body, serverPubkey)
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!.decryptMyMessageML768(JSON.parse(hash))
231
+ const decrypted = await wallet!.decryptMyMessageML(JSON.parse(hash))
232
232
  return new Response(decrypted != null ? decrypted : text, init2)
233
233
  }
234
234
 
@@ -57,7 +57,7 @@ import type Molecule from '../core/Molecule'
57
57
  * MutationProposeMolecule - Foundation for all molecular proposals
58
58
  * Matches JavaScript SDK MutationProposeMolecule implementation exactly
59
59
  */
60
- export default abstract class MutationProposeMolecule extends Mutation {
60
+ export default class MutationProposeMolecule extends Mutation {
61
61
  protected $__molecule: Molecule
62
62
  protected $__remainderWallet: any | null = null
63
63
 
@@ -141,8 +141,11 @@ export default abstract class MutationProposeMolecule extends Mutation {
141
141
  }
142
142
 
143
143
  /**
144
- * Abstract method to be implemented by subclasses
145
- * Fills the molecule with specific mutation data
144
+ * Fills the molecule with specific mutation data.
145
+ * Subclasses override to build domain-specific atoms.
146
+ * Default implementation is a no-op for pre-assembled molecules.
146
147
  */
147
- abstract fillMolecule(params: any): void
148
+ fillMolecule(_params?: any): void {
149
+ // Intentionally empty for pre-assembled molecules proposed directly
150
+ }
148
151
  }
@@ -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
  // =============================================================================
@@ -58,8 +58,8 @@ import type { WalletAddress, BundleHash, Position, TokenSlug } from './index'
58
58
  // =============================================================================
59
59
 
60
60
  export type HashAlgorithm = 'SHAKE256' | 'SHA3-256' | 'BLAKE2B'
61
- export type SignatureAlgorithm = 'XMSS' | 'ML-KEM768' | 'SPHINCS+'
62
- export type EncryptionAlgorithm = 'ML-KEM768' | 'Kyber768' | 'ChaCha20Poly1305'
61
+ export type SignatureAlgorithm = 'XMSS' | 'SPHINCS+'
62
+ export type EncryptionAlgorithm = 'ML-KEM-1024' | 'ML-KEM-768'
63
63
 
64
64
  // =============================================================================
65
65
  // SHAKE256 SPECIFIC TYPES
@@ -139,13 +139,13 @@ export interface XMSSSignature {
139
139
  }
140
140
 
141
141
  // =============================================================================
142
- // ML-KEM768 (Post-Quantum Key Encapsulation) TYPES
142
+ // ML-KEM (Post-Quantum Key Encapsulation) TYPES
143
143
  // =============================================================================
144
144
 
145
145
  export interface MLKEMKeyPair {
146
146
  privateKey: Uint8Array
147
147
  publicKey: Uint8Array
148
- algorithm: 'ML-KEM768'
148
+ algorithm: EncryptionAlgorithm
149
149
  }
150
150
 
151
151
  export interface MLKEMEncapsulationResult {
@@ -475,6 +475,11 @@ export const CRYPTO_CONSTANTS = {
475
475
  ML_KEM768_PRIVATE_KEY_SIZE: 2400,
476
476
  ML_KEM768_CIPHERTEXT_SIZE: 1088,
477
477
  ML_KEM768_SHARED_SECRET_SIZE: 32,
478
+
479
+ ML_KEM1024_PUBLIC_KEY_SIZE: 1568,
480
+ ML_KEM1024_PRIVATE_KEY_SIZE: 3168,
481
+ ML_KEM1024_CIPHERTEXT_SIZE: 1568,
482
+ ML_KEM1024_SHARED_SECRET_SIZE: 32,
478
483
 
479
484
  KEY_FRAGMENT_SIZE: 128,
480
485
  OTS_FRAGMENT_COUNT: 16,
@@ -231,7 +231,8 @@ export const KnishIOClientConfigSchema = z.object({
231
231
  // isn't rejected.
232
232
  defaultRequestPolicy: z.enum(['cache-first', 'cache-only', 'network-only', 'cache-and-network']).nullable().optional(),
233
233
  // Pluggable hardware envelope encryption secret storage provider
234
- secretStorage: z.unknown().optional()
234
+ secretStorage: z.unknown().optional(),
235
+ mlKemParameterSet: z.union([z.literal(1024), z.literal(768)]).optional()
235
236
  }).strict()
236
237
 
237
238
  // Environment configuration with validation.