@wishknish/knishio-client-js 0.9.2 → 0.9.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishknish/knishio-client-js",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
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.",
@@ -96,5 +96,5 @@
96
96
  "/src",
97
97
  "/dist"
98
98
  ],
99
- "packageManager": "yarn@4.9.2"
99
+ "packageManager": "yarn@4.15.0"
100
100
  }
@@ -46,6 +46,7 @@ Please visit https://github.com/WishKnish/KnishIO-Client-JS for information.
46
46
  License: https://github.com/WishKnish/KnishIO-Client-JS/blob/master/LICENSE
47
47
  */
48
48
  import Dot from './libraries/Dot.js'
49
+ import MemorySecretStorageProvider from './storage/MemorySecretStorageProvider.js'
49
50
  import Decimal from './libraries/Decimal.js'
50
51
  import {
51
52
  generateBatchId,
@@ -127,7 +128,8 @@ export default class KnishIOClient {
127
128
  socket = null,
128
129
  serverSdkVersion = 3,
129
130
  logging = false,
130
- defaultRequestPolicy = null
131
+ defaultRequestPolicy = null,
132
+ secretStorage = null
131
133
  }) {
132
134
  this.initialize({
133
135
  uri,
@@ -136,7 +138,8 @@ export default class KnishIOClient {
136
138
  client,
137
139
  serverSdkVersion,
138
140
  logging,
139
- defaultRequestPolicy
141
+ defaultRequestPolicy,
142
+ secretStorage
140
143
  })
141
144
  }
142
145
 
@@ -157,10 +160,12 @@ export default class KnishIOClient {
157
160
  client = null,
158
161
  serverSdkVersion = 3,
159
162
  logging = false,
160
- defaultRequestPolicy = null
163
+ defaultRequestPolicy = null,
164
+ secretStorage = null
161
165
  }) {
162
166
  this.reset()
163
167
 
168
+ this.$__secretStorage = secretStorage
164
169
  this.$__logging = logging
165
170
  // Client-level urql request policy applied to reads that omit a per-call
166
171
  // policy. A long-lived server/sync client set to 'network-only' never serves
@@ -278,6 +283,7 @@ export default class KnishIOClient {
278
283
  reset () {
279
284
  this.$__secret = ''
280
285
  this.$__bundle = ''
286
+ this.$__secretStorage = null
281
287
  this.remainderWallet = null
282
288
  this.$__capabilityCache = {}
283
289
  this.$__defaultRequestPolicy = null
@@ -376,7 +382,7 @@ export default class KnishIOClient {
376
382
  * @return {boolean}
377
383
  */
378
384
  hasSecret () {
379
- return !!this.$__secret
385
+ return (!!this.$__secret && this.$__secret.length > 0) || (!!this.$__secretStorage && !!this.$__bundle && this.$__bundle.length > 0)
380
386
  }
381
387
 
382
388
  /**
@@ -387,6 +393,13 @@ export default class KnishIOClient {
387
393
  setSecret (secret) {
388
394
  this.$__secret = secret
389
395
  this.$__bundle = this.hashSecret(secret, 'setSecret')
396
+ if (!this.$__secretStorage) {
397
+ const memStorage = new MemorySecretStorageProvider()
398
+ memStorage.storeSecret(this.$__bundle, secret)
399
+ this.$__secretStorage = memStorage
400
+ } else {
401
+ this.$__secretStorage.storeSecret(this.$__bundle, secret)
402
+ }
390
403
  }
391
404
 
392
405
  /**
@@ -412,6 +425,44 @@ export default class KnishIOClient {
412
425
  return this.$__secret
413
426
  }
414
427
 
428
+ /**
429
+ * Sets the secret storage provider and optionally sets the bundle hash
430
+ *
431
+ * @param {object} storage
432
+ * @param {string|null} [bundleHash]
433
+ */
434
+ setSecretStorage (storage, bundleHash = null) {
435
+ this.$__secretStorage = storage
436
+ if (bundleHash) {
437
+ this.$__bundle = bundleHash
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Returns current secret storage provider
443
+ *
444
+ * @returns {object|null}
445
+ */
446
+ getSecretStorage () {
447
+ return this.$__secretStorage
448
+ }
449
+
450
+ /**
451
+ * Asynchronously retrieves the secret from storage or returns in-memory secret
452
+ *
453
+ * @param {object} [options]
454
+ * @returns {Promise<string|null>}
455
+ */
456
+ async retrieveSecret (options = {}) {
457
+ if (this.$__secret && this.$__secret.length > 0) {
458
+ return this.$__secret
459
+ }
460
+ if (this.$__secretStorage && this.$__bundle && this.$__bundle.length > 0) {
461
+ return await this.$__secretStorage.retrieveSecret(this.$__bundle, options)
462
+ }
463
+ return null
464
+ }
465
+
415
466
  /**
416
467
  * Returns whether a bundle hash is being stored for this session
417
468
  *
@@ -496,10 +547,16 @@ export default class KnishIOClient {
496
547
  remainderWallet = null
497
548
  }) {
498
549
  this.log('info', 'KnishIOClient::createMolecule() - Creating a new molecule...')
550
+ if (!secret) {
551
+ if (this.$__secret && this.$__secret.length > 0) {
552
+ secret = this.getSecret()
553
+ } else if (this.$__secretStorage && this.$__bundle && this.$__bundle.length > 0) {
554
+ secret = await this.$__secretStorage.retrieveSecret(this.$__bundle)
555
+ }
556
+ }
499
557
 
500
558
  secret = secret || this.getSecret()
501
559
  bundle = bundle || this.getBundle()
502
-
503
560
  // For non-USER source wallets (V/B isotopes), capture the current USER
504
561
  // ContinuID position BEFORE overwriting the client's remainder wallet.
505
562
  // This position is needed by addContinuIdAtom() for previousPosition metadata.
@@ -546,6 +603,7 @@ export default class KnishIOClient {
546
603
 
547
604
  return new Molecule({
548
605
  secret,
606
+ bundle,
549
607
  sourceWallet,
550
608
  remainderWallet: this.getRemainderWallet(),
551
609
  cellSlug: this.getCellSlug(),
@@ -611,8 +669,9 @@ export default class KnishIOClient {
611
669
  // Guard with $__authInProcess to prevent concurrent auth requests
612
670
  if (this.$__authToken && this.$__authToken.isExpired() && !this.$__authInProcess) {
613
671
  this.log('info', 'KnishIOClient::executeQuery() - Access token is expired. Getting new one...')
672
+ const authSecret = this.$__secret || (await this.retrieveSecret()) || ''
614
673
  await this.requestAuthToken({
615
- secret: this.$__secret,
674
+ secret: authSecret,
616
675
  cellSlug: this.$__cellSlug,
617
676
  encrypt: this.$__encrypt
618
677
  })
@@ -2464,6 +2523,11 @@ export default class KnishIOClient {
2464
2523
  this.setCellSlug(cellSlug)
2465
2524
  }
2466
2525
 
2526
+ // Retrieve secret from storage provider if available
2527
+ if (secret === null && this.$__secretStorage && this.$__bundle) {
2528
+ secret = await this.$__secretStorage.retrieveSecret(this.$__bundle)
2529
+ }
2530
+
2467
2531
  // Auth in process...
2468
2532
  this.$__authInProcess = true
2469
2533
 
package/src/Molecule.js CHANGED
@@ -1080,7 +1080,7 @@ export default class Molecule {
1080
1080
  bundle = null,
1081
1081
  anonymous = false,
1082
1082
  compressed = true
1083
- }) {
1083
+ } = {}) {
1084
1084
  // Do we have atoms?
1085
1085
  if (this.atoms.length === 0 || this.atoms.filter(atom => !(atom instanceof Atom)).length !== 0) {
1086
1086
  throw new AtomsMissingException()
package/src/Wallet.js CHANGED
@@ -300,10 +300,23 @@ export default class Wallet {
300
300
  }
301
301
 
302
302
  serializeKey (key) {
303
- return btoa(String.fromCharCode.apply(null, key))
303
+ if (typeof Buffer !== 'undefined') {
304
+ return Buffer.from(key).toString('base64')
305
+ }
306
+ // String.fromCharCode.apply(null, key) overflows the call stack once the
307
+ // payload exceeds the engine's argument limit (~64K), so convert in chunks.
308
+ let binary = ''
309
+ const CHUNK_SIZE = 0x8000
310
+ for (let i = 0; i < key.length; i += CHUNK_SIZE) {
311
+ binary += String.fromCharCode(...key.subarray(i, i + CHUNK_SIZE))
312
+ }
313
+ return btoa(binary)
304
314
  }
305
315
 
306
316
  deserializeKey (serializedKey) {
317
+ if (typeof Buffer !== 'undefined') {
318
+ return new Uint8Array(Buffer.from(serializedKey, 'base64'))
319
+ }
307
320
  const binaryString = atob(serializedKey)
308
321
  return new Uint8Array(binaryString.length).map((_, i) => binaryString.charCodeAt(i))
309
322
  }
@@ -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
+ }
@@ -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'
@@ -6,11 +6,13 @@
6
6
  * @return {array}
7
7
  */
8
8
  export function chunkArray (arr, size) {
9
- if (!arr.length) {
10
- return []
9
+ // Iterative: the previous recursive form (`concat` + `slice(size)` per level) blew
10
+ // the call stack and allocated O(n^2) once arr grew past a few thousand elements.
11
+ const chunks = []
12
+ for (let i = 0; i < arr.length; i += size) {
13
+ chunks.push(arr.slice(i, i + size))
11
14
  }
12
-
13
- return [arr.slice(0, size)].concat(chunkArray(arr.slice(size), size))
15
+ return chunks
14
16
  }
15
17
 
16
18
  export function deepCloning (
@@ -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
+ }
@@ -45,13 +45,18 @@ export function chunkSubstr (str, size) {
45
45
  * @return {string}
46
46
  */
47
47
  export function randomString (length = 256, alphabet = 'abcdef0123456789') {
48
- let array = new Uint8Array(length)
48
+ const array = new Uint8Array(length)
49
49
 
50
- array = crypto.getRandomValues(array)
50
+ crypto.getRandomValues(array)
51
51
 
52
- array = array.map(x => alphabet.charCodeAt(x % alphabet.length))
53
-
54
- return String.fromCharCode.apply(null, array)
52
+ // Build in a loop: String.fromCharCode.apply(null, array) spread every byte as a
53
+ // function argument and overflowed the call stack for a large caller-supplied length.
54
+ // `alphabet.charAt(x % len)` is the same mapping as the old fromCharCode(charCodeAt).
55
+ let result = ''
56
+ for (let i = 0; i < length; i++) {
57
+ result += alphabet.charAt(array[i] % alphabet.length)
58
+ }
59
+ return result
55
60
  }
56
61
 
57
62
  /**
@@ -120,7 +125,19 @@ export function hexStringToBuffer (hexString) {
120
125
  */
121
126
  export function hexToBase64 (string) {
122
127
  const bytes = hexStringToBuffer(string)
123
- return btoa(String.fromCharCode.apply(null, bytes))
128
+ // Buffer fast path (Node). Byte-identical to the btoa encoding below.
129
+ if (typeof Buffer !== 'undefined') {
130
+ return Buffer.from(bytes).toString('base64')
131
+ }
132
+ // Browser fallback: String.fromCharCode.apply(null, bytes) spreads every byte as a
133
+ // function argument and overflows the call stack past the engine's argument limit
134
+ // (~64K), so convert in chunks.
135
+ let binary = ''
136
+ const CHUNK_SIZE = 0x8000
137
+ for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
138
+ binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE))
139
+ }
140
+ return btoa(binary)
124
141
  }
125
142
 
126
143
  /**
@@ -130,7 +147,16 @@ export function hexToBase64 (string) {
130
147
  * @return {string}
131
148
  */
132
149
  export function base64ToHex (string) {
133
- const bytes = new Uint8Array(atob(string).split('').map(char => char.charCodeAt(0)))
150
+ // Buffer fast path (Node); index loop otherwise. The old `atob(string).split('')`
151
+ // allocated one single-char string per byte — prohibitive for multi-MB payloads.
152
+ if (typeof Buffer !== 'undefined') {
153
+ return bufferToHexString(new Uint8Array(Buffer.from(string, 'base64')))
154
+ }
155
+ const binary = atob(string)
156
+ const bytes = new Uint8Array(binary.length)
157
+ for (let i = 0; i < binary.length; i++) {
158
+ bytes[i] = binary.charCodeAt(i)
159
+ }
134
160
  return bufferToHexString(bytes)
135
161
  }
136
162