corestore 6.0.1-alpha.14 → 6.0.1-alpha.17

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 CHANGED
@@ -5,31 +5,36 @@ const sodium = require('sodium-universal')
5
5
  const Hypercore = require('hypercore')
6
6
  const b4a = require('b4a')
7
7
 
8
- const KeyManager = require('./lib/keys')
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 PROFILES_DIR = 'profiles'
12
- const USERDATA_NAME_KEY = '@corestore/name'
13
- const USERDATA_NAMESPACE_KEY = '@corestore/namespace'
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: PROFILES_DIR + '/default' })
21
-
20
+ this.storage = Hypercore.defaultStorage(storage, { lock: PRIMARY_KEY_FILE_NAME })
22
21
  this.cores = opts._cores || new Map()
23
- this.keys = opts.keys
22
+ this.primaryKey = null
24
23
 
25
- this._namespace = opts._namespace || DEFAULT_NAMESPACE
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()
28
31
  this._sessions = new Set() // sessions for THIS namespace
29
32
 
30
33
  this._findingPeersCount = 0
31
34
  this._findingPeers = []
32
35
 
36
+ if (this._namespace.byteLength !== 32) throw new Error('Namespace must be a 32-byte Buffer or Uint8Array')
37
+
33
38
  this._opening = opts._opening ? opts._opening.then(() => this._open()) : this._open()
34
39
  this._opening.catch(safetyCatch)
35
40
  this.ready = () => this._opening
@@ -63,11 +68,28 @@ module.exports = class Corestore extends EventEmitter {
63
68
  }
64
69
 
65
70
  async _open () {
66
- if (this.keys) {
67
- this.keys = await this.keys // opts.keys can be a Promise that resolves to a KeyManager
68
- } else {
69
- 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
70
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
71
93
  }
72
94
 
73
95
  async _generateKeys (opts) {
@@ -89,7 +111,7 @@ module.exports = class Corestore extends EventEmitter {
89
111
  discoveryKey: crypto.discoveryKey(opts.publicKey)
90
112
  }
91
113
  }
92
- const { publicKey, auth } = await this.keys.createHypercoreKeyPair(opts.name, this._namespace)
114
+ const { publicKey, auth } = await this.createKeyPair(opts.name)
93
115
  return {
94
116
  keyPair: {
95
117
  publicKey,
@@ -101,6 +123,7 @@ module.exports = class Corestore extends EventEmitter {
101
123
  }
102
124
 
103
125
  _getPrereadyUserData (core, key) {
126
+ // Need to manually read the header values before the Hypercore is ready, hence the ugliness.
104
127
  for (const { key: savedKey, value } of core.core.header.userData) {
105
128
  if (key === savedKey) return value
106
129
  }
@@ -112,7 +135,7 @@ module.exports = class Corestore extends EventEmitter {
112
135
  if (!name) return
113
136
 
114
137
  const namespace = this._getPrereadyUserData(core, USERDATA_NAMESPACE_KEY)
115
- const { publicKey, auth } = await this.keys.createHypercoreKeyPair(b4a.toString(name), namespace)
138
+ const { publicKey, auth } = await this.createKeyPair(b4a.toString(name), namespace)
116
139
  if (!b4a.equals(publicKey, core.key)) throw new Error('Stored core key does not match the provided name')
117
140
 
118
141
  // TODO: Should Hypercore expose a helper for this, or should preready return keypair/auth?
@@ -179,6 +202,26 @@ module.exports = class Corestore extends EventEmitter {
179
202
  return { from: core, keyPair, auth }
180
203
  }
181
204
 
205
+ async createKeyPair (name, namespace = this._namespace) {
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 crypto.verify(signable, signature, keyPair.publicKey)
215
+ }
216
+ }
217
+ }
218
+
219
+ const seed = deriveSeed(this.primaryKey, namespace, name)
220
+ sodium.crypto_sign_seed_keypair(keyPair.publicKey, keyPair.secretKey, seed)
221
+
222
+ return keyPair
223
+ }
224
+
182
225
  get (opts = {}) {
183
226
  opts = validateGetOptions(opts)
184
227
  const core = new Hypercore(null, {
@@ -233,20 +276,23 @@ module.exports = class Corestore extends EventEmitter {
233
276
  }
234
277
 
235
278
  namespace (name) {
236
- if (!b4a.isBuffer(name)) name = b4a.from(name)
237
279
  return new Corestore(this.storage, {
238
- _namespace: generateNamespace(this._namespace, name),
280
+ primaryKey: this._opening.then(() => this.primaryKey),
281
+ namespace: generateNamespace(this._namespace, name),
239
282
  _opening: this._opening,
240
283
  _cores: this.cores,
241
284
  _streams: this._replicationStreams,
242
- _streamSessions: this._streamSessions,
243
- keys: this._opening.then(() => this.keys)
285
+ _streamSessions: this._streamSessions
244
286
  })
245
287
  }
246
288
 
247
289
  async _close () {
248
290
  await this._opening
249
- if (!b4a.equals(this._namespace, DEFAULT_NAMESPACE)) return // namespaces should not release resources on close
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
+ }
250
296
  const closePromises = []
251
297
  for (const core of this.cores.values()) {
252
298
  closePromises.push(core.close())
@@ -256,7 +302,13 @@ module.exports = class Corestore extends EventEmitter {
256
302
  // Only close streams that were created by the Corestore
257
303
  if (!isExternal) stream.destroy()
258
304
  }
259
- await this.keys.close()
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
+ })
260
312
  }
261
313
 
262
314
  close () {
@@ -265,10 +317,11 @@ module.exports = class Corestore extends EventEmitter {
265
317
  this._closing.catch(safetyCatch)
266
318
  return this._closing
267
319
  }
320
+ }
268
321
 
269
- static createToken () {
270
- return KeyManager.createToken()
271
- }
322
+ function sign (keyPair, message) {
323
+ if (!keyPair.secretKey) throw new Error('Invalid key pair')
324
+ return crypto.sign(message, keyPair.secretKey)
272
325
  }
273
326
 
274
327
  function validateGetOptions (opts) {
@@ -288,11 +341,17 @@ function validateGetOptions (opts) {
288
341
  return opts
289
342
  }
290
343
 
291
- function generateNamespace (first, second) {
292
- if (!b4a.isBuffer(first)) first = b4a.from(first)
293
- if (second && !b4a.isBuffer(second)) second = b4a.from(second)
344
+ function generateNamespace (namespace, name) {
345
+ if (!b4a.isBuffer(name)) name = b4a.from(name)
294
346
  const out = b4a.allocUnsafe(32)
295
- sodium.crypto_generichash(out, second ? b4a.concat([first, second]) : first)
347
+ sodium.crypto_generichash_batch(out, [namespace, name])
348
+ return out
349
+ }
350
+
351
+ function deriveSeed (primaryKey, namespace, name) {
352
+ if (!b4a.isBuffer(name)) name = b4a.from(name)
353
+ const out = b4a.alloc(32)
354
+ sodium.crypto_generichash_batch(out, [NS, namespace, name], primaryKey)
296
355
  return out
297
356
  }
298
357
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "corestore",
3
- "version": "6.0.1-alpha.14",
3
+ "version": "6.0.1-alpha.17",
4
4
  "description": "A Hypercore factory that simplifies managing collections of cores.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -20,15 +20,13 @@
20
20
  },
21
21
  "homepage": "https://github.com/hypercore-protocol/corestore#readme",
22
22
  "devDependencies": {
23
- "brittle": "^1.6.0",
23
+ "brittle": "^2.2.10",
24
24
  "random-access-file": "^2.2.0",
25
25
  "random-access-memory": "^4.0.0",
26
26
  "standardx": "^7.0.0"
27
27
  },
28
28
  "dependencies": {
29
29
  "b4a": "^1.3.1",
30
- "blake2b-universal": "^1.0.1",
31
- "derive-key": "^1.0.1",
32
30
  "hypercore": "next",
33
31
  "hypercore-crypto": "^3.2.1",
34
32
  "safety-catch": "^1.0.1",
package/test/all.js CHANGED
@@ -3,6 +3,8 @@ const crypto = require('hypercore-crypto')
3
3
  const ram = require('random-access-memory')
4
4
  const os = require('os')
5
5
  const path = require('path')
6
+ const b4a = require('b4a')
7
+ const sodium = require('sodium-universal')
6
8
 
7
9
  const Corestore = require('..')
8
10
 
@@ -166,6 +168,30 @@ test('writable core loaded from name userData', async function (t) {
166
168
  t.alike(await core.get(1), Buffer.from('world'))
167
169
  })
168
170
 
171
+ test('writable core loaded from name and namespace userData', async function (t) {
172
+ const dir = tmpdir()
173
+
174
+ let store = new Corestore(dir)
175
+ let core = store.namespace('ns1').get({ name: 'main' })
176
+ await core.ready()
177
+ const key = core.key
178
+
179
+ t.ok(core.writable)
180
+ await core.append('hello')
181
+ t.is(core.length, 1)
182
+
183
+ await store.close()
184
+ store = new Corestore(dir)
185
+ core = store.get(key)
186
+ await core.ready()
187
+
188
+ t.ok(core.writable)
189
+ await core.append('world')
190
+ t.is(core.length, 2)
191
+ t.alike(await core.get(0), Buffer.from('hello'))
192
+ t.alike(await core.get(1), Buffer.from('world'))
193
+ })
194
+
169
195
  test('storage locking', async function (t) {
170
196
  const dir = tmpdir()
171
197
 
@@ -247,6 +273,50 @@ test('findingPeers', async function (t) {
247
273
  t.is(cUpdated, true)
248
274
  })
249
275
 
276
+ test('different primary keys yield different keypairs', async function (t) {
277
+ const pk1 = randomBytes(32)
278
+ const pk2 = randomBytes(32)
279
+ t.unlike(pk1, pk2)
280
+
281
+ const store1 = new Corestore(ram, { primaryKey: pk1 })
282
+ const store2 = new Corestore(ram, { primaryKey: pk2 })
283
+
284
+ const kp1 = await store1.createKeyPair('hello')
285
+ const kp2 = await store2.createKeyPair('hello')
286
+
287
+ t.unlike(kp1.publicKey, kp2.publicKey)
288
+ })
289
+
290
+ test('keypair auth sign', async function (t) {
291
+ const store = new Corestore(ram)
292
+ const keyPair = await store.createKeyPair('foo')
293
+ const message = b4a.from('hello world')
294
+
295
+ const sig = keyPair.auth.sign(message)
296
+
297
+ t.is(sig.length, 64)
298
+ t.ok(crypto.verify(message, sig, keyPair.publicKey))
299
+ t.absent(crypto.verify(message, b4a.alloc(64), keyPair.publicKey))
300
+ })
301
+
302
+ test('keypair auth verify', async function (t) {
303
+ const store = new Corestore(ram)
304
+ const keyPair = await store.createKeyPair('foo')
305
+ const message = b4a.from('hello world')
306
+
307
+ const sig = crypto.sign(message, keyPair.secretKey)
308
+
309
+ t.is(sig.length, 64)
310
+ t.ok(keyPair.auth.verify(message, sig))
311
+ t.absent(keyPair.auth.verify(message, b4a.alloc(64)))
312
+ })
313
+
250
314
  function tmpdir () {
251
315
  return path.join(os.tmpdir(), 'corestore-' + Math.random().toString(16).slice(2))
252
316
  }
317
+
318
+ function randomBytes (n) {
319
+ const buf = b4a.allocUnsafe(n)
320
+ sodium.randombytes_buf(buf)
321
+ return buf
322
+ }
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,68 +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.join(__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 keys1.close()
55
- await keys2.close()
56
-
57
- await fs.promises.rm(testPath, { recursive: true })
58
- })
59
-
60
- test('different master keys -> different keys', async t => {
61
- const keys1 = await KeyManager.fromStorage(ram)
62
- const keys2 = await KeyManager.fromStorage(ram)
63
-
64
- const kp1 = await keys1.createHypercoreKeyPair('core1')
65
- const kp2 = await keys2.createHypercoreKeyPair('core1')
66
-
67
- t.unlike(kp1.publicKey, kp2.publicKey)
68
- })