@wishknish/knishio-client-js 1.0.0 → 1.1.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.
- package/README.md +13 -0
- package/dist/client.cjs.js +25 -25
- package/dist/client.cjs.js.map +1 -1
- package/dist/client.es.mjs +1544 -839
- package/dist/client.es.mjs.map +1 -1
- package/dist/client.iife.js +25 -25
- package/dist/client.iife.js.map +1 -1
- package/package.json +1 -1
- package/src/exception/SecretStorageException.js +10 -0
- package/src/index.js +11 -1
- package/src/storage/FileStorageBackend.js +199 -0
- package/src/storage/MemorySecretStorageProvider.js +69 -1
- package/src/storage/NonExtractableKeySecretStorageProvider.js +514 -0
- package/src/storage/WebAuthnPrfSecretStorageProvider.js +646 -0
- package/src/storage/WebCryptoSecretStorageProvider.js +107 -129
- package/src/storage/WebStorageBackend.js +117 -0
- package/src/storage/index.js +25 -3
- package/src/storage/secretEnvelope.js +208 -0
package/package.json
CHANGED
|
@@ -92,4 +92,14 @@ export default class SecretStorageException extends BaseException {
|
|
|
92
92
|
static unavailable (provider, reason = 'Hardware or API not accessible') {
|
|
93
93
|
return new SecretStorageException(`Secret storage provider '${provider}' is unavailable: ${reason}`)
|
|
94
94
|
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Factory method: validation error
|
|
98
|
+
*
|
|
99
|
+
* @param {string} message
|
|
100
|
+
* @returns {SecretStorageException}
|
|
101
|
+
*/
|
|
102
|
+
static validationError (message) {
|
|
103
|
+
return new SecretStorageException(message)
|
|
104
|
+
}
|
|
95
105
|
}
|
package/src/index.js
CHANGED
|
@@ -330,8 +330,18 @@ export {
|
|
|
330
330
|
export {
|
|
331
331
|
MemorySecretStorageProvider,
|
|
332
332
|
WebCryptoSecretStorageProvider,
|
|
333
|
+
WebAuthnPrfSecretStorageProvider,
|
|
334
|
+
NonExtractableKeySecretStorageProvider,
|
|
335
|
+
IndexedDbKeyStore,
|
|
336
|
+
MemoryKeyStore,
|
|
333
337
|
MemoryStorageBackend,
|
|
334
|
-
|
|
338
|
+
FileStorageBackend,
|
|
339
|
+
WebStorageBackend,
|
|
340
|
+
createDefaultSecretStorage,
|
|
341
|
+
sealEnvelope,
|
|
342
|
+
openEnvelope,
|
|
343
|
+
SECRET_KEY_PREFIX,
|
|
344
|
+
RECOVERY_KEY_PREFIX
|
|
335
345
|
} from './storage/index.js'
|
|
336
346
|
|
|
337
347
|
export {
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Node-only persistent file storage backend.
|
|
53
|
+
* Stores key-value entries in a JSON file with restrictive permissions (0o600).
|
|
54
|
+
* Uses temporary file writing followed by atomic rename to prevent corruption.
|
|
55
|
+
*/
|
|
56
|
+
export default class FileStorageBackend {
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} filePath
|
|
59
|
+
*/
|
|
60
|
+
constructor (filePath) {
|
|
61
|
+
if (!filePath) {
|
|
62
|
+
throw new SecretStorageException('Storage file path cannot be empty')
|
|
63
|
+
}
|
|
64
|
+
this.filePath = filePath
|
|
65
|
+
this.store = new Map()
|
|
66
|
+
this.loaded = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
async getFs () {
|
|
73
|
+
try {
|
|
74
|
+
// Platform-specific: node:fs and node:path do not exist in browser runtimes
|
|
75
|
+
// and cannot be statically imported without breaking browser bundles.
|
|
76
|
+
const fs = await import('node:fs/promises')
|
|
77
|
+
const path = await import('node:path')
|
|
78
|
+
return { fs, path }
|
|
79
|
+
} catch {
|
|
80
|
+
throw SecretStorageException.unavailable(
|
|
81
|
+
'file-storage',
|
|
82
|
+
'FileStorageBackend is only supported in Node.js environments with node:fs access'
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
async ensureLoaded () {
|
|
91
|
+
if (this.loaded) {
|
|
92
|
+
return this.store
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { fs } = await this.getFs()
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const content = await fs.readFile(this.filePath, 'utf8')
|
|
99
|
+
let parsed
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(content)
|
|
102
|
+
} catch {
|
|
103
|
+
throw SecretStorageException.decryptionFailed('Corrupted storage file format')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (parsed && typeof parsed === 'object') {
|
|
107
|
+
this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]))
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof SecretStorageException) {
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
if (err?.code !== 'ENOENT') {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
115
|
+
throw new SecretStorageException(`Failed to read storage file: ${msg}`)
|
|
116
|
+
}
|
|
117
|
+
this.store = new Map()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.loaded = true
|
|
121
|
+
return this.store
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
async persist () {
|
|
128
|
+
const { fs, path } = await this.getFs()
|
|
129
|
+
|
|
130
|
+
const dir = path.dirname(this.filePath)
|
|
131
|
+
if (dir && dir !== '.') {
|
|
132
|
+
await fs.mkdir(dir, { recursive: true })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`
|
|
136
|
+
const data = JSON.stringify(Object.fromEntries(this.store), null, 2)
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await fs.writeFile(tmpPath, data, { mode: 0o600, encoding: 'utf8' })
|
|
140
|
+
if (typeof process !== 'undefined' && process.platform !== 'win32') {
|
|
141
|
+
try {
|
|
142
|
+
await fs.chmod(tmpPath, 0o600)
|
|
143
|
+
} catch {
|
|
144
|
+
// Ignore chmod error if file system does not support it
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await fs.rename(tmpPath, this.filePath)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
try {
|
|
150
|
+
await fs.unlink(tmpPath)
|
|
151
|
+
} catch {
|
|
152
|
+
// Ignore cleanup error
|
|
153
|
+
}
|
|
154
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
155
|
+
throw new SecretStorageException(`Failed to persist storage file: ${msg}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} key
|
|
161
|
+
* @returns {Promise<string|null>}
|
|
162
|
+
*/
|
|
163
|
+
async getItem (key) {
|
|
164
|
+
await this.ensureLoaded()
|
|
165
|
+
return this.store.get(key) ?? null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string} key
|
|
170
|
+
* @param {string} value
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async setItem (key, value) {
|
|
174
|
+
await this.ensureLoaded()
|
|
175
|
+
this.store.set(key, value)
|
|
176
|
+
await this.persist()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {string} key
|
|
181
|
+
* @returns {Promise<boolean>}
|
|
182
|
+
*/
|
|
183
|
+
async removeItem (key) {
|
|
184
|
+
await this.ensureLoaded()
|
|
185
|
+
const existed = this.store.delete(key)
|
|
186
|
+
if (existed) {
|
|
187
|
+
await this.persist()
|
|
188
|
+
}
|
|
189
|
+
return existed
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @returns {Promise<string[]>}
|
|
194
|
+
*/
|
|
195
|
+
async keys () {
|
|
196
|
+
await this.ensureLoaded()
|
|
197
|
+
return Array.from(this.store.keys())
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -47,7 +47,8 @@ License: https://github.com/WishKnish/KnishIO-Client-JS/blob/master/LICENSE
|
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
49
|
import SecretStorageException from '../exception/SecretStorageException.js'
|
|
50
|
-
import { withSecureString } from '../libraries/secureMemory.js'
|
|
50
|
+
import { withSecureString, zeroizeBytes } from '../libraries/secureMemory.js'
|
|
51
|
+
import { sealEnvelope, openEnvelope } from './secretEnvelope.js'
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* In-memory secret storage provider
|
|
@@ -57,6 +58,7 @@ export default class MemorySecretStorageProvider {
|
|
|
57
58
|
constructor () {
|
|
58
59
|
this.providerType = 'memory'
|
|
59
60
|
this.secrets = new Map()
|
|
61
|
+
this.recoverySecrets = new Map()
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/**
|
|
@@ -102,6 +104,18 @@ export default class MemorySecretStorageProvider {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
this.secrets.set(bundleHash, { secret, metadata })
|
|
107
|
+
|
|
108
|
+
if (options.recoveryPassphrase) {
|
|
109
|
+
const recoveryMetadata = {
|
|
110
|
+
bundleHash,
|
|
111
|
+
label: options.label,
|
|
112
|
+
createdAt: Date.now(),
|
|
113
|
+
hardwareBacked: false,
|
|
114
|
+
providerType: 'webcrypto-aes-gcm'
|
|
115
|
+
}
|
|
116
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata)
|
|
117
|
+
this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload))
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
|
|
107
121
|
/**
|
|
@@ -122,6 +136,7 @@ export default class MemorySecretStorageProvider {
|
|
|
122
136
|
* @returns {Promise<boolean>}
|
|
123
137
|
*/
|
|
124
138
|
async deleteSecret (bundleHash) {
|
|
139
|
+
this.recoverySecrets.delete(bundleHash)
|
|
125
140
|
return this.secrets.delete(bundleHash)
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -166,5 +181,58 @@ export default class MemorySecretStorageProvider {
|
|
|
166
181
|
*/
|
|
167
182
|
clear () {
|
|
168
183
|
this.secrets.clear()
|
|
184
|
+
this.recoverySecrets.clear()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Recover a secret using its recovery envelope and restore it
|
|
189
|
+
*
|
|
190
|
+
* @param {string} bundleHash
|
|
191
|
+
* @param {string} recoveryPassphrase
|
|
192
|
+
* @param {{ label?: string }} [options]
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
async recoverSecret (bundleHash, recoveryPassphrase, options = {}) {
|
|
196
|
+
if (!bundleHash) {
|
|
197
|
+
throw new SecretStorageException('Bundle hash cannot be empty')
|
|
198
|
+
}
|
|
199
|
+
if (!recoveryPassphrase) {
|
|
200
|
+
throw new SecretStorageException('Recovery passphrase cannot be empty')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const raw = this.recoverySecrets.get(bundleHash)
|
|
204
|
+
if (!raw) {
|
|
205
|
+
throw SecretStorageException.notFound(bundleHash)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let payload
|
|
209
|
+
try {
|
|
210
|
+
payload = JSON.parse(raw)
|
|
211
|
+
} catch {
|
|
212
|
+
throw SecretStorageException.decryptionFailed('Corrupted recovery payload format')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let decryptedBytes
|
|
216
|
+
try {
|
|
217
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase)
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err instanceof SecretStorageException) {
|
|
220
|
+
throw err
|
|
221
|
+
}
|
|
222
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
223
|
+
throw SecretStorageException.decryptionFailed(msg)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let secretStr
|
|
227
|
+
try {
|
|
228
|
+
secretStr = new TextDecoder().decode(decryptedBytes)
|
|
229
|
+
} finally {
|
|
230
|
+
zeroizeBytes(decryptedBytes)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
234
|
+
...options,
|
|
235
|
+
recoveryPassphrase
|
|
236
|
+
})
|
|
169
237
|
}
|
|
170
238
|
}
|