@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.
@@ -0,0 +1,170 @@
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 SecretStorageException from '../exception/SecretStorageException.js'
50
+ import { withSecureString } from '../libraries/secureMemory.js'
51
+
52
+ /**
53
+ * In-memory secret storage provider
54
+ * Used for testing, headless runners, and backward-compatible fallback
55
+ */
56
+ export default class MemorySecretStorageProvider {
57
+ constructor () {
58
+ this.providerType = 'memory'
59
+ this.secrets = new Map()
60
+ }
61
+
62
+ /**
63
+ * Memory storage is not hardware backed
64
+ *
65
+ * @returns {boolean}
66
+ */
67
+ isHardwareBacked () {
68
+ return false
69
+ }
70
+
71
+ /**
72
+ * Memory storage is always available
73
+ *
74
+ * @returns {Promise<boolean>}
75
+ */
76
+ async isAvailable () {
77
+ return true
78
+ }
79
+
80
+ /**
81
+ * Store a secret in memory
82
+ *
83
+ * @param {string} bundleHash
84
+ * @param {string} secret
85
+ * @param {{ label?: string }} [options]
86
+ * @returns {Promise<void>}
87
+ */
88
+ async storeSecret (bundleHash, secret, options = {}) {
89
+ if (!bundleHash) {
90
+ throw new SecretStorageException('Bundle hash cannot be empty')
91
+ }
92
+ if (!secret) {
93
+ throw new SecretStorageException('Secret cannot be empty')
94
+ }
95
+
96
+ const metadata = {
97
+ bundleHash,
98
+ label: options.label,
99
+ createdAt: Date.now(),
100
+ hardwareBacked: false,
101
+ providerType: this.providerType
102
+ }
103
+
104
+ this.secrets.set(bundleHash, { secret, metadata })
105
+ }
106
+
107
+ /**
108
+ * Retrieve a secret from memory
109
+ *
110
+ * @param {string} bundleHash
111
+ * @returns {Promise<string|null>}
112
+ */
113
+ async retrieveSecret (bundleHash) {
114
+ const entry = this.secrets.get(bundleHash)
115
+ return entry ? entry.secret : null
116
+ }
117
+
118
+ /**
119
+ * Delete a stored secret
120
+ *
121
+ * @param {string} bundleHash
122
+ * @returns {Promise<boolean>}
123
+ */
124
+ async deleteSecret (bundleHash) {
125
+ return this.secrets.delete(bundleHash)
126
+ }
127
+
128
+ /**
129
+ * Check if a secret exists
130
+ *
131
+ * @param {string} bundleHash
132
+ * @returns {Promise<boolean>}
133
+ */
134
+ async hasSecret (bundleHash) {
135
+ return this.secrets.has(bundleHash)
136
+ }
137
+
138
+ /**
139
+ * List all stored secret metadata
140
+ *
141
+ * @returns {Promise<Array<{ bundleHash: string, label?: string, createdAt: number, hardwareBacked: boolean, providerType: string }>>}
142
+ */
143
+ async listSecrets () {
144
+ return Array.from(this.secrets.values()).map(entry => ({ ...entry.metadata }))
145
+ }
146
+
147
+ /**
148
+ * Execute callback with unwrapped secret and ensure cleanup
149
+ *
150
+ * @template T
151
+ * @param {string} bundleHash
152
+ * @param {(secret: string) => Promise<T>|T} fn
153
+ * @returns {Promise<T>}
154
+ */
155
+ async withSecret (bundleHash, fn) {
156
+ const entry = this.secrets.get(bundleHash)
157
+ if (!entry) {
158
+ throw SecretStorageException.notFound(bundleHash)
159
+ }
160
+
161
+ return withSecureString(entry.secret, fn)
162
+ }
163
+
164
+ /**
165
+ * Clear all secrets from memory
166
+ */
167
+ clear () {
168
+ this.secrets.clear()
169
+ }
170
+ }
@@ -0,0 +1,416 @@
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 SecretStorageException from '../exception/SecretStorageException.js'
50
+ import { zeroizeBytes, withSecureBytes } from '../libraries/secureMemory.js'
51
+
52
+ /**
53
+ * Default in-memory backend for WebCrypto encrypted payloads
54
+ */
55
+ export class MemoryStorageBackend {
56
+ constructor () {
57
+ this.store = new Map()
58
+ }
59
+
60
+ getItem (key) {
61
+ return this.store.get(key) ?? null
62
+ }
63
+
64
+ setItem (key, value) {
65
+ this.store.set(key, value)
66
+ }
67
+
68
+ removeItem (key) {
69
+ return this.store.delete(key)
70
+ }
71
+
72
+ keys () {
73
+ return Array.from(this.store.keys())
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Helper to convert Uint8Array to base64
79
+ *
80
+ * @param {Uint8Array} bytes
81
+ * @returns {string}
82
+ */
83
+ function uint8ArrayToBase64 (bytes) {
84
+ let binary = ''
85
+ const len = bytes.byteLength
86
+ for (let i = 0; i < len; i++) {
87
+ const byte = bytes[i]
88
+ if (byte !== undefined) {
89
+ binary += String.fromCharCode(byte)
90
+ }
91
+ }
92
+ return btoa(binary)
93
+ }
94
+
95
+ /**
96
+ * Helper to convert base64 to Uint8Array
97
+ *
98
+ * @param {string} base64
99
+ * @returns {Uint8Array}
100
+ */
101
+ function base64ToUint8Array (base64) {
102
+ const binary = atob(base64)
103
+ const len = binary.length
104
+ const bytes = new Uint8Array(len)
105
+ for (let i = 0; i < len; i++) {
106
+ bytes[i] = binary.charCodeAt(i)
107
+ }
108
+ return bytes
109
+ }
110
+
111
+ const textEncoder = new TextEncoder()
112
+ const textDecoder = new TextDecoder()
113
+ const KEY_PREFIX = 'knishio:secret:'
114
+ const DEFAULT_ITERATIONS = 100000
115
+
116
+ /**
117
+ * Hardware-compatible envelope encryption secret storage provider
118
+ * Uses WebCrypto AES-GCM (256-bit) with PBKDF2-HMAC-SHA256 key derivation
119
+ */
120
+ export default class WebCryptoSecretStorageProvider {
121
+ /**
122
+ * @param {{ backend?: object, defaultPassphrase?: string, hardwareBacked?: boolean }} [options]
123
+ */
124
+ constructor (options = {}) {
125
+ this.providerType = 'webcrypto-aes-gcm'
126
+ this.backend = options.backend || new MemoryStorageBackend()
127
+ this.defaultPassphrase = options.defaultPassphrase
128
+ this.hardwareBacked = options.hardwareBacked || false
129
+ }
130
+
131
+ /**
132
+ * Whether this provider is backed by hardware
133
+ *
134
+ * @returns {boolean}
135
+ */
136
+ isHardwareBacked () {
137
+ return this.hardwareBacked
138
+ }
139
+
140
+ /**
141
+ * Check if WebCrypto subtle API is available
142
+ *
143
+ * @returns {Promise<boolean>}
144
+ */
145
+ async isAvailable () {
146
+ return (
147
+ typeof globalThis.crypto !== 'undefined' &&
148
+ typeof globalThis.crypto.subtle !== 'undefined'
149
+ )
150
+ }
151
+
152
+ /**
153
+ * Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
154
+ *
155
+ * @param {string} passphrase
156
+ * @param {Uint8Array} salt
157
+ * @param {number} [iterations]
158
+ * @returns {Promise<CryptoKey>}
159
+ */
160
+ async deriveKey (passphrase, salt, iterations = DEFAULT_ITERATIONS) {
161
+ if (!await this.isAvailable()) {
162
+ throw SecretStorageException.unavailable(this.providerType, 'WebCrypto API is not available')
163
+ }
164
+
165
+ const passphraseBytes = textEncoder.encode(passphrase)
166
+ try {
167
+ const baseKey = await globalThis.crypto.subtle.importKey(
168
+ 'raw',
169
+ passphraseBytes,
170
+ 'PBKDF2',
171
+ false,
172
+ ['deriveKey']
173
+ )
174
+
175
+ return await globalThis.crypto.subtle.deriveKey(
176
+ {
177
+ name: 'PBKDF2',
178
+ salt,
179
+ iterations,
180
+ hash: 'SHA-256'
181
+ },
182
+ baseKey,
183
+ { name: 'AES-GCM', length: 256 },
184
+ false,
185
+ ['encrypt', 'decrypt']
186
+ )
187
+ } finally {
188
+ zeroizeBytes(passphraseBytes)
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Store and encrypt a master secret
194
+ *
195
+ * @param {string} bundleHash
196
+ * @param {string} secret
197
+ * @param {{ label?: string, passphrase?: string }} [options]
198
+ * @returns {Promise<void>}
199
+ */
200
+ async storeSecret (bundleHash, secret, options = {}) {
201
+ if (!bundleHash) {
202
+ throw new SecretStorageException('Bundle hash cannot be empty')
203
+ }
204
+ if (!secret) {
205
+ throw new SecretStorageException('Secret cannot be empty')
206
+ }
207
+
208
+ const passphrase = options.passphrase || this.defaultPassphrase
209
+ if (!passphrase) {
210
+ throw new SecretStorageException('Passphrase required for envelope encryption')
211
+ }
212
+
213
+ const salt = new Uint8Array(16)
214
+ const iv = new Uint8Array(12)
215
+ globalThis.crypto.getRandomValues(salt)
216
+ globalThis.crypto.getRandomValues(iv)
217
+
218
+ const key = await this.deriveKey(passphrase, salt, DEFAULT_ITERATIONS)
219
+ const secretBytes = textEncoder.encode(secret)
220
+
221
+ try {
222
+ const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
223
+ {
224
+ name: 'AES-GCM',
225
+ iv
226
+ },
227
+ key,
228
+ secretBytes
229
+ )
230
+
231
+ const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
232
+ const metadata = {
233
+ bundleHash,
234
+ label: options.label,
235
+ createdAt: Date.now(),
236
+ hardwareBacked: this.hardwareBacked,
237
+ providerType: this.providerType
238
+ }
239
+
240
+ const payload = {
241
+ version: 1,
242
+ ciphertext,
243
+ iv: uint8ArrayToBase64(iv),
244
+ salt: uint8ArrayToBase64(salt),
245
+ algorithm: 'AES-GCM',
246
+ iterations: DEFAULT_ITERATIONS,
247
+ metadata
248
+ }
249
+
250
+ await this.backend.setItem(`${KEY_PREFIX}${bundleHash}`, JSON.stringify(payload))
251
+ } catch (err) {
252
+ const msg = err instanceof Error ? err.message : String(err)
253
+ throw new SecretStorageException(`Encryption failed: ${msg}`)
254
+ } finally {
255
+ zeroizeBytes(secretBytes)
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Retrieve and decrypt the master secret
261
+ *
262
+ * @param {string} bundleHash
263
+ * @param {{ passphrase?: string }} [options]
264
+ * @returns {Promise<string|null>}
265
+ */
266
+ async retrieveSecret (bundleHash, options = {}) {
267
+ const raw = await this.backend.getItem(`${KEY_PREFIX}${bundleHash}`)
268
+ if (!raw) {
269
+ return null
270
+ }
271
+
272
+ let payload
273
+ try {
274
+ payload = JSON.parse(raw)
275
+ } catch {
276
+ throw SecretStorageException.decryptionFailed('Corrupted payload format')
277
+ }
278
+
279
+ const passphrase = options.passphrase || this.defaultPassphrase
280
+ if (!passphrase) {
281
+ throw new SecretStorageException('Passphrase required for secret decryption')
282
+ }
283
+
284
+ const salt = base64ToUint8Array(payload.salt)
285
+ const iv = base64ToUint8Array(payload.iv)
286
+ const ciphertext = base64ToUint8Array(payload.ciphertext)
287
+
288
+ try {
289
+ const key = await this.deriveKey(passphrase, salt, payload.iterations || DEFAULT_ITERATIONS)
290
+ const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
291
+ {
292
+ name: 'AES-GCM',
293
+ iv
294
+ },
295
+ key,
296
+ ciphertext
297
+ )
298
+
299
+ const decryptedBytes = new Uint8Array(decryptedBuffer)
300
+ try {
301
+ return textDecoder.decode(decryptedBytes)
302
+ } finally {
303
+ zeroizeBytes(decryptedBytes)
304
+ }
305
+ } catch (err) {
306
+ const msg = err instanceof Error ? err.message : String(err)
307
+ throw SecretStorageException.decryptionFailed(msg)
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Delete a stored secret
313
+ *
314
+ * @param {string} bundleHash
315
+ * @returns {Promise<boolean>}
316
+ */
317
+ async deleteSecret (bundleHash) {
318
+ const key = `${KEY_PREFIX}${bundleHash}`
319
+ const result = await this.backend.removeItem(key)
320
+ return result !== false
321
+ }
322
+
323
+ /**
324
+ * Check if a secret exists
325
+ *
326
+ * @param {string} bundleHash
327
+ * @returns {Promise<boolean>}
328
+ */
329
+ async hasSecret (bundleHash) {
330
+ const raw = await this.backend.getItem(`${KEY_PREFIX}${bundleHash}`)
331
+ return raw !== null
332
+ }
333
+
334
+ /**
335
+ * List all stored secret metadata
336
+ *
337
+ * @returns {Promise<Array<{ bundleHash: string, label?: string, createdAt: number, hardwareBacked: boolean, providerType: string }>>}
338
+ */
339
+ async listSecrets () {
340
+ const keys = await this.backend.keys()
341
+ const matchingKeys = keys.filter(k => k.startsWith(KEY_PREFIX))
342
+ const results = []
343
+
344
+ for (const key of matchingKeys) {
345
+ const raw = await this.backend.getItem(key)
346
+ if (raw) {
347
+ try {
348
+ const payload = JSON.parse(raw)
349
+ if (payload.metadata) {
350
+ results.push(payload.metadata)
351
+ }
352
+ } catch {
353
+ // Ignore corrupted entries
354
+ }
355
+ }
356
+ }
357
+
358
+ return results
359
+ }
360
+
361
+ /**
362
+ * Execute callback with unwrapped secret, zeroizing the decrypted buffer upon completion
363
+ *
364
+ * @template T
365
+ * @param {string} bundleHash
366
+ * @param {(secret: string) => Promise<T>|T} fn
367
+ * @param {{ passphrase?: string }} [options]
368
+ * @returns {Promise<T>}
369
+ */
370
+ async withSecret (bundleHash, fn, options = {}) {
371
+ const raw = await this.backend.getItem(`${KEY_PREFIX}${bundleHash}`)
372
+ if (!raw) {
373
+ throw SecretStorageException.notFound(bundleHash)
374
+ }
375
+
376
+ let payload
377
+ try {
378
+ payload = JSON.parse(raw)
379
+ } catch {
380
+ throw SecretStorageException.decryptionFailed('Corrupted payload format')
381
+ }
382
+
383
+ const passphrase = options.passphrase || this.defaultPassphrase
384
+ if (!passphrase) {
385
+ throw new SecretStorageException('Passphrase required for secret decryption')
386
+ }
387
+
388
+ const salt = base64ToUint8Array(payload.salt)
389
+ const iv = base64ToUint8Array(payload.iv)
390
+ const ciphertext = base64ToUint8Array(payload.ciphertext)
391
+
392
+ try {
393
+ const key = await this.deriveKey(passphrase, salt, payload.iterations || DEFAULT_ITERATIONS)
394
+ const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
395
+ {
396
+ name: 'AES-GCM',
397
+ iv
398
+ },
399
+ key,
400
+ ciphertext
401
+ )
402
+
403
+ const decryptedBytes = new Uint8Array(decryptedBuffer)
404
+ return await withSecureBytes(decryptedBytes, async (bytes) => {
405
+ const secretString = textDecoder.decode(bytes)
406
+ return await fn(secretString)
407
+ })
408
+ } catch (err) {
409
+ if (err instanceof SecretStorageException) {
410
+ throw err
411
+ }
412
+ const msg = err instanceof Error ? err.message : String(err)
413
+ throw SecretStorageException.decryptionFailed(msg)
414
+ }
415
+ }
416
+ }
@@ -0,0 +1,79 @@
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
+ export { default as MemorySecretStorageProvider } from './MemorySecretStorageProvider.js'
50
+ export {
51
+ default as WebCryptoSecretStorageProvider,
52
+ MemoryStorageBackend
53
+ } from './WebCryptoSecretStorageProvider.js'
54
+
55
+ import MemorySecretStorageProvider from './MemorySecretStorageProvider.js'
56
+ import WebCryptoSecretStorageProvider from './WebCryptoSecretStorageProvider.js'
57
+
58
+ /**
59
+ * Factory function to create a secret storage provider
60
+ *
61
+ * @param {{ type?: 'webcrypto'|'memory', defaultPassphrase?: string, backend?: object, hardwareBacked?: boolean }} [options]
62
+ * @returns {object}
63
+ */
64
+ export function createDefaultSecretStorage (options = {}) {
65
+ if (options.type === 'memory') {
66
+ return new MemorySecretStorageProvider()
67
+ }
68
+
69
+ // Default to WebCrypto if available
70
+ if (typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.subtle !== 'undefined') {
71
+ return new WebCryptoSecretStorageProvider({
72
+ backend: options.backend,
73
+ defaultPassphrase: options.defaultPassphrase,
74
+ hardwareBacked: options.hardwareBacked
75
+ })
76
+ }
77
+
78
+ return new MemorySecretStorageProvider()
79
+ }