@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/dist/client.cjs.js +29 -29
- package/dist/client.cjs.js.map +1 -1
- package/dist/client.es.mjs +1411 -900
- package/dist/client.es.mjs.map +1 -1
- package/dist/client.iife.js +29 -29
- package/dist/client.iife.js.map +1 -1
- package/package.json +2 -2
- package/src/KnishIOClient.js +70 -6
- package/src/Molecule.js +1 -1
- package/src/Wallet.js +14 -1
- package/src/exception/SecretStorageException.js +95 -0
- package/src/exception/index.js +2 -0
- package/src/index.js +15 -0
- package/src/libraries/array.js +6 -4
- package/src/libraries/secureMemory.js +125 -0
- package/src/libraries/strings.js +33 -7
- package/src/storage/MemorySecretStorageProvider.js +170 -0
- package/src/storage/WebCryptoSecretStorageProvider.js +416 -0
- package/src/storage/index.js +79 -0
- package/src/types/index.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wishknish/knishio-client-js",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
99
|
+
"packageManager": "yarn@4.15.0"
|
|
100
100
|
}
|
package/src/KnishIOClient.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
+
}
|
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'
|
package/src/libraries/array.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* @return {array}
|
|
7
7
|
*/
|
|
8
8
|
export function chunkArray (arr, size) {
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
}
|
package/src/libraries/strings.js
CHANGED
|
@@ -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
|
-
|
|
48
|
+
const array = new Uint8Array(length)
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
crypto.getRandomValues(array)
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|