@mblaney/holster 1.0.21 → 1.0.23
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/.dockerignore +9 -0
- package/Dockerfile +20 -0
- package/README.md +15 -3
- package/build/holster.js +3 -3
- package/build/holster.js.map +3 -3
- package/package.json +1 -1
- package/src/get.js +10 -0
- package/src/ham.js +34 -10
- package/src/holster.js +6 -15
- package/src/radisk.js +42 -18
- package/src/sea.js +86 -2
- package/src/store.js +13 -1
- package/src/user.js +55 -13
- package/src/utils.js +6 -6
- package/src/wire.js +3 -3
- package/test/holster.on.test.js +36 -5
- package/test/holster.user.on.test.js +40 -17
- package/test/holster.user.put.test.js +2 -5
package/package.json
CHANGED
package/src/get.js
CHANGED
|
@@ -16,6 +16,7 @@ const Get = (lex, graph) => {
|
|
|
16
16
|
if (!graph[soul]) return
|
|
17
17
|
|
|
18
18
|
const node = {_: {"#": soul, ">": {}}}
|
|
19
|
+
let signatures = {}
|
|
19
20
|
|
|
20
21
|
if (typeof lex["."] === "string") {
|
|
21
22
|
const key = lex["."]
|
|
@@ -23,14 +24,23 @@ const Get = (lex, graph) => {
|
|
|
23
24
|
|
|
24
25
|
node[key] = graph[soul][key]
|
|
25
26
|
node._[">"][key] = graph[soul]._[">"][key]
|
|
27
|
+
if (graph[soul]._["s"] && graph[soul]._["s"][key]) {
|
|
28
|
+
signatures[key] = graph[soul]._["s"][key]
|
|
29
|
+
}
|
|
26
30
|
} else {
|
|
27
31
|
for (const key of Object.keys(graph[soul])) {
|
|
28
32
|
if (match(lex["."], key)) {
|
|
29
33
|
node[key] = graph[soul][key]
|
|
30
34
|
node._[">"][key] = graph[soul]._[">"][key]
|
|
35
|
+
if (graph[soul]._["s"] && graph[soul]._["s"][key]) {
|
|
36
|
+
signatures[key] = graph[soul]._["s"][key]
|
|
37
|
+
}
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
}
|
|
41
|
+
if (Object.keys(signatures).length > 0) {
|
|
42
|
+
node._["s"] = signatures
|
|
43
|
+
}
|
|
34
44
|
return {[soul]: node}
|
|
35
45
|
}
|
|
36
46
|
|
package/src/ham.js
CHANGED
|
@@ -43,6 +43,7 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
43
43
|
const machine = Date.now()
|
|
44
44
|
const now = {}
|
|
45
45
|
const defer = {}
|
|
46
|
+
const validProperties = new Map() // Track valid properties per soul
|
|
46
47
|
let wait = 0
|
|
47
48
|
|
|
48
49
|
for (const soul of Object.keys(change)) {
|
|
@@ -54,10 +55,9 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
54
55
|
|
|
55
56
|
if (!node || !node._) continue
|
|
56
57
|
|
|
57
|
-
const sig = node[utils.userSignature]
|
|
58
58
|
const pub = node[utils.userPublicKey]
|
|
59
|
-
// If
|
|
60
|
-
if (
|
|
59
|
+
// If per-property signatures and public key are provided then always verify.
|
|
60
|
+
if (node._["s"] && pub) verify = true
|
|
61
61
|
|
|
62
62
|
// Special case if soul starts with "~". Node must be system data ie,
|
|
63
63
|
// ~@alias or ~publickey. For aliases, key and value must be a self
|
|
@@ -77,17 +77,35 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
if (verify) {
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
// which means verify won't work either.
|
|
83
|
-
if (!sig || !pub || !(await SEA.verify({m: node, s: sig}, {pub: pub}))) {
|
|
80
|
+
// Per-property signatures stored in node._["s"]
|
|
81
|
+
if (!pub || !node._) {
|
|
84
82
|
continue
|
|
85
83
|
}
|
|
84
|
+
|
|
85
|
+
// Verify properties individually, returns array of valid property names
|
|
86
|
+
const validProps = await SEA.verifyProperties(node, pub)
|
|
87
|
+
|
|
88
|
+
// If no properties verified, skip this update
|
|
89
|
+
if (validProps.length === 0) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Track valid properties for this soul
|
|
94
|
+
validProperties.set(soul, new Set(validProps))
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
for (const key of Object.keys(node)) {
|
|
89
98
|
if (key === "_") continue
|
|
90
99
|
|
|
100
|
+
// Skip metadata fields (including userSignature for old data)
|
|
101
|
+
if (key === utils.userSignature || key === utils.userPublicKey) continue
|
|
102
|
+
|
|
103
|
+
// If this soul had verification, only process properties that have valid signatures
|
|
104
|
+
// Properties without signatures in this update are left unchanged
|
|
105
|
+
if (validProperties.has(soul) && !validProperties.get(soul).has(key)) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
91
109
|
const value = node[key]
|
|
92
110
|
const state = node._ && node._[">"] ? node._[">"][key] : 0
|
|
93
111
|
const currentValue = (graph[soul] || {})[key]
|
|
@@ -107,21 +125,27 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
107
125
|
// Wait the shortest difference before trying the updates again.
|
|
108
126
|
if (wait === 0 || skew < wait) wait = nodeWait = skew
|
|
109
127
|
if (!defer[soul]) {
|
|
110
|
-
defer[soul] = {_: {"#": soul, ">": {}}}
|
|
128
|
+
defer[soul] = {_: {"#": soul, ">": {}, s: {}}}
|
|
111
129
|
}
|
|
112
130
|
defer[soul][key] = value
|
|
113
131
|
defer[soul]._[">"][key] = state
|
|
132
|
+
if (node._["s"] && node._["s"][key]) {
|
|
133
|
+
defer[soul]._["s"][key] = node._["s"][key]
|
|
134
|
+
}
|
|
114
135
|
} else {
|
|
115
136
|
const result = Ham(state, currentState, value, currentValue)
|
|
116
137
|
if (result.incoming) {
|
|
117
138
|
if (!now[soul]) {
|
|
118
|
-
now[soul] = {_: {"#": soul, ">": {}}}
|
|
139
|
+
now[soul] = {_: {"#": soul, ">": {}, s: {}}}
|
|
119
140
|
}
|
|
120
141
|
if (!graph[soul]) {
|
|
121
|
-
graph[soul] = {_: {"#": soul, ">": {}}}
|
|
142
|
+
graph[soul] = {_: {"#": soul, ">": {}, s: {}}}
|
|
122
143
|
}
|
|
123
144
|
graph[soul][key] = now[soul][key] = value
|
|
124
145
|
graph[soul]._[">"][key] = now[soul]._[">"][key] = state
|
|
146
|
+
if (node._["s"] && node._["s"][key]) {
|
|
147
|
+
graph[soul]._["s"][key] = now[soul]._["s"][key] = node._["s"][key]
|
|
148
|
+
}
|
|
125
149
|
// Call event listeners for update on key, mix is called before
|
|
126
150
|
// put has finished so wait for what could be multiple nested
|
|
127
151
|
// updates on a node.
|
package/src/holster.js
CHANGED
|
@@ -97,8 +97,9 @@ const Holster = opt => {
|
|
|
97
97
|
|
|
98
98
|
const graph = async (soul, data, userctx, cb) => {
|
|
99
99
|
if (userctx) {
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// Sign each property individually
|
|
101
|
+
const propertySignatures = await SEA.signProperties(data, userctx)
|
|
102
|
+
return utils.graph(soul, data, propertySignatures, userctx.pub)
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
if (opt.secure) {
|
|
@@ -151,12 +152,8 @@ const Holster = opt => {
|
|
|
151
152
|
// Found a soul that needs resolving, need the previous context
|
|
152
153
|
// (ie the parent node) to find a soul relation for it.
|
|
153
154
|
const {item, soul} = ctx.chain[i - 1]
|
|
154
|
-
// If this is a user context or using secure mode then need to get the
|
|
155
|
-
// whole node for verification.
|
|
156
|
-
const lex =
|
|
157
|
-
ctx.user || opt.secure ? {"#": soul} : {"#": soul, ".": item}
|
|
158
155
|
wire.get(
|
|
159
|
-
|
|
156
|
+
{"#": soul, ".": item},
|
|
160
157
|
async msg => {
|
|
161
158
|
if (msg.err) {
|
|
162
159
|
if (ctx.user || opt.secure) {
|
|
@@ -384,10 +381,7 @@ const Holster = opt => {
|
|
|
384
381
|
// When result is true data is a property to put on the current soul.
|
|
385
382
|
// Need to check if item is a rel and also set the node to null. (This
|
|
386
383
|
// applies for any update from a rel to a property, not just null.)
|
|
387
|
-
|
|
388
|
-
const lex =
|
|
389
|
-
ctx.user || opt.secure ? {"#": soul} : {"#": soul, ".": item}
|
|
390
|
-
wire.get(lex, async msg => {
|
|
384
|
+
wire.get({"#": soul, ".": item}, async msg => {
|
|
391
385
|
if (msg.err) {
|
|
392
386
|
_ack(`error getting ${soul}: ${msg.err}`)
|
|
393
387
|
return
|
|
@@ -452,10 +446,7 @@ const Holster = opt => {
|
|
|
452
446
|
|
|
453
447
|
// Otherwise put the data using the keys returned in result.
|
|
454
448
|
// Need to check if a rel has already been added on the current node.
|
|
455
|
-
|
|
456
|
-
const lex =
|
|
457
|
-
ctx.user || opt.secure ? {"#": soul} : {"#": soul, ".": item}
|
|
458
|
-
wire.get(lex, async msg => {
|
|
449
|
+
wire.get({"#": soul, ".": item}, async msg => {
|
|
459
450
|
if (msg.err) {
|
|
460
451
|
_ack(`error getting ${soul}.${item}: ${msg.err}`)
|
|
461
452
|
return
|
package/src/radisk.js
CHANGED
|
@@ -461,10 +461,14 @@ const Radisk = opt => {
|
|
|
461
461
|
|
|
462
462
|
Radisk.encode = data => {
|
|
463
463
|
// A key should be passed in as a string to encode, a value can optionally be
|
|
464
|
-
// an array of 2 items
|
|
464
|
+
// an array of 2-3 items: [value, state] or [value, state, signature].
|
|
465
465
|
let state = ""
|
|
466
|
-
|
|
466
|
+
let sig = ""
|
|
467
|
+
if (data instanceof Array && (data.length === 2 || data.length === 3)) {
|
|
467
468
|
state = etx + data[1]
|
|
469
|
+
if (data.length === 3 && data[2]) {
|
|
470
|
+
sig = etx + data[2]
|
|
471
|
+
}
|
|
468
472
|
data = data[0]
|
|
469
473
|
}
|
|
470
474
|
|
|
@@ -475,19 +479,19 @@ Radisk.encode = data => {
|
|
|
475
479
|
while ((current = data[i++])) {
|
|
476
480
|
if (current === unit) text += unit
|
|
477
481
|
}
|
|
478
|
-
return text + '"' + data + state + unit
|
|
482
|
+
return text + '"' + data + state + sig + unit
|
|
479
483
|
}
|
|
480
484
|
|
|
481
485
|
const rel = utils.rel.is(data)
|
|
482
|
-
if (rel) return unit + "#" + rel + state + unit
|
|
486
|
+
if (rel) return unit + "#" + rel + state + sig + unit
|
|
483
487
|
|
|
484
|
-
if (utils.num.is(data)) return unit + "+" + (data || 0) + state + unit
|
|
488
|
+
if (utils.num.is(data)) return unit + "+" + (data || 0) + state + sig + unit
|
|
485
489
|
|
|
486
|
-
if (data === true) return unit + "+" + state + unit
|
|
490
|
+
if (data === true) return unit + "+" + state + sig + unit
|
|
487
491
|
|
|
488
|
-
if (data === false) return unit + "-" + state + unit
|
|
492
|
+
if (data === false) return unit + "-" + state + sig + unit
|
|
489
493
|
|
|
490
|
-
if (data === null) return unit + " " + state + unit
|
|
494
|
+
if (data === null) return unit + " " + state + sig + unit
|
|
491
495
|
}
|
|
492
496
|
|
|
493
497
|
Radisk.decode = (data, obj) => {
|
|
@@ -522,7 +526,11 @@ Radisk.decode = (data, obj) => {
|
|
|
522
526
|
|
|
523
527
|
if (obj) obj.i = i + 1
|
|
524
528
|
|
|
525
|
-
let
|
|
529
|
+
let parts = text.split(etx)
|
|
530
|
+
let value = parts[0]
|
|
531
|
+
let state = parts[1]
|
|
532
|
+
let sig = parts[2]
|
|
533
|
+
|
|
526
534
|
if (!state) {
|
|
527
535
|
if (previous === '"') return text
|
|
528
536
|
|
|
@@ -539,20 +547,36 @@ Radisk.decode = (data, obj) => {
|
|
|
539
547
|
if (previous === " ") return null
|
|
540
548
|
} else {
|
|
541
549
|
state = parseFloat(state)
|
|
542
|
-
// If state was found then return an array
|
|
543
|
-
if (
|
|
550
|
+
// If state was found then return an array with [value, state] or [value, state, sig]
|
|
551
|
+
if (sig) {
|
|
552
|
+
if (previous === '"') return [value, state, sig]
|
|
544
553
|
|
|
545
|
-
|
|
554
|
+
if (previous === "#") return [utils.rel.ify(value), state, sig]
|
|
546
555
|
|
|
547
|
-
|
|
548
|
-
|
|
556
|
+
if (previous === "+") {
|
|
557
|
+
if (value.length === 0) return [true, state, sig]
|
|
549
558
|
|
|
550
|
-
|
|
551
|
-
|
|
559
|
+
return [parseFloat(value), state, sig]
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (previous === "-") return [false, state, sig]
|
|
552
563
|
|
|
553
|
-
|
|
564
|
+
if (previous === " ") return [null, state, sig]
|
|
565
|
+
} else {
|
|
566
|
+
if (previous === '"') return [value, state]
|
|
554
567
|
|
|
555
|
-
|
|
568
|
+
if (previous === "#") return [utils.rel.ify(value), state]
|
|
569
|
+
|
|
570
|
+
if (previous === "+") {
|
|
571
|
+
if (value.length === 0) return [true, state]
|
|
572
|
+
|
|
573
|
+
return [parseFloat(value), state]
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (previous === "-") return [false, state]
|
|
577
|
+
|
|
578
|
+
if (previous === " ") return [null, state]
|
|
579
|
+
}
|
|
556
580
|
}
|
|
557
581
|
}
|
|
558
582
|
|
package/src/sea.js
CHANGED
|
@@ -118,7 +118,7 @@ const SEA = {
|
|
|
118
118
|
// Allow data to be passed in with graph meta data,
|
|
119
119
|
// (which should not be part of signature, so not verified).
|
|
120
120
|
for (const k of Object.keys(signed.m).sort()) {
|
|
121
|
-
if (k !== "_" && k
|
|
121
|
+
if (k !== "_" && k !== userPublicKey && k !== userSignature) {
|
|
122
122
|
msg[k] = signed.m[k]
|
|
123
123
|
}
|
|
124
124
|
}
|
|
@@ -135,6 +135,59 @@ const SEA = {
|
|
|
135
135
|
if (cb) cb(null)
|
|
136
136
|
return null
|
|
137
137
|
},
|
|
138
|
+
// Verify individual property signatures stored in node._["s"]
|
|
139
|
+
// Returns array of valid property names
|
|
140
|
+
verifyProperties: async (node, pub, cb) => {
|
|
141
|
+
if (!pub || !node) {
|
|
142
|
+
if (cb) cb([])
|
|
143
|
+
return []
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const key = await utils.subtle.importKey(
|
|
147
|
+
"jwk",
|
|
148
|
+
utils.jwk(pub),
|
|
149
|
+
{name: "ECDSA", namedCurve: "P-256"},
|
|
150
|
+
false,
|
|
151
|
+
["verify"],
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const alg = {name: "ECDSA", hash: {name: "SHA-256"}}
|
|
155
|
+
const valid = []
|
|
156
|
+
const propertySignatures = (node._ && node._["s"]) || {}
|
|
157
|
+
|
|
158
|
+
// Verify each property individually
|
|
159
|
+
for (const k of Object.keys(node).sort()) {
|
|
160
|
+
if (k === "_" || k == userPublicKey) continue
|
|
161
|
+
|
|
162
|
+
const propSig = propertySignatures[k]
|
|
163
|
+
if (!propSig) {
|
|
164
|
+
console.log(`warning: property '${k}' missing signature`)
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const hash = await utils.sha256(node[k])
|
|
170
|
+
const sig = new Uint8Array(SafeBuffer.from(propSig, "base64"))
|
|
171
|
+
|
|
172
|
+
const isValid = await utils.subtle.verify(
|
|
173
|
+
alg,
|
|
174
|
+
key,
|
|
175
|
+
sig,
|
|
176
|
+
new Uint8Array(hash),
|
|
177
|
+
)
|
|
178
|
+
if (isValid) {
|
|
179
|
+
valid.push(k)
|
|
180
|
+
} else {
|
|
181
|
+
console.log(`warning: property '${k}' signature verification failed`)
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(`warning: property '${k}' error verifying: ${err.message}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (cb) cb(valid)
|
|
189
|
+
return valid
|
|
190
|
+
},
|
|
138
191
|
sign: async (data, pair, cb) => {
|
|
139
192
|
if (!pair || !pair.pub || !pair.priv) {
|
|
140
193
|
if (cb) cb(null)
|
|
@@ -147,7 +200,7 @@ const SEA = {
|
|
|
147
200
|
} else {
|
|
148
201
|
const check = utils.parse(data)
|
|
149
202
|
for (const k of Object.keys(check).sort()) {
|
|
150
|
-
if (k !== "_" && k
|
|
203
|
+
if (k !== "_" && k !== userPublicKey && k !== userSignature) {
|
|
151
204
|
msg[k] = check[k]
|
|
152
205
|
}
|
|
153
206
|
}
|
|
@@ -172,6 +225,37 @@ const SEA = {
|
|
|
172
225
|
if (cb) cb(signed)
|
|
173
226
|
return signed
|
|
174
227
|
},
|
|
228
|
+
// Sign individual properties for per-property verification
|
|
229
|
+
signProperties: async (data, pair, cb) => {
|
|
230
|
+
if (!pair || !pair.pub || !pair.priv) {
|
|
231
|
+
if (cb) cb(null)
|
|
232
|
+
return null
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const check = utils.parse(data)
|
|
236
|
+
const propertySignatures = {}
|
|
237
|
+
const jwk = utils.jwk(pair.pub, pair.priv)
|
|
238
|
+
const alg = {name: "ECDSA", namedCurve: "P-256"}
|
|
239
|
+
const key = await utils.subtle.importKey("jwk", jwk, alg, false, ["sign"])
|
|
240
|
+
|
|
241
|
+
// Sign each property individually
|
|
242
|
+
for (const k of Object.keys(check).sort()) {
|
|
243
|
+
if (k !== "_" && k != userPublicKey && k != userSignature) {
|
|
244
|
+
const hash = await utils.sha256(check[k])
|
|
245
|
+
const sig = await utils.subtle.sign(
|
|
246
|
+
{name: "ECDSA", hash: {name: "SHA-256"}},
|
|
247
|
+
key,
|
|
248
|
+
new Uint8Array(hash),
|
|
249
|
+
)
|
|
250
|
+
propertySignatures[k] = SafeBuffer.from(sig, "binary").toString(
|
|
251
|
+
"base64",
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (cb) cb(propertySignatures)
|
|
257
|
+
return propertySignatures
|
|
258
|
+
},
|
|
175
259
|
work: async (data, salt, cb) => {
|
|
176
260
|
if (typeof salt === "function") {
|
|
177
261
|
cb = salt
|
package/src/store.js
CHANGED
|
@@ -224,12 +224,17 @@ const Store = opt => {
|
|
|
224
224
|
var soul = lex["#"]
|
|
225
225
|
var key = typeof lex["."] === "string" ? lex["."] : ""
|
|
226
226
|
var node
|
|
227
|
+
var signatures = {}
|
|
227
228
|
const each = (value, key) => {
|
|
228
229
|
if (!utils.match(lex["."], key)) return
|
|
229
230
|
|
|
230
231
|
if (!node) node = {_: {"#": soul, ">": {}}}
|
|
231
232
|
node[key] = value[0]
|
|
232
233
|
node._[">"][key] = value[1]
|
|
234
|
+
// If signature is present, store it in _["s"]
|
|
235
|
+
if (value.length === 3 && value[2]) {
|
|
236
|
+
signatures[key] = value[2]
|
|
237
|
+
}
|
|
233
238
|
}
|
|
234
239
|
|
|
235
240
|
radisk(soul + enq + key, (err, value) => {
|
|
@@ -242,6 +247,10 @@ const Store = opt => {
|
|
|
242
247
|
each(value, key)
|
|
243
248
|
graph = {[soul]: node}
|
|
244
249
|
}
|
|
250
|
+
// Only add _["s"] if we found any signatures
|
|
251
|
+
if (graph && graph[soul] && Object.keys(signatures).length > 0) {
|
|
252
|
+
graph[soul]._["s"] = signatures
|
|
253
|
+
}
|
|
245
254
|
cb(err, graph)
|
|
246
255
|
})
|
|
247
256
|
},
|
|
@@ -277,7 +286,10 @@ const Store = opt => {
|
|
|
277
286
|
count++
|
|
278
287
|
let value = node[key]
|
|
279
288
|
let state = node._[">"][key]
|
|
280
|
-
|
|
289
|
+
let sig = node._["s"] && node._["s"][key]
|
|
290
|
+
// Only pass signature if it exists, otherwise just [value, state]
|
|
291
|
+
let data = sig ? [value, state, sig] : [value, state]
|
|
292
|
+
radisk(soul + enq + key, data, ack)
|
|
281
293
|
})
|
|
282
294
|
})
|
|
283
295
|
},
|
package/src/user.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as utils from "./utils.js"
|
|
2
2
|
import Wire from "./wire.js"
|
|
3
3
|
import SEA from "./sea.js"
|
|
4
|
+
import {userPublicKey, userSignature} from "./utils.js"
|
|
4
5
|
|
|
5
6
|
const User = (opt, wire) => {
|
|
6
7
|
if (!wire) wire = Wire(opt)
|
|
@@ -55,15 +56,12 @@ const User = (opt, wire) => {
|
|
|
55
56
|
const salt = utils.text.random(64)
|
|
56
57
|
const work = await SEA.work(newPassword, salt)
|
|
57
58
|
const enc = await SEA.encrypt(dec, work)
|
|
58
|
-
//
|
|
59
|
+
// Only update the auth property with the new password encryption.
|
|
59
60
|
const update = {
|
|
60
|
-
username: username,
|
|
61
|
-
pub: data.pub,
|
|
62
|
-
epub: data.epub,
|
|
63
61
|
auth: JSON.stringify({enc: enc, salt: salt}),
|
|
64
62
|
}
|
|
65
|
-
const
|
|
66
|
-
const graph = utils.graph(pub,
|
|
63
|
+
const propertySignatures = await SEA.signProperties(update, user.is)
|
|
64
|
+
const graph = utils.graph(pub, update, propertySignatures, data.pub)
|
|
67
65
|
wire.put(graph, err => {
|
|
68
66
|
if (err) {
|
|
69
67
|
done(`error putting ${update} on ${pub}: ${err}`)
|
|
@@ -74,9 +72,51 @@ const User = (opt, wire) => {
|
|
|
74
72
|
return
|
|
75
73
|
}
|
|
76
74
|
|
|
75
|
+
// Migrate old per-node signatures to per-property signatures
|
|
76
|
+
// Check if data has per-property signatures
|
|
77
|
+
if (
|
|
78
|
+
!data._ ||
|
|
79
|
+
!data._["s"] ||
|
|
80
|
+
Object.keys(data._["s"]).length === 0
|
|
81
|
+
) {
|
|
82
|
+
// Data has old per-node signatures, migrate to per-property
|
|
83
|
+
// Copy all properties from existing data, excluding metadata
|
|
84
|
+
const migrationData = {}
|
|
85
|
+
for (const key of Object.keys(data)) {
|
|
86
|
+
if (
|
|
87
|
+
key !== "_" &&
|
|
88
|
+
key !== userPublicKey &&
|
|
89
|
+
key !== userSignature
|
|
90
|
+
) {
|
|
91
|
+
migrationData[key] = data[key]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Sign each property individually
|
|
95
|
+
const propertySignatures = await SEA.signProperties(
|
|
96
|
+
migrationData,
|
|
97
|
+
user.is,
|
|
98
|
+
)
|
|
99
|
+
const graph = utils.graph(
|
|
100
|
+
pub,
|
|
101
|
+
migrationData,
|
|
102
|
+
propertySignatures,
|
|
103
|
+
data.pub,
|
|
104
|
+
)
|
|
105
|
+
wire.put(graph, err => {
|
|
106
|
+
if (err) {
|
|
107
|
+
// Log migration error but don't fail authentication
|
|
108
|
+
console.log(
|
|
109
|
+
`warning: failed to migrate signatures on login: ${err}`,
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
done(null)
|
|
113
|
+
})
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
return done(null)
|
|
78
118
|
},
|
|
79
|
-
{wait:
|
|
119
|
+
{wait: 5000},
|
|
80
120
|
)
|
|
81
121
|
}
|
|
82
122
|
|
|
@@ -103,7 +143,7 @@ const User = (opt, wire) => {
|
|
|
103
143
|
pubs = Object.keys(data)
|
|
104
144
|
next()
|
|
105
145
|
},
|
|
106
|
-
{wait:
|
|
146
|
+
{wait: 5000},
|
|
107
147
|
)
|
|
108
148
|
}
|
|
109
149
|
|
|
@@ -160,8 +200,9 @@ const User = (opt, wire) => {
|
|
|
160
200
|
}
|
|
161
201
|
|
|
162
202
|
const pub = "~" + pair.pub
|
|
163
|
-
|
|
164
|
-
const
|
|
203
|
+
// Sign each property individually for new account
|
|
204
|
+
const propertySignatures = await SEA.signProperties(data, pair)
|
|
205
|
+
const graph = utils.graph(pub, data, propertySignatures, pair.pub)
|
|
165
206
|
wire.put(graph, err => {
|
|
166
207
|
creating = false
|
|
167
208
|
if (err) {
|
|
@@ -181,7 +222,7 @@ const User = (opt, wire) => {
|
|
|
181
222
|
})
|
|
182
223
|
})
|
|
183
224
|
},
|
|
184
|
-
{wait:
|
|
225
|
+
{wait: 5000},
|
|
185
226
|
)
|
|
186
227
|
},
|
|
187
228
|
auth: (username, password, cb) => {
|
|
@@ -318,9 +359,10 @@ const User = (opt, wire) => {
|
|
|
318
359
|
}
|
|
319
360
|
|
|
320
361
|
const data = {username: null, pub: null, epub: null, auth: null}
|
|
321
|
-
|
|
362
|
+
// Sign each property individually for account deletion
|
|
363
|
+
const propertySignatures = await SEA.signProperties(data, user.is)
|
|
322
364
|
const pub = "~" + user.is.pub
|
|
323
|
-
const graph = utils.graph(pub,
|
|
365
|
+
const graph = utils.graph(pub, data, propertySignatures, user.is.pub)
|
|
324
366
|
wire.put(graph, err => {
|
|
325
367
|
if (err) {
|
|
326
368
|
ack(`error putting null on ${pub}: ${err}`)
|
package/src/utils.js
CHANGED
|
@@ -90,13 +90,13 @@ export const graph = (soul, data, sig, pub) => {
|
|
|
90
90
|
g[soul]._[">"][key] = timestamp
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// public key. The signature is requried because later get requests will
|
|
96
|
-
// broadcast the data as a put, which other devices will need to verify.
|
|
93
|
+
// Store per-property signatures in node._["s"]
|
|
94
|
+
// sig is an object with per-property signatures
|
|
97
95
|
if (sig && pub) {
|
|
98
|
-
g[soul][
|
|
99
|
-
|
|
96
|
+
g[soul]._["s"] = {}
|
|
97
|
+
for (const [key, signature] of Object.entries(sig)) {
|
|
98
|
+
g[soul]._["s"][key] = signature
|
|
99
|
+
}
|
|
100
100
|
g[soul][userPublicKey] = pub
|
|
101
101
|
g[soul]._[">"][userPublicKey] = timestamp
|
|
102
102
|
}
|
package/src/wire.js
CHANGED
|
@@ -15,7 +15,7 @@ if (typeof globalThis.WebSocket === "undefined") {
|
|
|
15
15
|
// Rate limiting with throttling
|
|
16
16
|
const createRateLimiter = isTestEnv => {
|
|
17
17
|
const clients = new Map()
|
|
18
|
-
const maxRequests =
|
|
18
|
+
const maxRequests = 1500 // requests per minute
|
|
19
19
|
const windowMs = 60000 // 1 minute window
|
|
20
20
|
const disconnectThreshold = 10 // Disconnect after 10 violations
|
|
21
21
|
let cleanupInterval = null
|
|
@@ -654,7 +654,7 @@ const Wire = opt => {
|
|
|
654
654
|
// Schedule processing of next message if queue not empty
|
|
655
655
|
if (messageQueue.length > 0) {
|
|
656
656
|
// Add delay between messages to respect rate limit
|
|
657
|
-
queueProcessor = setTimeout(processQueue,
|
|
657
|
+
queueProcessor = setTimeout(processQueue, 50)
|
|
658
658
|
} else {
|
|
659
659
|
queueProcessor = null
|
|
660
660
|
}
|
|
@@ -698,7 +698,7 @@ const Wire = opt => {
|
|
|
698
698
|
messageQueue.push(data)
|
|
699
699
|
// Start queue processor if not already running
|
|
700
700
|
if (!queueProcessor) {
|
|
701
|
-
queueProcessor = setTimeout(processQueue,
|
|
701
|
+
queueProcessor = setTimeout(processQueue, 50)
|
|
702
702
|
}
|
|
703
703
|
}
|
|
704
704
|
if (!(opt.peers instanceof Array)) {
|
package/test/holster.on.test.js
CHANGED
|
@@ -16,16 +16,47 @@ describe("holster.on", () => {
|
|
|
16
16
|
})
|
|
17
17
|
|
|
18
18
|
test("on for property on root then update - event", (t, done) => {
|
|
19
|
-
//
|
|
19
|
+
// Listener doesn't get called for initial put.
|
|
20
|
+
const callback = data => {
|
|
21
|
+
assert.equal(data, "update")
|
|
22
|
+
holster.get("key").off(callback)
|
|
23
|
+
done()
|
|
24
|
+
}
|
|
25
|
+
holster.get("key").on(callback)
|
|
26
|
+
|
|
20
27
|
holster.get("key").put("value", err => {
|
|
21
28
|
assert.equal(err, null)
|
|
22
29
|
|
|
23
|
-
holster.get("key").
|
|
24
|
-
assert.equal(
|
|
25
|
-
done()
|
|
30
|
+
holster.get("key").put("update", err => {
|
|
31
|
+
assert.equal(err, null)
|
|
26
32
|
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
test("on for property then update - two listeners", (t, done) => {
|
|
37
|
+
let done1 = false
|
|
38
|
+
let done2 = false
|
|
39
|
+
|
|
40
|
+
const callback1 = data => {
|
|
41
|
+
assert.equal(data, "update")
|
|
42
|
+
holster.get("two").off(callback1)
|
|
43
|
+
if (done2) done()
|
|
44
|
+
else done1 = true
|
|
45
|
+
}
|
|
46
|
+
holster.get("two").on(callback1)
|
|
47
|
+
|
|
48
|
+
const callback2 = data => {
|
|
49
|
+
assert.equal(data, "update")
|
|
50
|
+
holster.get("two").off(callback2)
|
|
51
|
+
if (done1) done()
|
|
52
|
+
else done2 = true
|
|
53
|
+
}
|
|
54
|
+
holster.get("two").on(callback2)
|
|
55
|
+
|
|
56
|
+
holster.get("two").put("value", err => {
|
|
57
|
+
assert.equal(err, null)
|
|
58
|
+
|
|
59
|
+
holster.get("two").put("update", err => {
|
|
29
60
|
assert.equal(err, null)
|
|
30
61
|
})
|
|
31
62
|
})
|