@mblaney/holster 1.0.24 → 1.0.25
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 +53 -20
- package/src/holster.js +95 -75
- package/src/sea.js +52 -70
- package/src/store.js +6 -2
- package/src/user.js +49 -36
- package/src/utils.js +15 -8
- package/src/wire.js +6 -0
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,39 @@ 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
|
+
for (const [key, sig] of Object.entries(node._["s"] || {})) {
|
|
99
|
+
const asNumber = Number(key)
|
|
100
|
+
if (!isNaN(asNumber) && asNumber > 0) {
|
|
101
|
+
const isValid = await SEA.verifyTimestamp(asNumber, sig, pub)
|
|
102
|
+
if (isValid) {
|
|
103
|
+
signedTimestamps.add(asNumber)
|
|
104
|
+
// Add all properties with this timestamp to signed set
|
|
105
|
+
for (const [prop, ts] of Object.entries(node._[">"] || {})) {
|
|
106
|
+
if (ts === asNumber) {
|
|
107
|
+
signedProperties.add(prop)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
87
113
|
|
|
88
114
|
// If no properties verified, skip this update
|
|
89
|
-
if (
|
|
115
|
+
if (signedProperties.size === 0) {
|
|
90
116
|
continue
|
|
91
117
|
}
|
|
92
118
|
|
|
93
|
-
// Track
|
|
94
|
-
validProperties.set(soul,
|
|
119
|
+
// Track signed properties and timestamps for this soul
|
|
120
|
+
validProperties.set(soul, signedProperties)
|
|
121
|
+
validTimestamps.set(soul, signedTimestamps)
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
for (const key of Object.keys(node)) {
|
|
@@ -100,7 +127,6 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
100
127
|
// Skip metadata fields (including userSignature for old data)
|
|
101
128
|
if (key === utils.userSignature || key === utils.userPublicKey) continue
|
|
102
129
|
|
|
103
|
-
// If this soul had verification, only process properties that have valid signatures
|
|
104
130
|
// Properties without signatures in this update are left unchanged
|
|
105
131
|
if (validProperties.has(soul) && !validProperties.get(soul).has(key)) {
|
|
106
132
|
continue
|
|
@@ -129,11 +155,18 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
129
155
|
}
|
|
130
156
|
defer[soul][key] = value
|
|
131
157
|
defer[soul]._[">"][key] = state
|
|
132
|
-
if (node._["s"]
|
|
133
|
-
|
|
158
|
+
if (node._["s"]) {
|
|
159
|
+
if (node._["s"][state]) {
|
|
160
|
+
defer[soul]._["s"][state] = node._["s"][state]
|
|
161
|
+
} else if (node._["s"][key]) {
|
|
162
|
+
defer[soul]._["s"][key] = node._["s"][key]
|
|
163
|
+
}
|
|
134
164
|
}
|
|
135
165
|
} else {
|
|
136
|
-
|
|
166
|
+
// Check if this property has a signed timestamp
|
|
167
|
+
const isSigned =
|
|
168
|
+
validTimestamps.has(soul) && validTimestamps.get(soul).has(state)
|
|
169
|
+
const result = Ham(state, currentState, value, currentValue, isSigned)
|
|
137
170
|
if (result.incoming) {
|
|
138
171
|
if (!now[soul]) {
|
|
139
172
|
now[soul] = {_: {"#": soul, ">": {}, s: {}}}
|
|
@@ -143,8 +176,13 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
143
176
|
}
|
|
144
177
|
graph[soul][key] = now[soul][key] = value
|
|
145
178
|
graph[soul]._[">"][key] = now[soul]._[">"][key] = state
|
|
146
|
-
if (node._["s"]
|
|
147
|
-
|
|
179
|
+
if (node._["s"]) {
|
|
180
|
+
if (node._["s"][state]) {
|
|
181
|
+
graph[soul]._["s"][state] = now[soul]._["s"][state] =
|
|
182
|
+
node._["s"][state]
|
|
183
|
+
} else if (node._["s"][key]) {
|
|
184
|
+
graph[soul]._["s"][key] = now[soul]._["s"][key] = node._["s"][key]
|
|
185
|
+
}
|
|
148
186
|
}
|
|
149
187
|
// Call event listeners for update on key, mix is called before
|
|
150
188
|
// put has finished so wait for what could be multiple nested
|
|
@@ -163,11 +201,6 @@ Ham.mix = async (change, graph, secure, listen) => {
|
|
|
163
201
|
}
|
|
164
202
|
}
|
|
165
203
|
|
|
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
204
|
// Call event listeners for update on soul.
|
|
172
205
|
if (updated && listen[soul]) {
|
|
173
206
|
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,71 @@ 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),
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
254
208
|
}
|
|
255
209
|
|
|
256
|
-
|
|
257
|
-
|
|
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
|
+
}
|
|
258
240
|
},
|
|
259
241
|
work: async (data, salt, cb) => {
|
|
260
242
|
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
|
})
|