corestore 6.0.1-alpha.12 → 6.0.1-alpha.15
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/index.js +133 -28
- package/package.json +4 -7
- package/test/all.js +83 -16
- package/lib/keys.js +0 -110
- package/test/keys.js +0 -65
package/index.js
CHANGED
|
@@ -5,38 +5,91 @@ const sodium = require('sodium-universal')
|
|
|
5
5
|
const Hypercore = require('hypercore')
|
|
6
6
|
const b4a = require('b4a')
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const [NS] = crypto.namespace('corestore', 1)
|
|
9
|
+
const DEFAULT_NAMESPACE = b4a.alloc(32) // This is meant to be 32 0-bytes
|
|
9
10
|
|
|
10
11
|
const CORES_DIR = 'cores'
|
|
11
|
-
const
|
|
12
|
-
const USERDATA_NAME_KEY = '
|
|
13
|
-
const USERDATA_NAMESPACE_KEY = '
|
|
14
|
-
const DEFAULT_NAMESPACE = generateNamespace('@corestore/default')
|
|
12
|
+
const PRIMARY_KEY_FILE_NAME = 'primary-key'
|
|
13
|
+
const USERDATA_NAME_KEY = 'corestore/name'
|
|
14
|
+
const USERDATA_NAMESPACE_KEY = 'corestore/namespace'
|
|
15
15
|
|
|
16
16
|
module.exports = class Corestore extends EventEmitter {
|
|
17
17
|
constructor (storage, opts = {}) {
|
|
18
18
|
super()
|
|
19
19
|
|
|
20
|
-
this.storage = Hypercore.defaultStorage(storage, { lock:
|
|
21
|
-
|
|
20
|
+
this.storage = Hypercore.defaultStorage(storage, { lock: PRIMARY_KEY_FILE_NAME })
|
|
22
21
|
this.cores = opts._cores || new Map()
|
|
23
|
-
this.
|
|
22
|
+
this.primaryKey = null
|
|
24
23
|
|
|
25
|
-
this.
|
|
24
|
+
this._keyStorage = null
|
|
25
|
+
this._primaryKey = opts.primaryKey
|
|
26
|
+
this._namespace = opts.namespace || DEFAULT_NAMESPACE
|
|
26
27
|
this._replicationStreams = opts._streams || []
|
|
28
|
+
this._overwrite = opts.overwrite === true
|
|
29
|
+
|
|
27
30
|
this._streamSessions = opts._streamSessions || new Map()
|
|
31
|
+
this._sessions = new Set() // sessions for THIS namespace
|
|
32
|
+
|
|
33
|
+
this._findingPeersCount = 0
|
|
34
|
+
this._findingPeers = []
|
|
35
|
+
|
|
36
|
+
if (this._namespace.byteLength !== 32) throw new Error('Namespace must be a 32-byte Buffer or Uint8Array')
|
|
28
37
|
|
|
29
38
|
this._opening = opts._opening ? opts._opening.then(() => this._open()) : this._open()
|
|
30
39
|
this._opening.catch(safetyCatch)
|
|
31
40
|
this.ready = () => this._opening
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
findingPeers () {
|
|
44
|
+
let done = false
|
|
45
|
+
this._incFindingPeers()
|
|
46
|
+
|
|
47
|
+
return () => {
|
|
48
|
+
if (done) return
|
|
49
|
+
done = true
|
|
50
|
+
this._decFindingPeers()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_incFindingPeers () {
|
|
55
|
+
if (++this._findingPeersCount !== 1) return
|
|
56
|
+
|
|
57
|
+
for (const core of this._sessions) {
|
|
58
|
+
this._findingPeers.push(core.findingPeers())
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_decFindingPeers () {
|
|
63
|
+
if (--this._findingPeersCount !== 0) return
|
|
64
|
+
|
|
65
|
+
while (this._findingPeers.length > 0) {
|
|
66
|
+
this._findingPeers.pop()()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
34
70
|
async _open () {
|
|
35
|
-
if (this.
|
|
36
|
-
this.
|
|
37
|
-
|
|
38
|
-
this.keys = await KeyManager.fromStorage(p => this.storage(PROFILES_DIR + '/' + p))
|
|
71
|
+
if (this._primaryKey) {
|
|
72
|
+
this.primaryKey = await this._primaryKey
|
|
73
|
+
return this.primaryKey
|
|
39
74
|
}
|
|
75
|
+
this._keyStorage = this.storage(PRIMARY_KEY_FILE_NAME)
|
|
76
|
+
this.primaryKey = await new Promise((resolve, reject) => {
|
|
77
|
+
this._keyStorage.stat((err, st) => {
|
|
78
|
+
if (err && err.code !== 'ENOENT') return reject(err)
|
|
79
|
+
if (err || st.size < 32 || this._overwrite) {
|
|
80
|
+
const key = crypto.randomBytes(32)
|
|
81
|
+
return this._keyStorage.write(0, key, err => {
|
|
82
|
+
if (err) return reject(err)
|
|
83
|
+
return resolve(key)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
this._keyStorage.read(0, 32, (err, key) => {
|
|
87
|
+
if (err) return reject(err)
|
|
88
|
+
return resolve(key)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
return this.primaryKey
|
|
40
93
|
}
|
|
41
94
|
|
|
42
95
|
async _generateKeys (opts) {
|
|
@@ -58,7 +111,7 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
58
111
|
discoveryKey: crypto.discoveryKey(opts.publicKey)
|
|
59
112
|
}
|
|
60
113
|
}
|
|
61
|
-
const { publicKey, auth } = await this.
|
|
114
|
+
const { publicKey, auth } = await this.createKeyPair(opts.name)
|
|
62
115
|
return {
|
|
63
116
|
keyPair: {
|
|
64
117
|
publicKey,
|
|
@@ -70,6 +123,7 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
70
123
|
}
|
|
71
124
|
|
|
72
125
|
_getPrereadyUserData (core, key) {
|
|
126
|
+
// Need to manually read the header values before the Hypercore is ready, hence the ugliness.
|
|
73
127
|
for (const { key: savedKey, value } of core.core.header.userData) {
|
|
74
128
|
if (key === savedKey) return value
|
|
75
129
|
}
|
|
@@ -81,7 +135,7 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
81
135
|
if (!name) return
|
|
82
136
|
|
|
83
137
|
const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
|
|
84
|
-
const { publicKey, auth } = await this.
|
|
138
|
+
const { publicKey, auth } = await this.createKeyPair(b4a.toString(name), namespace)
|
|
85
139
|
if (!b4a.equals(publicKey, core.key)) throw new Error('Stored core key does not match the provided name')
|
|
86
140
|
|
|
87
141
|
// TODO: Should Hypercore expose a helper for this, or should preready return keypair/auth?
|
|
@@ -148,6 +202,26 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
148
202
|
return { from: core, keyPair, auth }
|
|
149
203
|
}
|
|
150
204
|
|
|
205
|
+
async createKeyPair (name) {
|
|
206
|
+
if (!this.primaryKey) await this._opening
|
|
207
|
+
|
|
208
|
+
const keyPair = {
|
|
209
|
+
publicKey: b4a.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
|
|
210
|
+
secretKey: b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES),
|
|
211
|
+
auth: {
|
|
212
|
+
sign: (msg) => sign(keyPair, msg),
|
|
213
|
+
verify: (signable, signature) => {
|
|
214
|
+
return sodium.crypto_sign_detached(signature, signable, keyPair.publicKey)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const seed = deriveSeed(this.primaryKey, this._namespace, name)
|
|
220
|
+
sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, seed)
|
|
221
|
+
|
|
222
|
+
return keyPair
|
|
223
|
+
}
|
|
224
|
+
|
|
151
225
|
get (opts = {}) {
|
|
152
226
|
opts = validateGetOptions(opts)
|
|
153
227
|
const core = new Hypercore(null, {
|
|
@@ -155,6 +229,19 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
155
229
|
name: null,
|
|
156
230
|
preload: () => this._preload(opts)
|
|
157
231
|
})
|
|
232
|
+
|
|
233
|
+
this._sessions.add(core)
|
|
234
|
+
if (this._findingPeersCount > 0) {
|
|
235
|
+
this._findingPeers.push(core.findingPeers())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
core.once('close', () => {
|
|
239
|
+
// technically better to also clear _findingPeers if we added it,
|
|
240
|
+
// but the lifecycle for those are pretty short so prob not worth the complexity
|
|
241
|
+
// as _decFindingPeers clear them all.
|
|
242
|
+
this._sessions.delete(core)
|
|
243
|
+
})
|
|
244
|
+
|
|
158
245
|
return core
|
|
159
246
|
}
|
|
160
247
|
|
|
@@ -189,20 +276,23 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
189
276
|
}
|
|
190
277
|
|
|
191
278
|
namespace (name) {
|
|
192
|
-
if (!b4a.isBuffer(name)) name = b4a.from(name)
|
|
193
279
|
return new Corestore(this.storage, {
|
|
194
|
-
|
|
280
|
+
primaryKey: this._opening.then(() => this.primaryKey),
|
|
281
|
+
namespace: generateNamespace(this._namespace, name),
|
|
195
282
|
_opening: this._opening,
|
|
196
283
|
_cores: this.cores,
|
|
197
284
|
_streams: this._replicationStreams,
|
|
198
|
-
_streamSessions: this._streamSessions
|
|
199
|
-
keys: this._opening.then(() => this.keys)
|
|
285
|
+
_streamSessions: this._streamSessions
|
|
200
286
|
})
|
|
201
287
|
}
|
|
202
288
|
|
|
203
289
|
async _close () {
|
|
204
290
|
await this._opening
|
|
205
|
-
if (!this._namespace
|
|
291
|
+
if (!b4a.equals(this._namespace, DEFAULT_NAMESPACE)) {
|
|
292
|
+
// namespaces should not release resources on close
|
|
293
|
+
// TODO: Refactor the namespace close logic to actually close sessions with ref counting
|
|
294
|
+
return
|
|
295
|
+
}
|
|
206
296
|
const closePromises = []
|
|
207
297
|
for (const core of this.cores.values()) {
|
|
208
298
|
closePromises.push(core.close())
|
|
@@ -212,7 +302,13 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
212
302
|
// Only close streams that were created by the Corestore
|
|
213
303
|
if (!isExternal) stream.destroy()
|
|
214
304
|
}
|
|
215
|
-
|
|
305
|
+
if (!this._keyStorage) return
|
|
306
|
+
await new Promise((resolve, reject) => {
|
|
307
|
+
this._keyStorage.close(err => {
|
|
308
|
+
if (err) return reject(err)
|
|
309
|
+
return resolve(null)
|
|
310
|
+
})
|
|
311
|
+
})
|
|
216
312
|
}
|
|
217
313
|
|
|
218
314
|
close () {
|
|
@@ -221,10 +317,13 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
221
317
|
this._closing.catch(safetyCatch)
|
|
222
318
|
return this._closing
|
|
223
319
|
}
|
|
320
|
+
}
|
|
224
321
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
322
|
+
function sign (keyPair, message) {
|
|
323
|
+
if (!keyPair.secretKey) throw new Error('Invalid key pair')
|
|
324
|
+
const signature = b4a.allocUnsafe(sodium.crypto_sign_BYTES)
|
|
325
|
+
sodium.crypto_sign_detached(signature, message, keyPair.secretKey)
|
|
326
|
+
return signature
|
|
228
327
|
}
|
|
229
328
|
|
|
230
329
|
function validateGetOptions (opts) {
|
|
@@ -244,11 +343,17 @@ function validateGetOptions (opts) {
|
|
|
244
343
|
return opts
|
|
245
344
|
}
|
|
246
345
|
|
|
247
|
-
function generateNamespace (
|
|
248
|
-
if (!b4a.isBuffer(
|
|
249
|
-
if (second && !b4a.isBuffer(second)) second = b4a.from(second)
|
|
346
|
+
function generateNamespace (namespace, name) {
|
|
347
|
+
if (!b4a.isBuffer(name)) name = b4a.from(name)
|
|
250
348
|
const out = b4a.allocUnsafe(32)
|
|
251
|
-
sodium.
|
|
349
|
+
sodium.crypto_generichash_batch(out, [namespace, name])
|
|
350
|
+
return out
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function deriveSeed (primaryKey, namespace, name) {
|
|
354
|
+
if (!b4a.isBuffer(name)) name = b4a.from(name)
|
|
355
|
+
const out = b4a.alloc(32)
|
|
356
|
+
sodium.crypto_generichash_batch(out, [NS, namespace, name], primaryKey)
|
|
252
357
|
return out
|
|
253
358
|
}
|
|
254
359
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "corestore",
|
|
3
|
-
"version": "6.0.1-alpha.
|
|
3
|
+
"version": "6.0.1-alpha.15",
|
|
4
4
|
"description": "A Hypercore factory that simplifies managing collections of cores.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -20,16 +20,13 @@
|
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/hypercore-protocol/corestore#readme",
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"brittle": "^
|
|
23
|
+
"brittle": "^2.2.10",
|
|
24
24
|
"random-access-file": "^2.2.0",
|
|
25
|
-
"random-access-memory": "^
|
|
26
|
-
"standardx": "^7.0.0"
|
|
27
|
-
"tmp-promise": "^3.0.2"
|
|
25
|
+
"random-access-memory": "^4.0.0",
|
|
26
|
+
"standardx": "^7.0.0"
|
|
28
27
|
},
|
|
29
28
|
"dependencies": {
|
|
30
29
|
"b4a": "^1.3.1",
|
|
31
|
-
"blake2b-universal": "^1.0.1",
|
|
32
|
-
"derive-key": "^1.0.1",
|
|
33
30
|
"hypercore": "next",
|
|
34
31
|
"hypercore-crypto": "^3.2.1",
|
|
35
32
|
"safety-catch": "^1.0.1",
|
package/test/all.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const { test, configure } = require('brittle')
|
|
2
2
|
const crypto = require('hypercore-crypto')
|
|
3
3
|
const ram = require('random-access-memory')
|
|
4
|
-
const
|
|
4
|
+
const os = require('os')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const b4a = require('b4a')
|
|
7
|
+
const sodium = require('sodium-universal')
|
|
5
8
|
|
|
6
9
|
const Corestore = require('..')
|
|
7
10
|
|
|
@@ -99,8 +102,8 @@ test('replicating cores created after replication begins', async function (t) {
|
|
|
99
102
|
})
|
|
100
103
|
|
|
101
104
|
test('replicating cores using discovery key hook', async function (t) {
|
|
102
|
-
const dir =
|
|
103
|
-
let store1 = new Corestore(dir
|
|
105
|
+
const dir = tmpdir()
|
|
106
|
+
let store1 = new Corestore(dir)
|
|
104
107
|
const store2 = new Corestore(ram)
|
|
105
108
|
|
|
106
109
|
const core = store1.get({ name: 'main' })
|
|
@@ -108,15 +111,13 @@ test('replicating cores using discovery key hook', async function (t) {
|
|
|
108
111
|
const key = core.key
|
|
109
112
|
|
|
110
113
|
await store1.close()
|
|
111
|
-
store1 = new Corestore(dir
|
|
114
|
+
store1 = new Corestore(dir)
|
|
112
115
|
|
|
113
116
|
const s = store1.replicate(true, { live: true })
|
|
114
117
|
s.pipe(store2.replicate(false, { live: true })).pipe(s)
|
|
115
118
|
|
|
116
119
|
const core2 = store2.get(key)
|
|
117
120
|
t.alike(await core2.get(0), Buffer.from('hello'))
|
|
118
|
-
|
|
119
|
-
await dir.cleanup()
|
|
120
121
|
})
|
|
121
122
|
|
|
122
123
|
test('nested namespaces', async function (t) {
|
|
@@ -144,9 +145,9 @@ test('core uncached when all sessions close', async function (t) {
|
|
|
144
145
|
})
|
|
145
146
|
|
|
146
147
|
test('writable core loaded from name userData', async function (t) {
|
|
147
|
-
const dir =
|
|
148
|
+
const dir = tmpdir()
|
|
148
149
|
|
|
149
|
-
let store = new Corestore(dir
|
|
150
|
+
let store = new Corestore(dir)
|
|
150
151
|
let core = store.get({ name: 'main' })
|
|
151
152
|
await core.ready()
|
|
152
153
|
const key = core.key
|
|
@@ -156,7 +157,7 @@ test('writable core loaded from name userData', async function (t) {
|
|
|
156
157
|
t.is(core.length, 1)
|
|
157
158
|
|
|
158
159
|
await store.close()
|
|
159
|
-
store = new Corestore(dir
|
|
160
|
+
store = new Corestore(dir)
|
|
160
161
|
core = store.get(key)
|
|
161
162
|
await core.ready()
|
|
162
163
|
|
|
@@ -165,25 +166,21 @@ test('writable core loaded from name userData', async function (t) {
|
|
|
165
166
|
t.is(core.length, 2)
|
|
166
167
|
t.alike(await core.get(0), Buffer.from('hello'))
|
|
167
168
|
t.alike(await core.get(1), Buffer.from('world'))
|
|
168
|
-
|
|
169
|
-
await dir.cleanup()
|
|
170
169
|
})
|
|
171
170
|
|
|
172
171
|
test('storage locking', async function (t) {
|
|
173
|
-
const dir =
|
|
172
|
+
const dir = tmpdir()
|
|
174
173
|
|
|
175
|
-
const store1 = new Corestore(dir
|
|
174
|
+
const store1 = new Corestore(dir)
|
|
176
175
|
await store1.ready()
|
|
177
176
|
|
|
178
|
-
const store2 = new Corestore(dir
|
|
177
|
+
const store2 = new Corestore(dir)
|
|
179
178
|
try {
|
|
180
179
|
await store2.ready()
|
|
181
180
|
t.fail('dir should have been locked')
|
|
182
181
|
} catch {
|
|
183
182
|
t.pass('dir was locked')
|
|
184
183
|
}
|
|
185
|
-
|
|
186
|
-
await dir.cleanup()
|
|
187
184
|
})
|
|
188
185
|
|
|
189
186
|
test('closing a namespace does not close cores', async function (t) {
|
|
@@ -205,3 +202,73 @@ test('closing a namespace does not close cores', async function (t) {
|
|
|
205
202
|
t.ok(core1.closed)
|
|
206
203
|
t.ok(core2.closed)
|
|
207
204
|
})
|
|
205
|
+
|
|
206
|
+
test('findingPeers', async function (t) {
|
|
207
|
+
t.plan(6)
|
|
208
|
+
|
|
209
|
+
const store = new Corestore(ram)
|
|
210
|
+
|
|
211
|
+
const ns1 = store.namespace('ns1')
|
|
212
|
+
const ns2 = store.namespace('ns2')
|
|
213
|
+
|
|
214
|
+
const a = ns1.get(Buffer.alloc(32).fill('a'))
|
|
215
|
+
const b = ns2.get(Buffer.alloc(32).fill('b'))
|
|
216
|
+
|
|
217
|
+
const done = ns1.findingPeers()
|
|
218
|
+
|
|
219
|
+
let aUpdated = false
|
|
220
|
+
let bUpdated = false
|
|
221
|
+
let cUpdated = false
|
|
222
|
+
|
|
223
|
+
const c = ns1.get(Buffer.alloc(32).fill('c'))
|
|
224
|
+
|
|
225
|
+
a.update().then(function (bool) {
|
|
226
|
+
aUpdated = true
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
b.update().then(function (bool) {
|
|
230
|
+
bUpdated = true
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
c.update().then(function (bool) {
|
|
234
|
+
cUpdated = true
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
238
|
+
|
|
239
|
+
t.is(aUpdated, false)
|
|
240
|
+
t.is(bUpdated, true)
|
|
241
|
+
t.is(cUpdated, false)
|
|
242
|
+
|
|
243
|
+
done()
|
|
244
|
+
|
|
245
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
246
|
+
|
|
247
|
+
t.is(aUpdated, true)
|
|
248
|
+
t.is(bUpdated, true)
|
|
249
|
+
t.is(cUpdated, true)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('different primary keys yield different keypairs', async function (t) {
|
|
253
|
+
const pk1 = randomBytes(32)
|
|
254
|
+
const pk2 = randomBytes(32)
|
|
255
|
+
t.unlike(pk1, pk2)
|
|
256
|
+
|
|
257
|
+
const store1 = new Corestore(ram, { primaryKey: pk1 })
|
|
258
|
+
const store2 = new Corestore(ram, { primaryKey: pk2 })
|
|
259
|
+
|
|
260
|
+
const kp1 = await store1.createKeyPair('hello')
|
|
261
|
+
const kp2 = await store2.createKeyPair('hello')
|
|
262
|
+
|
|
263
|
+
t.unlike(kp1.publicKey, kp2.publicKey)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
function tmpdir () {
|
|
267
|
+
return path.join(os.tmpdir(), 'corestore-' + Math.random().toString(16).slice(2))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function randomBytes (n) {
|
|
271
|
+
const buf = b4a.allocUnsafe(n)
|
|
272
|
+
sodium.randombytes_buf(buf)
|
|
273
|
+
return buf
|
|
274
|
+
}
|
package/lib/keys.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
// TODO: Extract this into a standalone module
|
|
2
|
-
|
|
3
|
-
const sodium = require('sodium-universal')
|
|
4
|
-
const blake2b = require('blake2b-universal')
|
|
5
|
-
const b4a = require('b4a')
|
|
6
|
-
|
|
7
|
-
const DEFAULT_TOKEN = b4a.alloc(0)
|
|
8
|
-
const NAMESPACE = b4a.from('@hyperspace/key-manager')
|
|
9
|
-
|
|
10
|
-
module.exports = class KeyManager {
|
|
11
|
-
constructor (storage, profile, opts = {}) {
|
|
12
|
-
this.storage = storage
|
|
13
|
-
this.profile = profile
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
_sign (keyPair, message) {
|
|
17
|
-
if (!keyPair._secretKey) throw new Error('Invalid key pair')
|
|
18
|
-
const signature = b4a.allocUnsafe(sodium.crypto_sign_BYTES)
|
|
19
|
-
sodium.crypto_sign_detached(signature, message, keyPair._secretKey)
|
|
20
|
-
return signature
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
createSecret (name, token) {
|
|
24
|
-
return deriveSeed(this.profile, token, name)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
createHypercoreKeyPair (name, token) {
|
|
28
|
-
const keyPair = {
|
|
29
|
-
publicKey: b4a.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
|
|
30
|
-
_secretKey: b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES),
|
|
31
|
-
auth: {
|
|
32
|
-
sign: (msg) => this._sign(keyPair, msg),
|
|
33
|
-
verify: (signable, signature) => {
|
|
34
|
-
return sodium.crypto_sign_detached(signature, signable, keyPair.publicKey)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair._secretKey, this.createSecret(name, token))
|
|
40
|
-
|
|
41
|
-
return keyPair
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
createNetworkIdentity (name, token) {
|
|
45
|
-
const keyPair = {
|
|
46
|
-
publicKey: b4a.alloc(32),
|
|
47
|
-
secretKey: b4a.alloc(64)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, this.createSecret(name, token))
|
|
51
|
-
|
|
52
|
-
return keyPair
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
close () {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
this.storage.close(err => {
|
|
58
|
-
if (err) return reject(err)
|
|
59
|
-
return resolve()
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
static createToken () {
|
|
65
|
-
return randomBytes(32)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
static async fromStorage (storage, opts = {}) {
|
|
69
|
-
const profileStorage = storage(opts.name || 'default')
|
|
70
|
-
|
|
71
|
-
const profile = await new Promise((resolve, reject) => {
|
|
72
|
-
profileStorage.stat((err, st) => {
|
|
73
|
-
if (err && err.code !== 'ENOENT') return reject(err)
|
|
74
|
-
if (err || st.size < 32 || opts.overwrite) {
|
|
75
|
-
const key = randomBytes(32)
|
|
76
|
-
return profileStorage.write(0, key, err => {
|
|
77
|
-
if (err) return reject(err)
|
|
78
|
-
return resolve(key)
|
|
79
|
-
})
|
|
80
|
-
}
|
|
81
|
-
profileStorage.read(0, 32, (err, key) => {
|
|
82
|
-
if (err) return reject(err)
|
|
83
|
-
return resolve(key)
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
return new this(profileStorage, profile, opts)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function deriveSeed (profile, token, name, output) {
|
|
93
|
-
if (token && token.length < 32) throw new Error('Token must be a Buffer with length >= 32')
|
|
94
|
-
if (!name || typeof name !== 'string') throw new Error('name must be a String')
|
|
95
|
-
if (!output) output = b4a.alloc(32)
|
|
96
|
-
|
|
97
|
-
blake2b.batch(output, [
|
|
98
|
-
NAMESPACE,
|
|
99
|
-
token || DEFAULT_TOKEN,
|
|
100
|
-
b4a.from(b4a.byteLength(name, 'ascii') + '\n' + name, 'ascii')
|
|
101
|
-
], profile)
|
|
102
|
-
|
|
103
|
-
return output
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function randomBytes (n) {
|
|
107
|
-
const buf = b4a.allocUnsafe(n)
|
|
108
|
-
sodium.randombytes_buf(buf)
|
|
109
|
-
return buf
|
|
110
|
-
}
|
package/test/keys.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
const p = require('path')
|
|
2
|
-
const fs = require('fs')
|
|
3
|
-
|
|
4
|
-
const test = require('brittle')
|
|
5
|
-
const ram = require('random-access-memory')
|
|
6
|
-
const raf = require('random-access-file')
|
|
7
|
-
|
|
8
|
-
const KeyManager = require('../lib/keys')
|
|
9
|
-
|
|
10
|
-
test('can create hypercore keypairs', async t => {
|
|
11
|
-
const keys = await KeyManager.fromStorage(ram)
|
|
12
|
-
|
|
13
|
-
const kp1 = await keys.createHypercoreKeyPair('core1')
|
|
14
|
-
const kp2 = await keys.createHypercoreKeyPair('core2')
|
|
15
|
-
|
|
16
|
-
t.is(kp1.publicKey.length, 32)
|
|
17
|
-
t.is(kp2.publicKey.length, 32)
|
|
18
|
-
t.unlike(kp1.publicKey, kp2.publicKey)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test('distinct tokens create distinct hypercore keypairs', async t => {
|
|
22
|
-
const keys = await KeyManager.fromStorage(ram)
|
|
23
|
-
const token1 = KeyManager.createToken()
|
|
24
|
-
const token2 = KeyManager.createToken()
|
|
25
|
-
|
|
26
|
-
const kp1 = await keys.createHypercoreKeyPair('core1', token1)
|
|
27
|
-
const kp2 = await keys.createHypercoreKeyPair('core1', token2)
|
|
28
|
-
|
|
29
|
-
t.unlike(kp1.publicKey, kp2.publicKey)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('short user-provided token will throw', async t => {
|
|
33
|
-
const keys = await KeyManager.fromStorage(ram)
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
await keys.createHypercoreKeyPair('core1', Buffer.from('hello'))
|
|
37
|
-
t.fail('did not throw')
|
|
38
|
-
} catch {
|
|
39
|
-
t.pass('threw correctly')
|
|
40
|
-
}
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
test('persistent storage regenerates keys correctly', async t => {
|
|
44
|
-
const testPath = p.resolve(__dirname, 'test-data')
|
|
45
|
-
|
|
46
|
-
const keys1 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
|
|
47
|
-
const kp1 = await keys1.createHypercoreKeyPair('core1')
|
|
48
|
-
|
|
49
|
-
const keys2 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
|
|
50
|
-
const kp2 = await keys2.createHypercoreKeyPair('core1')
|
|
51
|
-
|
|
52
|
-
t.alike(kp1.publicKey, kp2.publicKey)
|
|
53
|
-
|
|
54
|
-
await fs.promises.rm(testPath, { recursive: true })
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('different master keys -> different keys', async t => {
|
|
58
|
-
const keys1 = await KeyManager.fromStorage(ram)
|
|
59
|
-
const keys2 = await KeyManager.fromStorage(ram)
|
|
60
|
-
|
|
61
|
-
const kp1 = await keys1.createHypercoreKeyPair('core1')
|
|
62
|
-
const kp2 = await keys2.createHypercoreKeyPair('core1')
|
|
63
|
-
|
|
64
|
-
t.unlike(kp1.publicKey, kp2.publicKey)
|
|
65
|
-
})
|