corestore 6.0.0-alpha.4 → 6.0.0
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/.github/workflows/test-node.yml +5 -6
- package/README.md +2 -2
- package/index.js +224 -77
- package/package.json +11 -12
- package/test/all.js +227 -49
- package/test/cache.js +46 -0
- package/lib/keys.js +0 -104
- package/test/keys.js +0 -74
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
name:
|
|
2
|
-
|
|
1
|
+
name: Build Status
|
|
3
2
|
on:
|
|
4
3
|
push:
|
|
5
4
|
branches:
|
|
6
|
-
-
|
|
5
|
+
- master
|
|
7
6
|
pull_request:
|
|
8
7
|
branches:
|
|
9
|
-
-
|
|
8
|
+
- master
|
|
10
9
|
jobs:
|
|
11
10
|
build:
|
|
12
11
|
strategy:
|
|
13
12
|
matrix:
|
|
14
|
-
node-version: [
|
|
13
|
+
node-version: [lts/*]
|
|
15
14
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
15
|
runs-on: ${{ matrix.os }}
|
|
17
16
|
steps:
|
|
18
17
|
- uses: actions/checkout@v2
|
|
19
18
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
20
|
-
uses: actions/setup-node@
|
|
19
|
+
uses: actions/setup-node@v2
|
|
21
20
|
with:
|
|
22
21
|
node-version: ${{ matrix.node-version }}
|
|
23
22
|
- run: npm install
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Corestore
|
|
1
|
+
# Corestore
|
|
2
2
|
|
|
3
3
|
Corestore is a Hypercore factory that makes it easier to manage large collections of named Hypercores.
|
|
4
4
|
|
|
@@ -9,7 +9,7 @@ Corestore provides:
|
|
|
9
9
|
4. __Namespacing__ - You can share a single Corestore instance between multiple applications or components without worrying about naming collisions by creating "namespaces" (e.g. `corestore.namespace('my-app').get({ name: 'main' })
|
|
10
10
|
|
|
11
11
|
### Installation
|
|
12
|
-
`npm install corestore
|
|
12
|
+
`npm install corestore`
|
|
13
13
|
|
|
14
14
|
### Usage
|
|
15
15
|
A corestore instance can be constructed with a random-access-storage module, a function that returns a random-access-storage module given a path, or a string. If a string is specified, it will be assumed to be a path to a local storage directory:
|
package/index.js
CHANGED
|
@@ -1,47 +1,105 @@
|
|
|
1
1
|
const { EventEmitter } = require('events')
|
|
2
|
+
const safetyCatch = require('safety-catch')
|
|
2
3
|
const crypto = require('hypercore-crypto')
|
|
3
4
|
const sodium = require('sodium-universal')
|
|
4
5
|
const Hypercore = require('hypercore')
|
|
6
|
+
const Xache = require('xache')
|
|
7
|
+
const b4a = require('b4a')
|
|
5
8
|
|
|
6
|
-
const
|
|
9
|
+
const [NS] = crypto.namespace('corestore', 1)
|
|
10
|
+
const DEFAULT_NAMESPACE = b4a.alloc(32) // This is meant to be 32 0-bytes
|
|
7
11
|
|
|
8
12
|
const CORES_DIR = 'cores'
|
|
9
|
-
const
|
|
10
|
-
const USERDATA_NAME_KEY = '
|
|
11
|
-
const USERDATA_NAMESPACE_KEY = '
|
|
12
|
-
const DEFAULT_NAMESPACE = generateNamespace('@corestore/default')
|
|
13
|
+
const PRIMARY_KEY_FILE_NAME = 'primary-key'
|
|
14
|
+
const USERDATA_NAME_KEY = 'corestore/name'
|
|
15
|
+
const USERDATA_NAMESPACE_KEY = 'corestore/namespace'
|
|
13
16
|
|
|
14
17
|
module.exports = class Corestore extends EventEmitter {
|
|
15
18
|
constructor (storage, opts = {}) {
|
|
16
19
|
super()
|
|
17
20
|
|
|
18
|
-
this.storage = Hypercore.defaultStorage(storage, { lock:
|
|
19
|
-
|
|
21
|
+
this.storage = Hypercore.defaultStorage(storage, { lock: PRIMARY_KEY_FILE_NAME })
|
|
20
22
|
this.cores = opts._cores || new Map()
|
|
21
|
-
this.
|
|
23
|
+
this.primaryKey = null
|
|
24
|
+
this.cache = !!opts.cache
|
|
22
25
|
|
|
23
|
-
this.
|
|
26
|
+
this._keyStorage = null
|
|
27
|
+
this._primaryKey = opts.primaryKey
|
|
28
|
+
this._namespace = opts.namespace || DEFAULT_NAMESPACE
|
|
24
29
|
this._replicationStreams = opts._streams || []
|
|
30
|
+
this._overwrite = opts.overwrite === true
|
|
31
|
+
|
|
32
|
+
this._streamSessions = opts._streamSessions || new Map()
|
|
33
|
+
this._sessions = new Set() // sessions for THIS namespace
|
|
34
|
+
|
|
35
|
+
this._findingPeersCount = 0
|
|
36
|
+
this._findingPeers = []
|
|
37
|
+
|
|
38
|
+
if (this._namespace.byteLength !== 32) throw new Error('Namespace must be a 32-byte Buffer or Uint8Array')
|
|
25
39
|
|
|
26
40
|
this._opening = opts._opening ? opts._opening.then(() => this._open()) : this._open()
|
|
27
|
-
this._opening.catch(
|
|
41
|
+
this._opening.catch(safetyCatch)
|
|
28
42
|
this.ready = () => this._opening
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
findingPeers () {
|
|
46
|
+
let done = false
|
|
47
|
+
this._incFindingPeers()
|
|
48
|
+
|
|
49
|
+
return () => {
|
|
50
|
+
if (done) return
|
|
51
|
+
done = true
|
|
52
|
+
this._decFindingPeers()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_incFindingPeers () {
|
|
57
|
+
if (++this._findingPeersCount !== 1) return
|
|
58
|
+
|
|
59
|
+
for (const core of this._sessions) {
|
|
60
|
+
this._findingPeers.push(core.findingPeers())
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_decFindingPeers () {
|
|
65
|
+
if (--this._findingPeersCount !== 0) return
|
|
66
|
+
|
|
67
|
+
while (this._findingPeers.length > 0) {
|
|
68
|
+
this._findingPeers.pop()()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
31
72
|
async _open () {
|
|
32
|
-
if (this.
|
|
33
|
-
this.
|
|
34
|
-
|
|
35
|
-
this.keys = await KeyManager.fromStorage(p => this.storage(PROFILES_DIR + '/' + p))
|
|
73
|
+
if (this._primaryKey) {
|
|
74
|
+
this.primaryKey = await this._primaryKey
|
|
75
|
+
return this.primaryKey
|
|
36
76
|
}
|
|
77
|
+
this._keyStorage = this.storage(PRIMARY_KEY_FILE_NAME)
|
|
78
|
+
this.primaryKey = await new Promise((resolve, reject) => {
|
|
79
|
+
this._keyStorage.stat((err, st) => {
|
|
80
|
+
if (err && err.code !== 'ENOENT') return reject(err)
|
|
81
|
+
if (err || st.size < 32 || this._overwrite) {
|
|
82
|
+
const key = crypto.randomBytes(32)
|
|
83
|
+
return this._keyStorage.write(0, key, err => {
|
|
84
|
+
if (err) return reject(err)
|
|
85
|
+
return resolve(key)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
this._keyStorage.read(0, 32, (err, key) => {
|
|
89
|
+
if (err) return reject(err)
|
|
90
|
+
return resolve(key)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
return this.primaryKey
|
|
37
95
|
}
|
|
38
96
|
|
|
39
97
|
async _generateKeys (opts) {
|
|
40
|
-
if (opts.
|
|
98
|
+
if (opts._discoveryKey) {
|
|
41
99
|
return {
|
|
42
100
|
keyPair: null,
|
|
43
|
-
|
|
44
|
-
discoveryKey: opts.
|
|
101
|
+
auth: null,
|
|
102
|
+
discoveryKey: opts._discoveryKey
|
|
45
103
|
}
|
|
46
104
|
}
|
|
47
105
|
if (!opts.name) {
|
|
@@ -51,30 +109,39 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
51
109
|
secretKey: opts.secretKey
|
|
52
110
|
},
|
|
53
111
|
sign: opts.sign,
|
|
112
|
+
auth: opts.auth,
|
|
54
113
|
discoveryKey: crypto.discoveryKey(opts.publicKey)
|
|
55
114
|
}
|
|
56
115
|
}
|
|
57
|
-
const { publicKey,
|
|
116
|
+
const { publicKey, auth } = await this.createKeyPair(opts.name)
|
|
58
117
|
return {
|
|
59
118
|
keyPair: {
|
|
60
119
|
publicKey,
|
|
61
120
|
secretKey: null
|
|
62
121
|
},
|
|
63
|
-
|
|
122
|
+
auth,
|
|
64
123
|
discoveryKey: crypto.discoveryKey(publicKey)
|
|
65
124
|
}
|
|
66
125
|
}
|
|
67
126
|
|
|
68
|
-
|
|
69
|
-
|
|
127
|
+
_getPrereadyUserData (core, key) {
|
|
128
|
+
// Need to manually read the header values before the Hypercore is ready, hence the ugliness.
|
|
129
|
+
for (const { key: savedKey, value } of core.core.header.userData) {
|
|
130
|
+
if (key === savedKey) return value
|
|
131
|
+
}
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async _preready (core) {
|
|
136
|
+
const name = this._getPrereadyUserData(core, USERDATA_NAME_KEY)
|
|
70
137
|
if (!name) return
|
|
71
138
|
|
|
72
|
-
const namespace =
|
|
73
|
-
const { publicKey,
|
|
74
|
-
if (!
|
|
139
|
+
const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
|
|
140
|
+
const { publicKey, auth } = await this.createKeyPair(b4a.toString(name), namespace)
|
|
141
|
+
if (!b4a.equals(publicKey, core.key)) throw new Error('Stored core key does not match the provided name')
|
|
75
142
|
|
|
76
|
-
// TODO: Should Hypercore expose a helper for this, or should
|
|
77
|
-
core.
|
|
143
|
+
// TODO: Should Hypercore expose a helper for this, or should preready return keypair/auth?
|
|
144
|
+
core.auth = auth
|
|
78
145
|
core.key = publicKey
|
|
79
146
|
core.writable = true
|
|
80
147
|
}
|
|
@@ -82,20 +149,22 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
82
149
|
async _preload (opts) {
|
|
83
150
|
await this.ready()
|
|
84
151
|
|
|
85
|
-
const { discoveryKey, keyPair,
|
|
86
|
-
const id =
|
|
152
|
+
const { discoveryKey, keyPair, auth } = await this._generateKeys(opts)
|
|
153
|
+
const id = b4a.toString(discoveryKey, 'hex')
|
|
87
154
|
|
|
88
155
|
while (this.cores.has(id)) {
|
|
89
156
|
const existing = this.cores.get(id)
|
|
90
|
-
if (existing) {
|
|
91
|
-
|
|
157
|
+
if (existing.opened && !existing.closing) return { from: existing, keyPair, auth }
|
|
158
|
+
if (existing.closing) {
|
|
92
159
|
await existing.close()
|
|
160
|
+
} else {
|
|
161
|
+
await existing.ready().catch(safetyCatch)
|
|
93
162
|
}
|
|
94
163
|
}
|
|
95
164
|
|
|
96
165
|
const userData = {}
|
|
97
166
|
if (opts.name) {
|
|
98
|
-
userData[USERDATA_NAME_KEY] =
|
|
167
|
+
userData[USERDATA_NAME_KEY] = b4a.from(opts.name)
|
|
99
168
|
userData[USERDATA_NAMESPACE_KEY] = this._namespace
|
|
100
169
|
}
|
|
101
170
|
|
|
@@ -103,98 +172,169 @@ module.exports = class Corestore extends EventEmitter {
|
|
|
103
172
|
|
|
104
173
|
const storageRoot = [CORES_DIR, id.slice(0, 2), id.slice(2, 4), id].join('/')
|
|
105
174
|
const core = new Hypercore(p => this.storage(storageRoot + '/' + p), {
|
|
175
|
+
_preready: this._preready.bind(this),
|
|
106
176
|
autoClose: true,
|
|
107
177
|
encryptionKey: opts.encryptionKey || null,
|
|
108
|
-
keyPair: {
|
|
109
|
-
publicKey: keyPair.publicKey,
|
|
110
|
-
secretKey: null
|
|
111
|
-
},
|
|
112
178
|
userData,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
createIfMissing:
|
|
179
|
+
auth,
|
|
180
|
+
cache: opts.cache,
|
|
181
|
+
createIfMissing: !opts._discoveryKey,
|
|
182
|
+
keyPair: keyPair && keyPair.publicKey
|
|
183
|
+
? {
|
|
184
|
+
publicKey: keyPair.publicKey,
|
|
185
|
+
secretKey: null
|
|
186
|
+
}
|
|
187
|
+
: null
|
|
116
188
|
})
|
|
117
189
|
|
|
118
190
|
this.cores.set(id, core)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
191
|
+
core.ready().then(() => {
|
|
192
|
+
for (const { stream } of this._replicationStreams) {
|
|
193
|
+
const sessions = this._streamSessions.get(stream)
|
|
194
|
+
const session = core.session()
|
|
195
|
+
sessions.push(session)
|
|
196
|
+
core.replicate(stream)
|
|
197
|
+
}
|
|
198
|
+
}, () => {
|
|
199
|
+
this.cores.delete(id)
|
|
200
|
+
})
|
|
122
201
|
core.once('close', () => {
|
|
123
202
|
this.cores.delete(id)
|
|
124
203
|
})
|
|
125
204
|
|
|
126
|
-
return { from: core, keyPair,
|
|
205
|
+
return { from: core, keyPair, auth }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async createKeyPair (name, namespace = this._namespace) {
|
|
209
|
+
if (!this.primaryKey) await this._opening
|
|
210
|
+
|
|
211
|
+
const keyPair = {
|
|
212
|
+
publicKey: b4a.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
|
|
213
|
+
secretKey: b4a.alloc(sodium.crypto_sign_SECRETKEYBYTES),
|
|
214
|
+
auth: {
|
|
215
|
+
sign: (msg) => sign(keyPair, msg),
|
|
216
|
+
verify: (signable, signature) => {
|
|
217
|
+
return crypto.verify(signable, signature, keyPair.publicKey)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const seed = deriveSeed(this.primaryKey, namespace, name)
|
|
223
|
+
sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, seed)
|
|
224
|
+
|
|
225
|
+
return keyPair
|
|
127
226
|
}
|
|
128
227
|
|
|
129
228
|
get (opts = {}) {
|
|
130
229
|
opts = validateGetOptions(opts)
|
|
230
|
+
|
|
231
|
+
if (opts.cache !== false) {
|
|
232
|
+
opts.cache = opts.cache === true || (this.cache && !opts.cache) ? defaultCache() : opts.cache
|
|
233
|
+
}
|
|
234
|
+
|
|
131
235
|
const core = new Hypercore(null, {
|
|
132
236
|
...opts,
|
|
133
237
|
name: null,
|
|
134
238
|
preload: () => this._preload(opts)
|
|
135
239
|
})
|
|
240
|
+
|
|
241
|
+
this._sessions.add(core)
|
|
242
|
+
if (this._findingPeersCount > 0) {
|
|
243
|
+
this._findingPeers.push(core.findingPeers())
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
core.once('close', () => {
|
|
247
|
+
// technically better to also clear _findingPeers if we added it,
|
|
248
|
+
// but the lifecycle for those are pretty short so prob not worth the complexity
|
|
249
|
+
// as _decFindingPeers clear them all.
|
|
250
|
+
this._sessions.delete(core)
|
|
251
|
+
})
|
|
252
|
+
|
|
136
253
|
return core
|
|
137
254
|
}
|
|
138
255
|
|
|
139
|
-
replicate (opts
|
|
140
|
-
const
|
|
256
|
+
replicate (isInitiator, opts) {
|
|
257
|
+
const isExternal = isStream(isInitiator) || !!(opts && opts.stream)
|
|
258
|
+
const stream = Hypercore.createProtocolStream(isInitiator, {
|
|
259
|
+
...opts,
|
|
260
|
+
ondiscoverykey: discoveryKey => {
|
|
261
|
+
const core = this.get({ _discoveryKey: discoveryKey })
|
|
262
|
+
return core.ready().catch(safetyCatch)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const sessions = []
|
|
141
267
|
for (const core of this.cores.values()) {
|
|
268
|
+
if (!core.opened) continue // If the core is not opened, it will be replicated in preload.
|
|
269
|
+
const session = core.session()
|
|
270
|
+
sessions.push(session)
|
|
142
271
|
core.replicate(stream)
|
|
143
272
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
stream.close(discoveryKey)
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
this._replicationStreams.push(stream)
|
|
273
|
+
|
|
274
|
+
const streamRecord = { stream, isExternal }
|
|
275
|
+
this._replicationStreams.push(streamRecord)
|
|
276
|
+
this._streamSessions.set(stream, sessions)
|
|
277
|
+
|
|
153
278
|
stream.once('close', () => {
|
|
154
|
-
this._replicationStreams.splice(this._replicationStreams.indexOf(
|
|
279
|
+
this._replicationStreams.splice(this._replicationStreams.indexOf(streamRecord), 1)
|
|
280
|
+
this._streamSessions.delete(stream)
|
|
281
|
+
Promise.all(sessions.map(s => s.close())).catch(safetyCatch)
|
|
155
282
|
})
|
|
156
283
|
return stream
|
|
157
284
|
}
|
|
158
285
|
|
|
159
286
|
namespace (name) {
|
|
160
|
-
if (!Buffer.isBuffer(name)) name = Buffer.from(name)
|
|
161
287
|
return new Corestore(this.storage, {
|
|
162
|
-
|
|
288
|
+
primaryKey: this._opening.then(() => this.primaryKey),
|
|
289
|
+
namespace: generateNamespace(this._namespace, name),
|
|
290
|
+
cache: this.cache,
|
|
163
291
|
_opening: this._opening,
|
|
164
292
|
_cores: this.cores,
|
|
165
293
|
_streams: this._replicationStreams,
|
|
166
|
-
|
|
294
|
+
_streamSessions: this._streamSessions
|
|
167
295
|
})
|
|
168
296
|
}
|
|
169
297
|
|
|
170
298
|
async _close () {
|
|
171
|
-
if (this._closing) return this._closing
|
|
172
299
|
await this._opening
|
|
300
|
+
if (!b4a.equals(this._namespace, DEFAULT_NAMESPACE)) {
|
|
301
|
+
// namespaces should not release resources on close
|
|
302
|
+
// TODO: Refactor the namespace close logic to actually close sessions with ref counting
|
|
303
|
+
return
|
|
304
|
+
}
|
|
173
305
|
const closePromises = []
|
|
174
306
|
for (const core of this.cores.values()) {
|
|
175
307
|
closePromises.push(core.close())
|
|
176
308
|
}
|
|
177
309
|
await Promise.allSettled(closePromises)
|
|
178
|
-
for (const stream of this._replicationStreams) {
|
|
179
|
-
|
|
310
|
+
for (const { stream, isExternal } of this._replicationStreams) {
|
|
311
|
+
// Only close streams that were created by the Corestore
|
|
312
|
+
if (!isExternal) stream.destroy()
|
|
180
313
|
}
|
|
181
|
-
|
|
314
|
+
if (!this._keyStorage) return
|
|
315
|
+
await new Promise((resolve, reject) => {
|
|
316
|
+
this._keyStorage.close(err => {
|
|
317
|
+
if (err) return reject(err)
|
|
318
|
+
return resolve(null)
|
|
319
|
+
})
|
|
320
|
+
})
|
|
182
321
|
}
|
|
183
322
|
|
|
184
323
|
close () {
|
|
185
324
|
if (this._closing) return this._closing
|
|
186
325
|
this._closing = this._close()
|
|
187
|
-
this._closing.catch(
|
|
326
|
+
this._closing.catch(safetyCatch)
|
|
188
327
|
return this._closing
|
|
189
328
|
}
|
|
329
|
+
}
|
|
190
330
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
331
|
+
function sign (keyPair, message) {
|
|
332
|
+
if (!keyPair.secretKey) throw new Error('Invalid key pair')
|
|
333
|
+
return crypto.sign(message, keyPair.secretKey)
|
|
194
334
|
}
|
|
195
335
|
|
|
196
336
|
function validateGetOptions (opts) {
|
|
197
|
-
if (
|
|
337
|
+
if (b4a.isBuffer(opts)) return { key: opts, publicKey: opts }
|
|
198
338
|
if (opts.key) {
|
|
199
339
|
opts.publicKey = opts.key
|
|
200
340
|
}
|
|
@@ -204,23 +344,30 @@ function validateGetOptions (opts) {
|
|
|
204
344
|
}
|
|
205
345
|
if (opts.name && typeof opts.name !== 'string') throw new Error('name option must be a String')
|
|
206
346
|
if (opts.name && opts.secretKey) throw new Error('Cannot provide both a name and a secret key')
|
|
207
|
-
if (opts.publicKey && !
|
|
208
|
-
if (opts.secretKey && !
|
|
209
|
-
if (opts.
|
|
210
|
-
if (!opts.name && !opts.publicKey) throw new Error('Must provide either a name or a publicKey')
|
|
347
|
+
if (opts.publicKey && !b4a.isBuffer(opts.publicKey)) throw new Error('publicKey option must be a Buffer or Uint8Array')
|
|
348
|
+
if (opts.secretKey && !b4a.isBuffer(opts.secretKey)) throw new Error('secretKey option must be a Buffer or Uint8Array')
|
|
349
|
+
if (!opts._discoveryKey && (!opts.name && !opts.publicKey)) throw new Error('Must provide either a name or a publicKey')
|
|
211
350
|
return opts
|
|
212
351
|
}
|
|
213
352
|
|
|
214
|
-
function generateNamespace (
|
|
215
|
-
if (!
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
353
|
+
function generateNamespace (namespace, name) {
|
|
354
|
+
if (!b4a.isBuffer(name)) name = b4a.from(name)
|
|
355
|
+
const out = b4a.allocUnsafe(32)
|
|
356
|
+
sodium.crypto_generichash_batch(out, [namespace, name])
|
|
357
|
+
return out
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function deriveSeed (primaryKey, namespace, name) {
|
|
361
|
+
if (!b4a.isBuffer(name)) name = b4a.from(name)
|
|
362
|
+
const out = b4a.alloc(32)
|
|
363
|
+
sodium.crypto_generichash_batch(out, [NS, namespace, name], primaryKey)
|
|
219
364
|
return out
|
|
220
365
|
}
|
|
221
366
|
|
|
367
|
+
function defaultCache () {
|
|
368
|
+
return new Xache({ maxSize: 65536, maxAge: 0 })
|
|
369
|
+
}
|
|
370
|
+
|
|
222
371
|
function isStream (s) {
|
|
223
372
|
return typeof s === 'object' && s && typeof s.pipe === 'function'
|
|
224
373
|
}
|
|
225
|
-
|
|
226
|
-
function noop () {}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "corestore",
|
|
3
|
-
"version": "6.0.0
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "A Hypercore factory that simplifies managing collections of cores.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "standard &&
|
|
7
|
+
"test": "standard && brittle test/*.js"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -20,17 +20,16 @@
|
|
|
20
20
|
},
|
|
21
21
|
"homepage": "https://github.com/hypercore-protocol/corestore#readme",
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"
|
|
24
|
-
"random-access-memory": "^
|
|
25
|
-
"standardx": "^7.0.0"
|
|
26
|
-
"tape": "^5.3.1",
|
|
27
|
-
"tmp-promise": "^3.0.2"
|
|
23
|
+
"brittle": "^3.0.0",
|
|
24
|
+
"random-access-memory": "^4.0.0",
|
|
25
|
+
"standardx": "^7.0.0"
|
|
28
26
|
},
|
|
29
27
|
"dependencies": {
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"hypercore": "
|
|
33
|
-
"
|
|
34
|
-
"sodium-universal": "^3.0.4"
|
|
28
|
+
"b4a": "^1.3.1",
|
|
29
|
+
"hypercore": "v10.0.0",
|
|
30
|
+
"hypercore-crypto": "^3.2.1",
|
|
31
|
+
"safety-catch": "^1.0.1",
|
|
32
|
+
"sodium-universal": "^3.0.4",
|
|
33
|
+
"xache": "^1.1.0"
|
|
35
34
|
}
|
|
36
35
|
}
|
package/test/all.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
const test = require('
|
|
1
|
+
const test = 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
|
|
|
@@ -13,15 +16,13 @@ test('basic get with caching', async function (t) {
|
|
|
13
16
|
|
|
14
17
|
await Promise.all([core1a.ready(), core1b.ready(), core2.ready()])
|
|
15
18
|
|
|
16
|
-
t.
|
|
17
|
-
t.
|
|
19
|
+
t.alike(core1a.key, core1b.key)
|
|
20
|
+
t.unlike(core1a.key, core2.key)
|
|
18
21
|
|
|
19
|
-
t.
|
|
20
|
-
t.
|
|
22
|
+
t.ok(core1a.writable)
|
|
23
|
+
t.ok(core1b.writable)
|
|
21
24
|
|
|
22
|
-
t.
|
|
23
|
-
|
|
24
|
-
t.end()
|
|
25
|
+
t.is(store.cores.size, 2)
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
test('basic get with custom keypair', async function (t) {
|
|
@@ -33,12 +34,10 @@ test('basic get with custom keypair', async function (t) {
|
|
|
33
34
|
const core2 = store.get(kp2)
|
|
34
35
|
await Promise.all([core1.ready(), core2.ready()])
|
|
35
36
|
|
|
36
|
-
t.
|
|
37
|
-
t.
|
|
38
|
-
t.
|
|
39
|
-
t.
|
|
40
|
-
|
|
41
|
-
t.end()
|
|
37
|
+
t.alike(core1.key, kp1.publicKey)
|
|
38
|
+
t.alike(core2.key, kp2.publicKey)
|
|
39
|
+
t.ok(core1.writable)
|
|
40
|
+
t.ok(core2.writable)
|
|
42
41
|
})
|
|
43
42
|
|
|
44
43
|
test('basic namespaces', async function (t) {
|
|
@@ -52,12 +51,12 @@ test('basic namespaces', async function (t) {
|
|
|
52
51
|
const core3 = ns3.get({ name: 'main' })
|
|
53
52
|
await Promise.all([core1.ready(), core2.ready(), core3.ready()])
|
|
54
53
|
|
|
55
|
-
t.
|
|
56
|
-
t.
|
|
57
|
-
t.
|
|
58
|
-
t.
|
|
59
|
-
t.
|
|
60
|
-
t.
|
|
54
|
+
t.absent(core1.key.equals(core2.key))
|
|
55
|
+
t.ok(core1.key.equals(core3.key))
|
|
56
|
+
t.ok(core1.writable)
|
|
57
|
+
t.ok(core2.writable)
|
|
58
|
+
t.ok(core3.writable)
|
|
59
|
+
t.is(store.cores.size, 2)
|
|
61
60
|
|
|
62
61
|
t.end()
|
|
63
62
|
})
|
|
@@ -77,10 +76,46 @@ test('basic replication', async function (t) {
|
|
|
77
76
|
const s = store1.replicate(true)
|
|
78
77
|
s.pipe(store2.replicate(false)).pipe(s)
|
|
79
78
|
|
|
80
|
-
t.
|
|
81
|
-
t.
|
|
79
|
+
t.alike(await core3.get(0), Buffer.from('hello'))
|
|
80
|
+
t.alike(await core4.get(0), Buffer.from('world'))
|
|
81
|
+
})
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
test('replicating cores created after replication begins', async function (t) {
|
|
84
|
+
const store1 = new Corestore(ram)
|
|
85
|
+
const store2 = new Corestore(ram)
|
|
86
|
+
|
|
87
|
+
const s = store1.replicate(true, { live: true })
|
|
88
|
+
s.pipe(store2.replicate(false, { live: true })).pipe(s)
|
|
89
|
+
|
|
90
|
+
const core1 = store1.get({ name: 'core-1' })
|
|
91
|
+
const core2 = store1.get({ name: 'core-2' })
|
|
92
|
+
await core1.append('hello')
|
|
93
|
+
await core2.append('world')
|
|
94
|
+
|
|
95
|
+
const core3 = store2.get({ key: core1.key })
|
|
96
|
+
const core4 = store2.get({ key: core2.key })
|
|
97
|
+
|
|
98
|
+
t.alike(await core3.get(0), Buffer.from('hello'))
|
|
99
|
+
t.alike(await core4.get(0), Buffer.from('world'))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('replicating cores using discovery key hook', async function (t) {
|
|
103
|
+
const dir = tmpdir()
|
|
104
|
+
let store1 = new Corestore(dir)
|
|
105
|
+
const store2 = new Corestore(ram)
|
|
106
|
+
|
|
107
|
+
const core = store1.get({ name: 'main' })
|
|
108
|
+
await core.append('hello')
|
|
109
|
+
const key = core.key
|
|
110
|
+
|
|
111
|
+
await store1.close()
|
|
112
|
+
store1 = new Corestore(dir)
|
|
113
|
+
|
|
114
|
+
const s = store1.replicate(true, { live: true })
|
|
115
|
+
s.pipe(store2.replicate(false, { live: true })).pipe(s)
|
|
116
|
+
|
|
117
|
+
const core2 = store2.get(key)
|
|
118
|
+
t.alike(await core2.get(0), Buffer.from('hello'))
|
|
84
119
|
})
|
|
85
120
|
|
|
86
121
|
test('nested namespaces', async function (t) {
|
|
@@ -92,65 +127,208 @@ test('nested namespaces', async function (t) {
|
|
|
92
127
|
const core2 = ns1b.get({ name: 'main' })
|
|
93
128
|
await Promise.all([core1.ready(), core2.ready()])
|
|
94
129
|
|
|
95
|
-
t.
|
|
96
|
-
t.
|
|
97
|
-
t.
|
|
98
|
-
t.
|
|
99
|
-
|
|
100
|
-
t.end()
|
|
130
|
+
t.not(core1.key.equals(core2.key))
|
|
131
|
+
t.ok(core1.writable)
|
|
132
|
+
t.ok(core2.writable)
|
|
133
|
+
t.is(store.cores.size, 2)
|
|
101
134
|
})
|
|
102
135
|
|
|
103
136
|
test('core uncached when all sessions close', async function (t) {
|
|
104
137
|
const store = new Corestore(ram)
|
|
105
138
|
const core1 = store.get({ name: 'main' })
|
|
106
139
|
await core1.ready()
|
|
107
|
-
t.
|
|
140
|
+
t.is(store.cores.size, 1)
|
|
108
141
|
await core1.close()
|
|
109
|
-
t.
|
|
110
|
-
t.end()
|
|
142
|
+
t.is(store.cores.size, 0)
|
|
111
143
|
})
|
|
112
144
|
|
|
113
145
|
test('writable core loaded from name userData', async function (t) {
|
|
114
|
-
const dir =
|
|
146
|
+
const dir = tmpdir()
|
|
115
147
|
|
|
116
|
-
let store = new Corestore(dir
|
|
148
|
+
let store = new Corestore(dir)
|
|
117
149
|
let core = store.get({ name: 'main' })
|
|
118
150
|
await core.ready()
|
|
119
151
|
const key = core.key
|
|
120
152
|
|
|
121
|
-
t.
|
|
153
|
+
t.ok(core.writable)
|
|
122
154
|
await core.append('hello')
|
|
123
|
-
t.
|
|
155
|
+
t.is(core.length, 1)
|
|
124
156
|
|
|
125
157
|
await store.close()
|
|
126
|
-
store = new Corestore(dir
|
|
158
|
+
store = new Corestore(dir)
|
|
127
159
|
core = store.get(key)
|
|
128
160
|
await core.ready()
|
|
129
161
|
|
|
130
|
-
t.
|
|
162
|
+
t.ok(core.writable)
|
|
131
163
|
await core.append('world')
|
|
132
|
-
t.
|
|
133
|
-
t.
|
|
134
|
-
t.
|
|
164
|
+
t.is(core.length, 2)
|
|
165
|
+
t.alike(await core.get(0), Buffer.from('hello'))
|
|
166
|
+
t.alike(await core.get(1), Buffer.from('world'))
|
|
167
|
+
})
|
|
135
168
|
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
test('writable core loaded from name and namespace userData', async function (t) {
|
|
170
|
+
const dir = tmpdir()
|
|
171
|
+
|
|
172
|
+
let store = new Corestore(dir)
|
|
173
|
+
let core = store.namespace('ns1').get({ name: 'main' })
|
|
174
|
+
await core.ready()
|
|
175
|
+
const key = core.key
|
|
176
|
+
|
|
177
|
+
t.ok(core.writable)
|
|
178
|
+
await core.append('hello')
|
|
179
|
+
t.is(core.length, 1)
|
|
180
|
+
|
|
181
|
+
await store.close()
|
|
182
|
+
store = new Corestore(dir)
|
|
183
|
+
core = store.get(key)
|
|
184
|
+
await core.ready()
|
|
185
|
+
|
|
186
|
+
t.ok(core.writable)
|
|
187
|
+
await core.append('world')
|
|
188
|
+
t.is(core.length, 2)
|
|
189
|
+
t.alike(await core.get(0), Buffer.from('hello'))
|
|
190
|
+
t.alike(await core.get(1), Buffer.from('world'))
|
|
138
191
|
})
|
|
139
192
|
|
|
140
193
|
test('storage locking', async function (t) {
|
|
141
|
-
const dir =
|
|
194
|
+
const dir = tmpdir()
|
|
142
195
|
|
|
143
|
-
const store1 = new Corestore(dir
|
|
196
|
+
const store1 = new Corestore(dir)
|
|
144
197
|
await store1.ready()
|
|
145
198
|
|
|
146
|
-
const store2 = new Corestore(dir
|
|
199
|
+
const store2 = new Corestore(dir)
|
|
147
200
|
try {
|
|
148
201
|
await store2.ready()
|
|
149
202
|
t.fail('dir should have been locked')
|
|
150
203
|
} catch {
|
|
151
204
|
t.pass('dir was locked')
|
|
152
205
|
}
|
|
206
|
+
})
|
|
153
207
|
|
|
154
|
-
|
|
155
|
-
|
|
208
|
+
test('closing a namespace does not close cores', async function (t) {
|
|
209
|
+
const store = new Corestore(ram)
|
|
210
|
+
const ns1 = store.namespace('ns1')
|
|
211
|
+
const core1 = ns1.get({ name: 'core-1' })
|
|
212
|
+
const core2 = ns1.get({ name: 'core-2' })
|
|
213
|
+
await Promise.all([core1.ready(), core2.ready()])
|
|
214
|
+
|
|
215
|
+
await ns1.close()
|
|
216
|
+
|
|
217
|
+
t.is(store.cores.size, 2)
|
|
218
|
+
t.not(core1.closed)
|
|
219
|
+
t.not(core1.closed)
|
|
220
|
+
|
|
221
|
+
await store.close()
|
|
222
|
+
|
|
223
|
+
t.is(store.cores.size, 0)
|
|
224
|
+
t.ok(core1.closed)
|
|
225
|
+
t.ok(core2.closed)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('findingPeers', async function (t) {
|
|
229
|
+
t.plan(6)
|
|
230
|
+
|
|
231
|
+
const store = new Corestore(ram)
|
|
232
|
+
|
|
233
|
+
const ns1 = store.namespace('ns1')
|
|
234
|
+
const ns2 = store.namespace('ns2')
|
|
235
|
+
|
|
236
|
+
const a = ns1.get(Buffer.alloc(32).fill('a'))
|
|
237
|
+
const b = ns2.get(Buffer.alloc(32).fill('b'))
|
|
238
|
+
|
|
239
|
+
const done = ns1.findingPeers()
|
|
240
|
+
|
|
241
|
+
let aUpdated = false
|
|
242
|
+
let bUpdated = false
|
|
243
|
+
let cUpdated = false
|
|
244
|
+
|
|
245
|
+
const c = ns1.get(Buffer.alloc(32).fill('c'))
|
|
246
|
+
|
|
247
|
+
a.update().then(function (bool) {
|
|
248
|
+
aUpdated = true
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
b.update().then(function (bool) {
|
|
252
|
+
bUpdated = true
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
c.update().then(function (bool) {
|
|
256
|
+
cUpdated = true
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
260
|
+
|
|
261
|
+
t.is(aUpdated, false)
|
|
262
|
+
t.is(bUpdated, true)
|
|
263
|
+
t.is(cUpdated, false)
|
|
264
|
+
|
|
265
|
+
done()
|
|
266
|
+
|
|
267
|
+
await new Promise(resolve => setImmediate(resolve))
|
|
268
|
+
|
|
269
|
+
t.is(aUpdated, true)
|
|
270
|
+
t.is(bUpdated, true)
|
|
271
|
+
t.is(cUpdated, true)
|
|
156
272
|
})
|
|
273
|
+
|
|
274
|
+
test('different primary keys yield different keypairs', async function (t) {
|
|
275
|
+
const pk1 = randomBytes(32)
|
|
276
|
+
const pk2 = randomBytes(32)
|
|
277
|
+
t.unlike(pk1, pk2)
|
|
278
|
+
|
|
279
|
+
const store1 = new Corestore(ram, { primaryKey: pk1 })
|
|
280
|
+
const store2 = new Corestore(ram, { primaryKey: pk2 })
|
|
281
|
+
|
|
282
|
+
const kp1 = await store1.createKeyPair('hello')
|
|
283
|
+
const kp2 = await store2.createKeyPair('hello')
|
|
284
|
+
|
|
285
|
+
t.unlike(kp1.publicKey, kp2.publicKey)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('keypair auth sign', async function (t) {
|
|
289
|
+
const store = new Corestore(ram)
|
|
290
|
+
const keyPair = await store.createKeyPair('foo')
|
|
291
|
+
const message = b4a.from('hello world')
|
|
292
|
+
|
|
293
|
+
const sig = keyPair.auth.sign(message)
|
|
294
|
+
|
|
295
|
+
t.is(sig.length, 64)
|
|
296
|
+
t.ok(crypto.verify(message, sig, keyPair.publicKey))
|
|
297
|
+
t.absent(crypto.verify(message, b4a.alloc(64), keyPair.publicKey))
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
test('keypair auth verify', async function (t) {
|
|
301
|
+
const store = new Corestore(ram)
|
|
302
|
+
const keyPair = await store.createKeyPair('foo')
|
|
303
|
+
const message = b4a.from('hello world')
|
|
304
|
+
|
|
305
|
+
const sig = crypto.sign(message, keyPair.secretKey)
|
|
306
|
+
|
|
307
|
+
t.is(sig.length, 64)
|
|
308
|
+
t.ok(keyPair.auth.verify(message, sig))
|
|
309
|
+
t.absent(keyPair.auth.verify(message, b4a.alloc(64)))
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
test('core caching after reopen regression', async function (t) {
|
|
313
|
+
const store = new Corestore(ram)
|
|
314
|
+
const core = store.get({ name: 'test-core' })
|
|
315
|
+
await core.ready()
|
|
316
|
+
|
|
317
|
+
core.close()
|
|
318
|
+
await core.opening
|
|
319
|
+
|
|
320
|
+
const core2 = store.get({ name: 'test-core' })
|
|
321
|
+
await core2.ready()
|
|
322
|
+
|
|
323
|
+
t.pass('did not infinite loop')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
function tmpdir () {
|
|
327
|
+
return path.join(os.tmpdir(), 'corestore-' + Math.random().toString(16).slice(2))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function randomBytes (n) {
|
|
331
|
+
const buf = b4a.allocUnsafe(n)
|
|
332
|
+
sodium.randombytes_buf(buf)
|
|
333
|
+
return buf
|
|
334
|
+
}
|
package/test/cache.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const test = require('brittle')
|
|
2
|
+
const RAM = require('random-access-memory')
|
|
3
|
+
|
|
4
|
+
const Corestore = require('..')
|
|
5
|
+
|
|
6
|
+
test('core cache', async function (t) {
|
|
7
|
+
const store = new Corestore(RAM, { cache: true })
|
|
8
|
+
|
|
9
|
+
const core = store.get({ name: 'core' })
|
|
10
|
+
await core.append(['a', 'b', 'c'])
|
|
11
|
+
|
|
12
|
+
const p = core.get(0)
|
|
13
|
+
const q = core.get(0)
|
|
14
|
+
|
|
15
|
+
t.is(await p, await q)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('clear cache on truncate', async function (t) {
|
|
19
|
+
const store = new Corestore(RAM, { cache: true })
|
|
20
|
+
|
|
21
|
+
const core = store.get({ name: 'core' })
|
|
22
|
+
await core.append(['a', 'b', 'c'])
|
|
23
|
+
|
|
24
|
+
const p = core.get(0)
|
|
25
|
+
|
|
26
|
+
await core.truncate(0)
|
|
27
|
+
await core.append('d')
|
|
28
|
+
|
|
29
|
+
const q = core.get(0)
|
|
30
|
+
|
|
31
|
+
t.alike(await p, Buffer.from('a'))
|
|
32
|
+
t.alike(await q, Buffer.from('d'))
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('core cache on namespace', async function (t) {
|
|
36
|
+
const store = new Corestore(RAM, { cache: true })
|
|
37
|
+
const ns1 = store.namespace('test-namespace-1')
|
|
38
|
+
|
|
39
|
+
const c1 = store.get({ name: 'test-core' })
|
|
40
|
+
const c2 = ns1.get({ name: 'test-core' })
|
|
41
|
+
|
|
42
|
+
await Promise.all([c1.ready(), c2.ready()])
|
|
43
|
+
|
|
44
|
+
t.ok(c1.cache)
|
|
45
|
+
t.ok(c2.cache)
|
|
46
|
+
})
|
package/lib/keys.js
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// TODO: Extract this into a standalone module
|
|
2
|
-
|
|
3
|
-
const sodium = require('sodium-universal')
|
|
4
|
-
const blake2b = require('blake2b-universal')
|
|
5
|
-
|
|
6
|
-
const DEFAULT_TOKEN = Buffer.alloc(0)
|
|
7
|
-
const NAMESPACE = Buffer.from('@hyperspace/key-manager')
|
|
8
|
-
|
|
9
|
-
module.exports = class KeyManager {
|
|
10
|
-
constructor (storage, profile, opts = {}) {
|
|
11
|
-
this.storage = storage
|
|
12
|
-
this.profile = profile
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
_sign (keyPair, message) {
|
|
16
|
-
if (!keyPair._secretKey) throw new Error('Invalid key pair')
|
|
17
|
-
const signature = Buffer.allocUnsafe(sodium.crypto_sign_BYTES)
|
|
18
|
-
sodium.crypto_sign_detached(signature, message, keyPair._secretKey)
|
|
19
|
-
return signature
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
createSecret (name, token) {
|
|
23
|
-
return deriveSeed(this.profile, token, name)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
createHypercoreKeyPair (name, token) {
|
|
27
|
-
const keyPair = {
|
|
28
|
-
publicKey: Buffer.allocUnsafe(sodium.crypto_sign_PUBLICKEYBYTES),
|
|
29
|
-
_secretKey: Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES),
|
|
30
|
-
sign: (msg) => this._sign(keyPair, msg)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair._secretKey, this.createSecret(name, token))
|
|
34
|
-
|
|
35
|
-
return keyPair
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
createNetworkIdentity (name, token) {
|
|
39
|
-
const keyPair = {
|
|
40
|
-
publicKey: Buffer.alloc(32),
|
|
41
|
-
secretKey: Buffer.alloc(64)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, this.createSecret(name, token))
|
|
45
|
-
|
|
46
|
-
return keyPair
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
close () {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
this.storage.close(err => {
|
|
52
|
-
if (err) return reject(err)
|
|
53
|
-
return resolve()
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
static createToken () {
|
|
59
|
-
return randomBytes(32)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
static async fromStorage (storage, opts = {}) {
|
|
63
|
-
const profileStorage = storage(opts.name || 'default')
|
|
64
|
-
|
|
65
|
-
const profile = await new Promise((resolve, reject) => {
|
|
66
|
-
profileStorage.stat((err, st) => {
|
|
67
|
-
if (err && err.code !== 'ENOENT') return reject(err)
|
|
68
|
-
if (err || st.size < 32 || opts.overwrite) {
|
|
69
|
-
const key = randomBytes(32)
|
|
70
|
-
return profileStorage.write(0, key, err => {
|
|
71
|
-
if (err) return reject(err)
|
|
72
|
-
return resolve(key)
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
profileStorage.read(0, 32, (err, key) => {
|
|
76
|
-
if (err) return reject(err)
|
|
77
|
-
return resolve(key)
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
return new this(profileStorage, profile, opts)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function deriveSeed (profile, token, name, output) {
|
|
87
|
-
if (token && token.length < 32) throw new Error('Token must be a Buffer with length >= 32')
|
|
88
|
-
if (!name || typeof name !== 'string') throw new Error('name must be a String')
|
|
89
|
-
if (!output) output = Buffer.alloc(32)
|
|
90
|
-
|
|
91
|
-
blake2b.batch(output, [
|
|
92
|
-
NAMESPACE,
|
|
93
|
-
token || DEFAULT_TOKEN,
|
|
94
|
-
Buffer.from(Buffer.byteLength(name, 'ascii') + '\n' + name, 'ascii')
|
|
95
|
-
], profile)
|
|
96
|
-
|
|
97
|
-
return output
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function randomBytes (n) {
|
|
101
|
-
const buf = Buffer.allocUnsafe(n)
|
|
102
|
-
sodium.randombytes_buf(buf)
|
|
103
|
-
return buf
|
|
104
|
-
}
|
package/test/keys.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
const p = require('path')
|
|
2
|
-
const fs = require('fs')
|
|
3
|
-
|
|
4
|
-
const test = require('tape')
|
|
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.same(kp1.publicKey.length, 32)
|
|
17
|
-
t.same(kp2.publicKey.length, 32)
|
|
18
|
-
t.notSame(kp1.publicKey, kp2.publicKey)
|
|
19
|
-
|
|
20
|
-
t.end()
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('distinct tokens create distinct hypercore keypairs', async t => {
|
|
24
|
-
const keys = await KeyManager.fromStorage(ram)
|
|
25
|
-
const token1 = KeyManager.createToken()
|
|
26
|
-
const token2 = KeyManager.createToken()
|
|
27
|
-
|
|
28
|
-
const kp1 = await keys.createHypercoreKeyPair('core1', token1)
|
|
29
|
-
const kp2 = await keys.createHypercoreKeyPair('core1', token2)
|
|
30
|
-
|
|
31
|
-
t.notSame(kp1.publicKey, kp2.publicKey)
|
|
32
|
-
|
|
33
|
-
t.end()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('short user-provided token will throw', async t => {
|
|
37
|
-
const keys = await KeyManager.fromStorage(ram)
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
await keys.createHypercoreKeyPair('core1', Buffer.from('hello'))
|
|
41
|
-
t.fail('did not throw')
|
|
42
|
-
} catch {
|
|
43
|
-
t.pass('threw correctly')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
t.end()
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('persistent storage regenerates keys correctly', async t => {
|
|
50
|
-
const testPath = p.resolve(__dirname, 'test-data')
|
|
51
|
-
|
|
52
|
-
const keys1 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
|
|
53
|
-
const kp1 = await keys1.createHypercoreKeyPair('core1')
|
|
54
|
-
|
|
55
|
-
const keys2 = await KeyManager.fromStorage((name) => raf(testPath, { directory: testPath }))
|
|
56
|
-
const kp2 = await keys2.createHypercoreKeyPair('core1')
|
|
57
|
-
|
|
58
|
-
t.same(kp1.publicKey, kp2.publicKey)
|
|
59
|
-
|
|
60
|
-
await fs.promises.rm(testPath, { recursive: true })
|
|
61
|
-
t.end()
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('different master keys -> different keys', async t => {
|
|
65
|
-
const keys1 = await KeyManager.fromStorage(ram)
|
|
66
|
-
const keys2 = await KeyManager.fromStorage(ram)
|
|
67
|
-
|
|
68
|
-
const kp1 = await keys1.createHypercoreKeyPair('core1')
|
|
69
|
-
const kp2 = await keys2.createHypercoreKeyPair('core1')
|
|
70
|
-
|
|
71
|
-
t.notSame(kp1.publicKey, kp2.publicKey)
|
|
72
|
-
|
|
73
|
-
t.end()
|
|
74
|
-
})
|