@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
|
@@ -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
|
+
}
|
package/src/types/index.js
DELETED