@mblaney/holster 1.0.24 → 1.0.26
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 +14 -4
- package/src/ham.js +59 -20
- package/src/holster.js +95 -75
- package/src/sea.js +103 -68
- package/src/store.js +6 -2
- package/src/user.js +49 -36
- package/src/utils.js +15 -8
- package/src/wire.js +7 -2
package/package.json
CHANGED
package/src/get.js
CHANGED
|
@@ -24,16 +24,26 @@ const Get = (lex, graph) => {
|
|
|
24
24
|
|
|
25
25
|
node[key] = graph[soul][key]
|
|
26
26
|
node._[">"][key] = graph[soul]._[">"][key]
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const state = graph[soul]._[">"][key]
|
|
28
|
+
if (graph[soul]._["s"]) {
|
|
29
|
+
if (graph[soul]._["s"][state]) {
|
|
30
|
+
signatures[state] = graph[soul]._["s"][state]
|
|
31
|
+
} else if (graph[soul]._["s"][key]) {
|
|
32
|
+
signatures[key] = graph[soul]._["s"][key]
|
|
33
|
+
}
|
|
29
34
|
}
|
|
30
35
|
} else {
|
|
31
36
|
for (const key of Object.keys(graph[soul])) {
|
|
32
37
|
if (match(lex["."], key)) {
|
|
33
38
|
node[key] = graph[soul][key]
|
|
34
39
|
node._[">"][key] = graph[soul]._[">"][key]
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
const state = graph[soul]._[">"][key]
|
|
41
|
+
if (graph[soul]._["s"]) {
|
|
42
|
+
if (graph[soul]._["s"][state]) {
|
|
43
|
+
signatures[state] = graph[soul]._["s"][state]
|
|
44
|
+
} else if (graph[soul]._["s"][key]) {
|
|
45
|
+
signatures[key] = graph[soul]._["s"][key]
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
}
|
package/src/ham.js
CHANGED
|
@@ -7,12 +7,20 @@ const MAX_GRAPH_SIZE = 10000
|
|
|
7
7
|
|
|
8
8
|
// state and value are the incoming changes.
|
|
9
9
|
// currentState and currentValue are the current graph data.
|
|
10
|
-
const Ham = (state, currentState, value, currentValue) => {
|
|
10
|
+
const Ham = (state, currentState, value, currentValue, signed = false) => {
|
|
11
11
|
if (state < currentState) return {historical: true}
|
|
12
12
|
|
|
13
13
|
if (state > currentState) return {incoming: true}
|
|
14
14
|
|
|
15
|
-
// state is equal to currentState
|
|
15
|
+
// state is equal to currentState
|
|
16
|
+
// If using signed timestamps then reject conflicting values because the
|
|
17
|
+
// owner can only create one value per timestamp.
|
|
18
|
+
if (signed && value !== currentValue) {
|
|
19
|
+
console.log(`Signed timestamp ${state}: reject conflicting value`)
|
|
20
|
+
return {historical: true}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Lexically compare to resolve conflict (for unsigned or matching values)
|
|
16
24
|
if (typeof value !== "string") {
|
|
17
25
|
value = JSON.stringify(value) || ""
|
|
18
26
|
}
|
|
@@ -44,6 +52,7 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
44
52
|
const now = {}
|
|
45
53
|
const defer = {}
|
|
46
54
|
const validProperties = new Map() // Track valid properties per soul
|
|
55
|
+
const validTimestamps = new Map() // Track valid timestamps per soul
|
|
47
56
|
let wait = 0
|
|
48
57
|
|
|
49
58
|
for (const soul of Object.keys(change)) {
|
|
@@ -56,7 +65,7 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
56
65
|
if (!node || !node._) continue
|
|
57
66
|
|
|
58
67
|
const pub = node[utils.userPublicKey]
|
|
59
|
-
// If
|
|
68
|
+
// If timestamp signatures and public key are provided then always verify
|
|
60
69
|
if (node._["s"] && pub) verify = true
|
|
61
70
|
|
|
62
71
|
// Special case if soul starts with "~". Node must be system data ie,
|
|
@@ -77,21 +86,45 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
88
|
if (verify) {
|
|
80
|
-
//
|
|
89
|
+
// Signature verification: check node._["s"] which can contain:
|
|
90
|
+
// 1. Per-property signatures (legacy): keys are property names
|
|
91
|
+
// 2. Timestamp signatures (new): keys are numeric timestamps
|
|
81
92
|
if (!pub || !node._) {
|
|
82
93
|
continue
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
const
|
|
96
|
+
const signedTimestamps = new Set()
|
|
97
|
+
const signedProperties = new Set()
|
|
98
|
+
|
|
99
|
+
// Verify timestamp signatures (numeric keys in node._["s"])
|
|
100
|
+
for (const [key, sig] of Object.entries(node._["s"] || {})) {
|
|
101
|
+
const asNumber = Number(key)
|
|
102
|
+
if (!isNaN(asNumber) && asNumber > 0) {
|
|
103
|
+
const isValid = await SEA.verifyTimestamp(asNumber, sig, pub)
|
|
104
|
+
if (isValid) {
|
|
105
|
+
signedTimestamps.add(asNumber)
|
|
106
|
+
// Add all properties with this timestamp to signed set
|
|
107
|
+
for (const [prop, ts] of Object.entries(node._[">"] || {})) {
|
|
108
|
+
if (Number(ts) === asNumber) {
|
|
109
|
+
signedProperties.add(prop)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Verify per-property signatures (backward compatibility)
|
|
117
|
+
const perPropertySignedProps = await SEA.verifyProperties(node, pub)
|
|
118
|
+
perPropertySignedProps.forEach(prop => signedProperties.add(prop))
|
|
87
119
|
|
|
88
120
|
// If no properties verified, skip this update
|
|
89
|
-
if (
|
|
121
|
+
if (signedProperties.size === 0) {
|
|
90
122
|
continue
|
|
91
123
|
}
|
|
92
124
|
|
|
93
|
-
// Track
|
|
94
|
-
validProperties.set(soul,
|
|
125
|
+
// Track signed properties and timestamps for this soul
|
|
126
|
+
validProperties.set(soul, signedProperties)
|
|
127
|
+
validTimestamps.set(soul, signedTimestamps)
|
|
95
128
|
}
|
|
96
129
|
|
|
97
130
|
for (const key of Object.keys(node)) {
|
|
@@ -100,7 +133,6 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
100
133
|
// Skip metadata fields (including userSignature for old data)
|
|
101
134
|
if (key === utils.userSignature || key === utils.userPublicKey) continue
|
|
102
135
|
|
|
103
|
-
// If this soul had verification, only process properties that have valid signatures
|
|
104
136
|
// Properties without signatures in this update are left unchanged
|
|
105
137
|
if (validProperties.has(soul) && !validProperties.get(soul).has(key)) {
|
|
106
138
|
continue
|
|
@@ -129,11 +161,18 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
129
161
|
}
|
|
130
162
|
defer[soul][key] = value
|
|
131
163
|
defer[soul]._[">"][key] = state
|
|
132
|
-
if (node._["s"]
|
|
133
|
-
|
|
164
|
+
if (node._["s"]) {
|
|
165
|
+
if (node._["s"][state]) {
|
|
166
|
+
defer[soul]._["s"][state] = node._["s"][state]
|
|
167
|
+
} else if (node._["s"][key]) {
|
|
168
|
+
defer[soul]._["s"][key] = node._["s"][key]
|
|
169
|
+
}
|
|
134
170
|
}
|
|
135
171
|
} else {
|
|
136
|
-
|
|
172
|
+
// Check if this property has a signed timestamp
|
|
173
|
+
const isSigned =
|
|
174
|
+
validTimestamps.has(soul) && validTimestamps.get(soul).has(state)
|
|
175
|
+
const result = Ham(state, currentState, value, currentValue, isSigned)
|
|
137
176
|
if (result.incoming) {
|
|
138
177
|
if (!now[soul]) {
|
|
139
178
|
now[soul] = {_: {"#": soul, ">": {}, s: {}}}
|
|
@@ -143,8 +182,13 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
143
182
|
}
|
|
144
183
|
graph[soul][key] = now[soul][key] = value
|
|
145
184
|
graph[soul]._[">"][key] = now[soul]._[">"][key] = state
|
|
146
|
-
if (node._["s"]
|
|
147
|
-
|
|
185
|
+
if (node._["s"]) {
|
|
186
|
+
if (node._["s"][state]) {
|
|
187
|
+
graph[soul]._["s"][state] = now[soul]._["s"][state] =
|
|
188
|
+
node._["s"][state]
|
|
189
|
+
} else if (node._["s"][key]) {
|
|
190
|
+
graph[soul]._["s"][key] = now[soul]._["s"][key] = node._["s"][key]
|
|
191
|
+
}
|
|
148
192
|
}
|
|
149
193
|
// Call event listeners for update on key, mix is called before
|
|
150
194
|
// put has finished so wait for what could be multiple nested
|
|
@@ -163,11 +207,6 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
163
207
|
}
|
|
164
208
|
}
|
|
165
209
|
|
|
166
|
-
if (verify && nodeWait !== 0 && now[soul]) {
|
|
167
|
-
// Secure updates can't be split, so move now to deferred as well.
|
|
168
|
-
Object.assign(defer[soul], now[soul])
|
|
169
|
-
delete now[soul]
|
|
170
|
-
}
|
|
171
210
|
// Call event listeners for update on soul.
|
|
172
211
|
if (updated && listen[soul]) {
|
|
173
212
|
setTimeout(() => {
|
package/src/holster.js
CHANGED
|
@@ -97,9 +97,10 @@ const Holster = opt => {
|
|
|
97
97
|
|
|
98
98
|
const graph = async (soul, data, userctx, cb) => {
|
|
99
99
|
if (userctx) {
|
|
100
|
-
// Sign
|
|
101
|
-
const
|
|
102
|
-
|
|
100
|
+
// Sign the timestamp
|
|
101
|
+
const timestamp = Date.now()
|
|
102
|
+
const sig = await SEA.signTimestamp(timestamp, userctx)
|
|
103
|
+
return utils.graph(soul, data, sig, userctx.pub, timestamp)
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
if (opt.secure) {
|
|
@@ -270,7 +271,7 @@ const Holster = opt => {
|
|
|
270
271
|
// Top level keys are added to a root node so their values don't need
|
|
271
272
|
// to be objects.
|
|
272
273
|
ctxid = utils.text.random()
|
|
273
|
-
allctx.set(ctxid, {chain: [{item: key, soul: "root"}], cb: cb})
|
|
274
|
+
allctx.set(ctxid, {chain: [{item: String(key), soul: "root"}], cb: cb})
|
|
274
275
|
if (!cb) return api(ctxid)
|
|
275
276
|
|
|
276
277
|
const _done = done(ctxid)
|
|
@@ -325,7 +326,7 @@ const Holster = opt => {
|
|
|
325
326
|
|
|
326
327
|
// Push the key to the context as it needs a soul lookup.
|
|
327
328
|
// (null is used to call the api with updated context)
|
|
328
|
-
if (key !== null) ctx.chain.push({item: key, soul: null})
|
|
329
|
+
if (key !== null) ctx.chain.push({item: String(key), soul: null})
|
|
329
330
|
if (!ctx.cb) return api(ctxid)
|
|
330
331
|
|
|
331
332
|
// When there's a callback need to resolve the context first.
|
|
@@ -446,81 +447,92 @@ const Holster = opt => {
|
|
|
446
447
|
|
|
447
448
|
// Otherwise put the data using the keys returned in result.
|
|
448
449
|
// Need to check if a rel has already been added on the current node.
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
450
|
+
// Use attemptRead to retry if data hasn't propagated yet.
|
|
451
|
+
const attemptRead = (retries = 0) => {
|
|
452
|
+
wire.get({"#": soul, ".": item}, async msg => {
|
|
453
|
+
if (msg.err) {
|
|
454
|
+
_ack(`error getting ${soul}.${item}: ${msg.err}`)
|
|
455
|
+
return
|
|
456
|
+
}
|
|
454
457
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const id = utils.rel.is(current)
|
|
458
|
-
if (!id) {
|
|
459
|
-
// The current rel doesn't exist, so add it first.
|
|
460
|
-
if (!node) node = {}
|
|
461
|
-
node[item] = utils.rel.ify(utils.text.random())
|
|
462
|
-
const g = await graph(soul, node, ctx.user, _ack)
|
|
463
|
-
if (g === null) return
|
|
464
|
-
|
|
465
|
-
wire.put(g, err => {
|
|
466
|
-
if (err) {
|
|
467
|
-
_ack(`error putting ${item} on ${soul}: ${err}`)
|
|
468
|
-
} else {
|
|
469
|
-
const _ctxid = utils.text.random()
|
|
470
|
-
const chain = [{item: item, soul: soul}]
|
|
471
|
-
// Pass on the previous context's callback and user flag here.
|
|
472
|
-
allctx.set(_ctxid, {chain: chain, user: ctx.user, cb: ctx.cb})
|
|
473
|
-
api(_ctxid).put(data)
|
|
474
|
-
}
|
|
475
|
-
})
|
|
476
|
-
return
|
|
477
|
-
}
|
|
458
|
+
let node = msg.put && msg.put[soul]
|
|
459
|
+
const current = node && node[item]
|
|
478
460
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
461
|
+
// If we didn't find the item but still have retries, retry
|
|
462
|
+
if (!current && retries < 5) {
|
|
463
|
+
await new Promise(r => setTimeout(r, 50))
|
|
464
|
+
return attemptRead(retries + 1)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const id = utils.rel.is(current)
|
|
468
|
+
if (!id) {
|
|
469
|
+
// The current rel doesn't exist, so add it first.
|
|
470
|
+
if (!node) node = {}
|
|
471
|
+
node[item] = utils.rel.ify(utils.text.random())
|
|
472
|
+
const g = await graph(soul, node, ctx.user, _ack)
|
|
473
|
+
if (g === null) return
|
|
474
|
+
|
|
475
|
+
wire.put(g, err => {
|
|
476
|
+
if (err) {
|
|
477
|
+
_ack(`error putting ${item} on ${soul}: ${err}`)
|
|
478
|
+
} else {
|
|
479
|
+
const _ctxid = utils.text.random()
|
|
480
|
+
const chain = [{item: item, soul: soul}]
|
|
481
|
+
// Pass on the previous context's callback and user flag here.
|
|
482
|
+
allctx.set(_ctxid, {chain: chain, user: ctx.user, cb: ctx.cb})
|
|
483
|
+
api(_ctxid).put(data)
|
|
484
|
+
}
|
|
485
|
+
})
|
|
496
486
|
return
|
|
497
487
|
}
|
|
498
|
-
}
|
|
499
488
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
489
|
+
const update = []
|
|
490
|
+
for (const key of result) {
|
|
491
|
+
const err = await new Promise(res => {
|
|
492
|
+
if (utils.obj.is(data[key]) && !utils.rel.is(data[key])) {
|
|
493
|
+
// Use the current rel as the context for nested objects.
|
|
494
|
+
const _ctxid = utils.text.random()
|
|
495
|
+
const chain = [{item: key, soul: id}]
|
|
496
|
+
allctx.set(_ctxid, {chain: chain, user: ctx.user})
|
|
497
|
+
api(_ctxid).put(data[key], res)
|
|
498
|
+
} else {
|
|
499
|
+
// Group the rest of the updates for put below.
|
|
500
|
+
update.push(key)
|
|
501
|
+
res(null)
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
if (err) {
|
|
505
|
+
_ack(err, ctxid)
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
}
|
|
504
509
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
wire.get({"#": id}, async msg => {
|
|
508
|
-
if (msg.err) {
|
|
509
|
-
_ack(`error getting ${id}: ${msg.err}`)
|
|
510
|
+
if (update.length === 0) {
|
|
511
|
+
_ack(null)
|
|
510
512
|
return
|
|
511
513
|
}
|
|
512
514
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
515
|
+
// The nested objects created above will also have rels on the parent
|
|
516
|
+
// object, so fetch the node so the rest of the updates can be added.
|
|
517
|
+
wire.get({"#": id}, async msg => {
|
|
518
|
+
if (msg.err) {
|
|
519
|
+
_ack(`error getting ${id}: ${msg.err}`)
|
|
520
|
+
return
|
|
521
|
+
}
|
|
520
522
|
|
|
521
|
-
|
|
523
|
+
let node = msg.put && msg.put[id]
|
|
524
|
+
if (!node) node = {}
|
|
525
|
+
update.forEach(key => {
|
|
526
|
+
node[key] = data[key]
|
|
527
|
+
})
|
|
528
|
+
const g = await graph(id, node, ctx.user, _ack)
|
|
529
|
+
if (g === null) return
|
|
530
|
+
|
|
531
|
+
wire.put(g, _ack)
|
|
532
|
+
})
|
|
522
533
|
})
|
|
523
|
-
}
|
|
534
|
+
}
|
|
535
|
+
attemptRead()
|
|
524
536
|
},
|
|
525
537
|
on: (lex, cb, _get, _opt) => {
|
|
526
538
|
if (typeof lex === "function") {
|
|
@@ -557,8 +569,8 @@ const Holster = opt => {
|
|
|
557
569
|
const current = msg.put && msg.put[soul] && msg.put[soul][item]
|
|
558
570
|
const id = utils.rel.is(current)
|
|
559
571
|
if (id) {
|
|
560
|
-
// It's a rel, read the related node. It might not be in the
|
|
561
|
-
// yet when the listener fires, so we retry with delays.
|
|
572
|
+
// It's a rel, read the related node. It might not be in the
|
|
573
|
+
// graph yet when the listener fires, so we retry with delays.
|
|
562
574
|
const attemptRead = async (retries = 0) => {
|
|
563
575
|
const data = await new Promise(res => {
|
|
564
576
|
const _ctxid = utils.text.random()
|
|
@@ -587,8 +599,16 @@ const Holster = opt => {
|
|
|
587
599
|
// queueing. Initially register the soul without _get, then update if
|
|
588
600
|
// it's a rel.
|
|
589
601
|
let initialLex
|
|
590
|
-
if (lex)
|
|
591
|
-
|
|
602
|
+
if (lex) {
|
|
603
|
+
initialLex = utils.obj.put(lex, "#", soul)
|
|
604
|
+
// Normalize numeric property filters to strings for wire operations
|
|
605
|
+
// Only convert if it's a number, not if it's a complex filter object
|
|
606
|
+
if (initialLex["."] != null && typeof initialLex["."] === "number") {
|
|
607
|
+
initialLex = {...initialLex, ".": String(initialLex["."])}
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
initialLex = {"#": soul, ".": item}
|
|
611
|
+
}
|
|
592
612
|
wire.on(initialLex, map.get(cb), false, _opt)
|
|
593
613
|
|
|
594
614
|
// Check if item is a rel and update listener if needed.
|
|
@@ -609,7 +629,7 @@ const Holster = opt => {
|
|
|
609
629
|
wire.off(initialLex, map.get(cb))
|
|
610
630
|
// Then add listener on the related node
|
|
611
631
|
let relLex
|
|
612
|
-
if (lex) relLex = utils.obj.put(
|
|
632
|
+
if (lex) relLex = utils.obj.put(initialLex, "#", id)
|
|
613
633
|
else relLex = {"#": id, ".": null}
|
|
614
634
|
wire.on(relLex, map.get(cb), _get, _opt)
|
|
615
635
|
} else if (_get) {
|
|
@@ -691,7 +711,7 @@ const Holster = opt => {
|
|
|
691
711
|
}
|
|
692
712
|
|
|
693
713
|
ctxid = utils.text.random()
|
|
694
|
-
const chain = [{item: key, soul: "~" + pub}]
|
|
714
|
+
const chain = [{item: String(key), soul: "~" + pub}]
|
|
695
715
|
allctx.set(ctxid, {chain: chain, user: user.is, cb: cb})
|
|
696
716
|
if (!cb) return api(ctxid)
|
|
697
717
|
|
package/src/sea.js
CHANGED
|
@@ -135,59 +135,6 @@ 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
|
-
},
|
|
191
138
|
sign: async (data, pair, cb) => {
|
|
192
139
|
if (!pair || !pair.pub || !pair.priv) {
|
|
193
140
|
if (cb) cb(null)
|
|
@@ -225,36 +172,124 @@ const SEA = {
|
|
|
225
172
|
if (cb) cb(signed)
|
|
226
173
|
return signed
|
|
227
174
|
},
|
|
228
|
-
// Sign
|
|
229
|
-
|
|
175
|
+
// Sign a timestamp to prove ownership of an update
|
|
176
|
+
signTimestamp: async (timestamp, pair, cb) => {
|
|
230
177
|
if (!pair || !pair.pub || !pair.priv) {
|
|
231
178
|
if (cb) cb(null)
|
|
232
179
|
return null
|
|
233
180
|
}
|
|
234
181
|
|
|
235
|
-
|
|
236
|
-
const
|
|
182
|
+
// Convert timestamp to string for consistent hashing
|
|
183
|
+
const timestampStr = timestamp.toString()
|
|
184
|
+
const hash = await utils.sha256(timestampStr)
|
|
237
185
|
const jwk = utils.jwk(pair.pub, pair.priv)
|
|
238
186
|
const alg = {name: "ECDSA", namedCurve: "P-256"}
|
|
239
|
-
const key = await utils.subtle.importKey("jwk", jwk, alg, false, ["sign"])
|
|
240
187
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const sig = await utils.subtle.sign(
|
|
188
|
+
const sig = await utils.subtle
|
|
189
|
+
.importKey("jwk", jwk, alg, false, ["sign"])
|
|
190
|
+
.then(key =>
|
|
191
|
+
utils.subtle.sign(
|
|
246
192
|
{name: "ECDSA", hash: {name: "SHA-256"}},
|
|
247
193
|
key,
|
|
248
194
|
new Uint8Array(hash),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
const signature = SafeBuffer.from(sig, "binary").toString("base64")
|
|
199
|
+
|
|
200
|
+
if (cb) cb(signature)
|
|
201
|
+
return signature
|
|
202
|
+
},
|
|
203
|
+
// Verify a timestamp signature to confirm ownership of update
|
|
204
|
+
verifyTimestamp: async (timestamp, signature, publicKey, cb) => {
|
|
205
|
+
if (!publicKey || !timestamp || !signature) {
|
|
206
|
+
if (cb) cb(false)
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
// Convert timestamp to string for consistent hashing
|
|
212
|
+
const timestampStr = timestamp.toString()
|
|
213
|
+
const hash = await utils.sha256(timestampStr)
|
|
214
|
+
|
|
215
|
+
const key = await utils.subtle.importKey(
|
|
216
|
+
"jwk",
|
|
217
|
+
utils.jwk(publicKey),
|
|
218
|
+
{name: "ECDSA", namedCurve: "P-256"},
|
|
219
|
+
false,
|
|
220
|
+
["verify"],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
const sig = new Uint8Array(SafeBuffer.from(signature, "base64"))
|
|
224
|
+
const alg = {name: "ECDSA", hash: {name: "SHA-256"}}
|
|
225
|
+
|
|
226
|
+
const isValid = await utils.subtle.verify(
|
|
227
|
+
alg,
|
|
228
|
+
key,
|
|
229
|
+
sig,
|
|
230
|
+
new Uint8Array(hash),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if (cb) cb(isValid)
|
|
234
|
+
return isValid
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.log(`error verifying timestamp signature: ${err.message}`)
|
|
237
|
+
if (cb) cb(false)
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
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),
|
|
249
280
|
)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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}`)
|
|
253
288
|
}
|
|
254
289
|
}
|
|
255
290
|
|
|
256
|
-
if (cb) cb(
|
|
257
|
-
return
|
|
291
|
+
if (cb) cb(valid)
|
|
292
|
+
return valid
|
|
258
293
|
},
|
|
259
294
|
work: async (data, salt, cb) => {
|
|
260
295
|
if (typeof salt === "function") {
|
package/src/store.js
CHANGED
|
@@ -233,6 +233,11 @@ const Store = opt => {
|
|
|
233
233
|
node._[">"][key] = value[1]
|
|
234
234
|
// If signature is present, store it in _["s"]
|
|
235
235
|
if (value.length === 3 && value[2]) {
|
|
236
|
+
const state = value[1]
|
|
237
|
+
// Store signature under both state and key for compatibility
|
|
238
|
+
signatures[state] = value[2]
|
|
239
|
+
// TODO: Can probably remove the following when old data is no
|
|
240
|
+
// longer a problem.
|
|
236
241
|
signatures[key] = value[2]
|
|
237
242
|
}
|
|
238
243
|
}
|
|
@@ -286,8 +291,7 @@ const Store = opt => {
|
|
|
286
291
|
count++
|
|
287
292
|
let value = node[key]
|
|
288
293
|
let state = node._[">"][key]
|
|
289
|
-
let sig = node._["s"] && node._["s"][
|
|
290
|
-
// Only pass signature if it exists, otherwise just [value, state]
|
|
294
|
+
let sig = node._["s"] && node._["s"][state]
|
|
291
295
|
let data = sig ? [value, state, sig] : [value, state]
|
|
292
296
|
radisk(soul + enq + key, data, ack)
|
|
293
297
|
})
|