@wishknish/knishio-client-js 0.9.4 → 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 +38 -40
- package/dist/client.cjs.js.map +1 -1
- package/dist/client.es.mjs +6660 -9256
- package/dist/client.es.mjs.map +1 -1
- package/dist/client.iife.js +38 -40
- package/dist/client.iife.js.map +1 -1
- package/package.json +10 -11
- package/src/AuthToken.js +25 -3
- package/src/KnishIOClient.js +54 -15
- package/src/Molecule.js +15 -7
- package/src/Wallet.js +137 -30
- package/src/exception/SecretStorageException.js +10 -0
- package/src/index.js +11 -1
- package/src/libraries/urql/UrqlClientWrapper.js +7 -2
- 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
|
@@ -63,6 +63,11 @@ class UrqlClientWrapper {
|
|
|
63
63
|
return createClient({
|
|
64
64
|
url: serverUri,
|
|
65
65
|
exchanges,
|
|
66
|
+
// urql 5 had no default and always POSTed; urql 6 defaults to 'within-url-limit', which
|
|
67
|
+
// URL-encodes short queries and sends NO body. That would silently disable the CipherHash
|
|
68
|
+
// envelope below — cipherFetch's `typeof init.body === 'string'` guard fails on a GET, so
|
|
69
|
+
// the query would leave as plaintext URL parameters with no error. Pin POST explicitly.
|
|
70
|
+
preferGetMethod: false,
|
|
66
71
|
// PQ-transport Phase E: when encryption is on, route fetch through the CipherHash
|
|
67
72
|
// wrapper (encrypt the request body to the validator's ML-KEM pubkey, decrypt the
|
|
68
73
|
// response). Undefined → urql uses the global fetch (plaintext).
|
|
@@ -114,7 +119,7 @@ class UrqlClientWrapper {
|
|
|
114
119
|
let requestInit = init
|
|
115
120
|
|
|
116
121
|
if (wallet && serverPubkey && init && typeof init.body === 'string' && this.shouldEncrypt(init.body)) {
|
|
117
|
-
const hashVar = await wallet.
|
|
122
|
+
const hashVar = await wallet.encryptStringML(init.body, serverPubkey)
|
|
118
123
|
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) }
|
|
119
124
|
encryptedRequest = true
|
|
120
125
|
}
|
|
@@ -138,7 +143,7 @@ class UrqlClientWrapper {
|
|
|
138
143
|
// Plaintext (e.g. a validator-side error response) — pass through unchanged.
|
|
139
144
|
return new Response(text, init2)
|
|
140
145
|
}
|
|
141
|
-
const decrypted = await wallet.
|
|
146
|
+
const decrypted = await wallet.decryptMyMessageML(JSON.parse(hash))
|
|
142
147
|
return new Response(decrypted != null ? decrypted : text, init2)
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -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
|
}
|