@mblaney/holster 1.0.25 → 1.0.27
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/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/get.js +3 -2
- package/src/ham.js +7 -1
- package/src/radisk.js +22 -7
- package/src/sea.js +53 -0
- package/src/store.js +2 -1
- package/src/wire.js +1 -2
package/package.json
CHANGED
package/src/get.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {match} from "./utils.js"
|
|
1
|
+
import {match, userPublicKey} from "./utils.js"
|
|
2
2
|
|
|
3
3
|
const Get = (lex, graph) => {
|
|
4
4
|
if (!lex || typeof lex !== "object") {
|
|
@@ -34,7 +34,8 @@ const Get = (lex, graph) => {
|
|
|
34
34
|
}
|
|
35
35
|
} else {
|
|
36
36
|
for (const key of Object.keys(graph[soul])) {
|
|
37
|
-
|
|
37
|
+
// Always include userPublicKey for verification, regardless of filter
|
|
38
|
+
if (key === userPublicKey || match(lex["."], key)) {
|
|
38
39
|
node[key] = graph[soul][key]
|
|
39
40
|
node._[">"][key] = graph[soul]._[">"][key]
|
|
40
41
|
const state = graph[soul]._[">"][key]
|
package/src/ham.js
CHANGED
|
@@ -95,6 +95,8 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
95
95
|
|
|
96
96
|
const signedTimestamps = new Set()
|
|
97
97
|
const signedProperties = new Set()
|
|
98
|
+
|
|
99
|
+
// Verify timestamp signatures (numeric keys in node._["s"])
|
|
98
100
|
for (const [key, sig] of Object.entries(node._["s"] || {})) {
|
|
99
101
|
const asNumber = Number(key)
|
|
100
102
|
if (!isNaN(asNumber) && asNumber > 0) {
|
|
@@ -103,7 +105,7 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
103
105
|
signedTimestamps.add(asNumber)
|
|
104
106
|
// Add all properties with this timestamp to signed set
|
|
105
107
|
for (const [prop, ts] of Object.entries(node._[">"] || {})) {
|
|
106
|
-
if (ts === asNumber) {
|
|
108
|
+
if (Number(ts) === asNumber) {
|
|
107
109
|
signedProperties.add(prop)
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -111,6 +113,10 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
111
113
|
}
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
// Verify per-property signatures (backward compatibility)
|
|
117
|
+
const perPropertySignedProps = await SEA.verifyProperties(node, pub)
|
|
118
|
+
perPropertySignedProps.forEach(prop => signedProperties.add(prop))
|
|
119
|
+
|
|
114
120
|
// If no properties verified, skip this update
|
|
115
121
|
if (signedProperties.size === 0) {
|
|
116
122
|
continue
|
package/src/radisk.js
CHANGED
|
@@ -34,6 +34,7 @@ const Radisk = opt => {
|
|
|
34
34
|
if (!opt.write) opt.write = 1 // Wait time before write in milliseconds
|
|
35
35
|
if (!opt.size) opt.size = 1024 * 1024 // File size on disk, default 1MB
|
|
36
36
|
if (!opt.memoryLimit) opt.memoryLimit = 500 // Memory limit in MB
|
|
37
|
+
if (!opt.readTimeout) opt.readTimeout = 1000 // Read timeout in milliseconds
|
|
37
38
|
if (typeof opt.cache === "undefined") opt.cache = true
|
|
38
39
|
|
|
39
40
|
if (!opt.store) {
|
|
@@ -126,20 +127,34 @@ const Radisk = opt => {
|
|
|
126
127
|
// Check if there's already a pending read for this key
|
|
127
128
|
if (pendingReads.has(key)) {
|
|
128
129
|
// Add callback to existing pending read
|
|
129
|
-
pendingReads.get(key).push(cb)
|
|
130
|
+
pendingReads.get(key).callbacks.push(cb)
|
|
130
131
|
return
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
// Start new read and create callback queue
|
|
134
|
-
|
|
134
|
+
// Start new read and create callback queue with fired flag
|
|
135
|
+
const pending = {callbacks: [cb], fired: false, timeoutId: null}
|
|
136
|
+
pendingReads.set(key, pending)
|
|
137
|
+
|
|
138
|
+
// Set a timeout to fire callbacks if read takes too long
|
|
139
|
+
pending.timeoutId = setTimeout(() => {
|
|
140
|
+
if (!pending.fired) {
|
|
141
|
+
pending.fired = true
|
|
142
|
+
pendingReads.delete(key)
|
|
143
|
+
const err = new Error("radisk read timeout")
|
|
144
|
+
pending.callbacks.forEach(callback => callback(err, undefined))
|
|
145
|
+
}
|
|
146
|
+
}, opt.readTimeout)
|
|
135
147
|
|
|
136
148
|
const readStart = Date.now()
|
|
137
149
|
return radisk.read(key, (err, result) => {
|
|
150
|
+
clearTimeout(pending.timeoutId)
|
|
138
151
|
perfLog("read", readStart, key)
|
|
139
|
-
// Execute all pending callbacks for this key
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
// Execute all pending callbacks for this key if timeout hasn't fired
|
|
153
|
+
if (!pending.fired) {
|
|
154
|
+
pending.fired = true
|
|
155
|
+
pendingReads.delete(key)
|
|
156
|
+
pending.callbacks.forEach(callback => callback(err, result))
|
|
157
|
+
}
|
|
143
158
|
})
|
|
144
159
|
}
|
|
145
160
|
|
package/src/sea.js
CHANGED
|
@@ -238,6 +238,59 @@ const SEA = {
|
|
|
238
238
|
return false
|
|
239
239
|
}
|
|
240
240
|
},
|
|
241
|
+
// Verify individual property signatures stored in node._["s"]
|
|
242
|
+
// Returns array of valid property names
|
|
243
|
+
verifyProperties: async (node, pub, cb) => {
|
|
244
|
+
if (!pub || !node) {
|
|
245
|
+
if (cb) cb([])
|
|
246
|
+
return []
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const key = await utils.subtle.importKey(
|
|
250
|
+
"jwk",
|
|
251
|
+
utils.jwk(pub),
|
|
252
|
+
{name: "ECDSA", namedCurve: "P-256"},
|
|
253
|
+
false,
|
|
254
|
+
["verify"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const alg = {name: "ECDSA", hash: {name: "SHA-256"}}
|
|
258
|
+
const valid = []
|
|
259
|
+
const propertySignatures = (node._ && node._["s"]) || {}
|
|
260
|
+
|
|
261
|
+
// Verify each property individually
|
|
262
|
+
for (const k of Object.keys(node).sort()) {
|
|
263
|
+
if (k === "_" || k == userPublicKey) continue
|
|
264
|
+
|
|
265
|
+
const propSig = propertySignatures[k]
|
|
266
|
+
if (!propSig) {
|
|
267
|
+
console.log(`warning: property '${k}' missing signature`)
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const hash = await utils.sha256(node[k])
|
|
273
|
+
const sig = new Uint8Array(SafeBuffer.from(propSig, "base64"))
|
|
274
|
+
|
|
275
|
+
const isValid = await utils.subtle.verify(
|
|
276
|
+
alg,
|
|
277
|
+
key,
|
|
278
|
+
sig,
|
|
279
|
+
new Uint8Array(hash),
|
|
280
|
+
)
|
|
281
|
+
if (isValid) {
|
|
282
|
+
valid.push(k)
|
|
283
|
+
} else {
|
|
284
|
+
console.log(`warning: property '${k}' signature verification failed`)
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
console.log(`warning: property '${k}' error verifying: ${err.message}`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (cb) cb(valid)
|
|
292
|
+
return valid
|
|
293
|
+
},
|
|
241
294
|
work: async (data, salt, cb) => {
|
|
242
295
|
if (typeof salt === "function") {
|
|
243
296
|
cb = salt
|
package/src/store.js
CHANGED
|
@@ -226,7 +226,8 @@ const Store = opt => {
|
|
|
226
226
|
var node
|
|
227
227
|
var signatures = {}
|
|
228
228
|
const each = (value, key) => {
|
|
229
|
-
|
|
229
|
+
// Always include userPublicKey for verification, regardless of filter
|
|
230
|
+
if (key !== utils.userPublicKey && !utils.match(lex["."], key)) return
|
|
230
231
|
|
|
231
232
|
if (!node) node = {_: {"#": soul, ">": {}}}
|
|
232
233
|
node[key] = value[0]
|
package/src/wire.js
CHANGED
|
@@ -302,10 +302,9 @@ const Wire = opt => {
|
|
|
302
302
|
|
|
303
303
|
const ack = Get(lex, graph)
|
|
304
304
|
const track = utils.text.random(9)
|
|
305
|
-
// Request the whole node in secure mode for verification.
|
|
306
305
|
const request = JSON.stringify({
|
|
307
306
|
"#": dup.track(track),
|
|
308
|
-
get:
|
|
307
|
+
get: lex,
|
|
309
308
|
})
|
|
310
309
|
|
|
311
310
|
if (ack) {
|