@wishknish/knishio-client-ts 0.9.6 → 0.9.8

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