hypercore 10.0.0-alpha.6 → 10.0.0-alpha.7

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/README.md CHANGED
@@ -63,7 +63,8 @@ Note that `tree`, `data`, and `bitfield` are normally heavily sparse files.
63
63
  createIfMissing: true, // create a new Hypercore key pair if none was present in storage
64
64
  overwrite: false, // overwrite any old Hypercore that might already exist
65
65
  valueEncoding: 'json' | 'utf-8' | 'binary', // defaults to binary
66
- keyPair: kp // optionally pass the public key and secret key as a key pair
66
+ keyPair: kp, // optionally pass the public key and secret key as a key pair
67
+ encryptionKey: k // optionally pass an encryption key to enable block encryption
67
68
  }
68
69
  ```
69
70
 
@@ -202,6 +203,10 @@ In contrast to `core.key` this key does not allow you to verify the data but can
202
203
 
203
204
  Populated after `ready` has been emitted. Will be `null` before the event.
204
205
 
206
+ #### `core.encryptionKey`
207
+
208
+ Buffer containing the optional block encryption key of this core. Will be `null` unless block encryption is enabled.
209
+
205
210
  #### `core.length`
206
211
 
207
212
  How many blocks of data are available on this core?
@@ -220,6 +225,10 @@ What is the current fork id of this core?
220
225
 
221
226
  Populated after `ready` has been emitted. Will be `0` before the event.
222
227
 
228
+ #### `core.padding`
229
+
230
+ How much padding is applied to each block of this core? Will be `0` unless block encryption is enabled.
231
+
223
232
  #### `const stream = core.replicate(isInitiatorOrReplicationStream)`
224
233
 
225
234
  Create a replication stream. You should pipe this to another Hypercore instance.
package/index.js CHANGED
@@ -12,6 +12,7 @@ const fsctl = requireMaybe('fsctl') || { lock: noop, sparse: noop }
12
12
  const Replicator = require('./lib/replicator')
13
13
  const Extensions = require('./lib/extensions')
14
14
  const Core = require('./lib/core')
15
+ const BlockEncryption = require('./lib/block-encryption')
15
16
 
16
17
  const promises = Symbol.for('hypercore.promises')
17
18
  const inspect = Symbol.for('nodejs.util.inspect.custom')
@@ -49,6 +50,7 @@ module.exports = class Hypercore extends EventEmitter {
49
50
  this.replicator = null
50
51
  this.extensions = opts.extensions || new Extensions()
51
52
  this.cache = opts.cache === true ? new Xache({ maxSize: 65536, maxAge: 0 }) : (opts.cache || null)
53
+ this.encryption = opts.encryption || null
52
54
 
53
55
  this.valueEncoding = null
54
56
  this.key = key || null
@@ -64,6 +66,8 @@ module.exports = class Hypercore extends EventEmitter {
64
66
  this.closing = null
65
67
  this.opening = opts._opening || this._open(key, storage, opts)
66
68
  this.opening.catch(noop)
69
+
70
+ this._preappend = this.encryption && preappend.bind(this)
67
71
  }
68
72
 
69
73
  [inspect] (depth, opts) {
@@ -119,6 +123,7 @@ module.exports = class Hypercore extends EventEmitter {
119
123
  ...opts,
120
124
  sign: opts.sign || (keyPair && keyPair.secretKey && Core.createSigner(this.crypto, keyPair)) || this.sign,
121
125
  valueEncoding: this.valueEncoding,
126
+ encryption: this.encryption,
122
127
  extensions: this.extensions,
123
128
  _opening: this.opening,
124
129
  _sessions: this.sessions
@@ -207,7 +212,7 @@ module.exports = class Hypercore extends EventEmitter {
207
212
  }
208
213
 
209
214
  get byteLength () {
210
- return this.core === null ? 0 : this.core.tree.byteLength
215
+ return this.core === null ? 0 : this.core.tree.byteLength - (this.core.tree.length * this.padding)
211
216
  }
212
217
 
213
218
  get fork () {
@@ -218,6 +223,14 @@ module.exports = class Hypercore extends EventEmitter {
218
223
  return this.replicator === null ? [] : this.replicator.peers
219
224
  }
220
225
 
226
+ get encryptionKey () {
227
+ return this.encryption && this.encryption.key
228
+ }
229
+
230
+ get padding () {
231
+ return this.encryption === null ? 0 : this.encryption.padding
232
+ }
233
+
221
234
  ready () {
222
235
  return this.opening
223
236
  }
@@ -271,6 +284,11 @@ module.exports = class Hypercore extends EventEmitter {
271
284
  this.key = this.core.header.signer.publicKey
272
285
  this.writable = !!this.sign
273
286
 
287
+ if (!this.encryption && opts.encryptionKey) {
288
+ this.encryption = new BlockEncryption(opts.encryptionKey, this.key)
289
+ this._preappend = preappend.bind(this)
290
+ }
291
+
274
292
  this.extensions.attach(this.replicator)
275
293
  this.opened = true
276
294
 
@@ -305,6 +323,12 @@ module.exports = class Hypercore extends EventEmitter {
305
323
  }
306
324
 
307
325
  if (value) {
326
+ if (this.encryption) {
327
+ this.encryption.decrypt(bitfield.start, value)
328
+ }
329
+
330
+ value = value.subarray(this.padding)
331
+
308
332
  for (let i = 0; i < this.sessions.length; i++) {
309
333
  this.sessions[i].emit('download', bitfield.start, value, from)
310
334
  }
@@ -343,7 +367,7 @@ module.exports = class Hypercore extends EventEmitter {
343
367
  async seek (bytes) {
344
368
  if (this.opened === false) await this.opening
345
369
 
346
- const s = this.core.tree.seek(bytes)
370
+ const s = this.core.tree.seek(bytes, this.padding)
347
371
 
348
372
  return (await s.update()) || this.replicator.requestSeek(s)
349
373
  }
@@ -367,10 +391,17 @@ module.exports = class Hypercore extends EventEmitter {
367
391
  async _get (index, opts) {
368
392
  const encoding = (opts && opts.valueEncoding && c.from(codecs(opts.valueEncoding))) || this.valueEncoding
369
393
 
370
- if (this.core.bitfield.get(index)) return decode(encoding, await this.core.blocks.get(index))
371
- if (opts && opts.onwait) opts.onwait(index)
394
+ let block
395
+
396
+ if (this.core.bitfield.get(index)) {
397
+ block = await this.core.blocks.get(index)
398
+ } else {
399
+ if (opts && opts.onwait) opts.onwait(index)
400
+ block = await this.replicator.requestBlock(index)
401
+ }
372
402
 
373
- return decode(encoding, await this.replicator.requestBlock(index))
403
+ if (this.encryption) this.encryption.decrypt(index, block)
404
+ return this._decode(encoding, block)
374
405
  }
375
406
 
376
407
  download (range) {
@@ -427,22 +458,17 @@ module.exports = class Hypercore extends EventEmitter {
427
458
  if (this.opened === false) await this.opening
428
459
  if (this.writable === false) throw new Error('Core is not writable')
429
460
 
430
- const blks = Array.isArray(blocks) ? blocks : [blocks]
431
- const buffers = new Array(blks.length)
432
-
433
- for (let i = 0; i < blks.length; i++) {
434
- const blk = blks[i]
461
+ blocks = Array.isArray(blocks) ? blocks : [blocks]
435
462
 
436
- const buf = Buffer.isBuffer(blk)
437
- ? blk
438
- : this.valueEncoding
439
- ? c.encode(this.valueEncoding, blk)
440
- : Buffer.from(blk)
463
+ const buffers = new Array(blocks.length)
441
464
 
442
- buffers[i] = buf
465
+ for (let i = 0; i < blocks.length; i++) {
466
+ buffers[i] = this._encode(this.valueEncoding, blocks[i])
443
467
  }
444
468
 
445
- return await this.core.append(buffers, this.sign)
469
+ return await this.core.append(buffers, this.sign, {
470
+ preappend: this._preappend
471
+ })
446
472
  }
447
473
 
448
474
  registerExtension (name, handlers) {
@@ -453,14 +479,38 @@ module.exports = class Hypercore extends EventEmitter {
453
479
  onextensionupdate () {
454
480
  if (this.replicator !== null) this.replicator.broadcastOptions()
455
481
  }
456
- }
457
482
 
458
- function noop () {}
483
+ _encode (enc, val) {
484
+ const state = { start: this.padding, end: this.padding, buffer: null }
485
+
486
+ if (Buffer.isBuffer(val)) {
487
+ if (state.start === 0) return val
488
+ state.end += val.byteLength
489
+ } else if (enc) {
490
+ enc.preencode(state, val)
491
+ } else {
492
+ val = Buffer.from(val)
493
+ if (state.start === 0) return val
494
+ state.end += val.byteLength
495
+ }
496
+
497
+ state.buffer = Buffer.allocUnsafe(state.end)
498
+
499
+ if (enc) enc.encode(state, val)
500
+ else state.buffer.set(val, state.start)
501
+
502
+ return state.buffer
503
+ }
459
504
 
460
- function decode (enc, buf) {
461
- return enc ? c.decode(enc, buf) : buf
505
+ _decode (enc, block) {
506
+ block = block.subarray(this.padding)
507
+ if (enc) return c.decode(enc, block)
508
+ return block
509
+ }
462
510
  }
463
511
 
512
+ function noop () {}
513
+
464
514
  function isStream (s) {
465
515
  return typeof s === 'object' && s && typeof s.pipe === 'function'
466
516
  }
@@ -489,3 +539,12 @@ function min (arr) {
489
539
  function max (arr) {
490
540
  return reduce(arr, (a, b) => Math.max(a, b), -Infinity)
491
541
  }
542
+
543
+ function preappend (blocks) {
544
+ const offset = this.core.tree.length
545
+ const fork = this.core.tree.fork
546
+
547
+ for (let i = 0; i < blocks.length; i++) {
548
+ this.encryption.encrypt(offset + i, blocks[i], fork)
549
+ }
550
+ }
@@ -0,0 +1,67 @@
1
+ const sodium = require('sodium-universal')
2
+ const c = require('compact-encoding')
3
+
4
+ const nonce = Buffer.alloc(sodium.crypto_stream_NONCEBYTES)
5
+
6
+ module.exports = class BlockEncryption {
7
+ constructor (encryptionKey, hypercoreKey) {
8
+ const subKeys = Buffer.alloc(2 * sodium.crypto_stream_KEYBYTES)
9
+
10
+ this.key = encryptionKey
11
+ this.blockKey = subKeys.subarray(0, sodium.crypto_stream_KEYBYTES)
12
+ this.blindingKey = subKeys.subarray(sodium.crypto_stream_KEYBYTES)
13
+ this.padding = 8
14
+
15
+ sodium.crypto_generichash(this.blockKey, encryptionKey, hypercoreKey)
16
+ sodium.crypto_generichash(this.blindingKey, this.blockKey)
17
+ }
18
+
19
+ encrypt (index, block, fork) {
20
+ const padding = block.subarray(0, this.padding)
21
+ block = block.subarray(this.padding)
22
+
23
+ c.uint64.encode({ start: 0, end: 8, buffer: padding }, fork)
24
+ c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
25
+
26
+ // Zero out any previous padding.
27
+ nonce.fill(0, 8, 8 + padding.byteLength)
28
+
29
+ // Blind the fork ID, possibly risking reusing the nonce on a reorg of the
30
+ // Hypercore. This is fine as the blinding is best-effort and the latest
31
+ // fork ID shared on replication anyway.
32
+ sodium.crypto_stream_xor(
33
+ padding,
34
+ padding,
35
+ nonce,
36
+ this.blindingKey
37
+ )
38
+
39
+ nonce.set(padding, 8)
40
+
41
+ // The combination of a (blinded) fork ID and a block index is unique for a
42
+ // given Hypercore and is therefore a valid nonce for encrypting the block.
43
+ sodium.crypto_stream_xor(
44
+ block,
45
+ block,
46
+ nonce,
47
+ this.blockKey
48
+ )
49
+ }
50
+
51
+ decrypt (index, block) {
52
+ const padding = block.subarray(0, this.padding)
53
+ block = block.subarray(this.padding)
54
+
55
+ c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
56
+
57
+ nonce.set(padding, 8)
58
+
59
+ // Decrypt the block using the blinded fork ID.
60
+ sodium.crypto_stream_xor(
61
+ block,
62
+ block,
63
+ nonce,
64
+ this.blockKey
65
+ )
66
+ }
67
+ }
package/lib/core.js CHANGED
@@ -214,10 +214,12 @@ module.exports = class Core {
214
214
  }
215
215
  }
216
216
 
217
- async append (values, sign = this.defaultSign) {
217
+ async append (values, sign = this.defaultSign, hooks = {}) {
218
218
  await this._mutex.lock()
219
219
 
220
220
  try {
221
+ if (hooks.preappend) await hooks.preappend(values)
222
+
221
223
  if (!values.length) return this.tree.length
222
224
 
223
225
  const batch = this.tree.batch()
@@ -272,11 +272,15 @@ class ReorgBatch extends MerkleTreeBatch {
272
272
  }
273
273
 
274
274
  class ByteSeeker {
275
- constructor (tree, bytes) {
275
+ constructor (tree, bytes, padding = 0) {
276
276
  this.tree = tree
277
277
  this.bytes = bytes
278
- this.start = bytes >= tree.byteLength ? tree.length : 0
279
- this.end = bytes < tree.byteLength ? tree.length : 0
278
+ this.padding = padding
279
+
280
+ const size = tree.byteLength - (tree.length * padding)
281
+
282
+ this.start = bytes >= size ? tree.length : 0
283
+ this.end = bytes < size ? tree.length : 0
280
284
  }
281
285
 
282
286
  nodes () {
@@ -287,12 +291,12 @@ class ByteSeeker {
287
291
  if (!bytes) return [0, 0]
288
292
 
289
293
  for (const node of this.tree.roots) { // all async ticks happen once we find the root so safe
290
- if (bytes === node.size) {
291
- return [flat.rightSpan(node.index) + 2, 0]
292
- }
294
+ let size = node.size
295
+ if (this.padding > 0) size -= this.padding * flat.countLeaves(node.index)
293
296
 
294
- if (bytes > node.size) {
295
- bytes -= node.size
297
+ if (bytes === size) return [flat.rightSpan(node.index) + 2, 0]
298
+ if (bytes > size) {
299
+ bytes -= size
296
300
  continue
297
301
  }
298
302
 
@@ -301,9 +305,12 @@ class ByteSeeker {
301
305
  while ((ite.index & 1) !== 0) {
302
306
  const l = await this.tree.get(ite.leftChild(), false)
303
307
  if (l) {
304
- if (l.size === bytes) return [ite.rightSpan() + 2, 0]
305
- if (l.size > bytes) continue
306
- bytes -= l.size
308
+ let size = l.size
309
+ if (this.padding > 0) size -= this.padding * ite.countLeaves()
310
+
311
+ if (size === bytes) return [ite.rightSpan() + 2, 0]
312
+ if (size > bytes) continue
313
+ bytes -= size
307
314
  ite.sibling()
308
315
  } else {
309
316
  ite.parent()
@@ -355,8 +362,8 @@ module.exports = class MerkleTree {
355
362
  return new MerkleTreeBatch(this)
356
363
  }
357
364
 
358
- seek (bytes) {
359
- return new ByteSeeker(this, bytes)
365
+ seek (bytes, padding) {
366
+ return new ByteSeeker(this, bytes, padding)
360
367
  }
361
368
 
362
369
  hash () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypercore",
3
- "version": "10.0.0-alpha.6",
3
+ "version": "10.0.0-alpha.7",
4
4
  "description": "Hypercore 10",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -31,14 +31,15 @@
31
31
  "@hyperswarm/secret-stream": "^5.0.0",
32
32
  "big-sparse-array": "^1.0.2",
33
33
  "codecs": "^2.2.0",
34
- "compact-encoding": "^2.0.0",
34
+ "compact-encoding": "^2.5.0",
35
35
  "crc32-universal": "^1.0.1",
36
- "flat-tree": "^1.7.0",
36
+ "flat-tree": "^1.9.0",
37
37
  "hypercore-crypto": "^2.1.1",
38
38
  "is-options": "^1.0.1",
39
39
  "random-access-file": "^2.1.4",
40
40
  "random-array-iterator": "^1.0.0",
41
41
  "safety-catch": "^1.0.1",
42
+ "sodium-universal": "^3.0.4",
42
43
  "uint64le": "^1.0.0",
43
44
  "xache": "^1.0.0"
44
45
  },
@@ -0,0 +1,85 @@
1
+ const test = require('brittle')
2
+ const { create, replicate } = require('./helpers')
3
+
4
+ const encryptionKey = Buffer.alloc(32, 'hello world')
5
+
6
+ test('encrypted append and get', async function (t) {
7
+ const a = await create({ encryptionKey })
8
+
9
+ t.alike(a.encryptionKey, encryptionKey)
10
+
11
+ await a.append(['hello'])
12
+
13
+ t.is(a.byteLength, 5)
14
+ t.is(a.core.tree.byteLength, 5 + a.padding)
15
+
16
+ const unencrypted = await a.get(0)
17
+ const encrypted = await a.core.blocks.get(0)
18
+
19
+ t.alike(unencrypted, Buffer.from('hello'))
20
+ t.unlike(unencrypted, encrypted)
21
+ })
22
+
23
+ test('encrypted seek', async function (t) {
24
+ const a = await create({ encryptionKey })
25
+
26
+ await a.append(['hello', 'world', '!'])
27
+
28
+ t.alike(await a.seek(0), [0, 0])
29
+ t.alike(await a.seek(4), [0, 4])
30
+ t.alike(await a.seek(5), [1, 0])
31
+ t.alike(await a.seek(6), [1, 1])
32
+ t.alike(await a.seek(6), [1, 1])
33
+ t.alike(await a.seek(9), [1, 4])
34
+ t.alike(await a.seek(10), [2, 0])
35
+ t.alike(await a.seek(11), [3, 0])
36
+ })
37
+
38
+ test('encrypted replication', async function (t) {
39
+ const a = await create({ encryptionKey })
40
+
41
+ await a.append(['a', 'b', 'c', 'd', 'e'])
42
+
43
+ t.test('with encryption key', async function (t) {
44
+ t.plan(10)
45
+
46
+ const b = await create(a.key, { encryptionKey })
47
+
48
+ b.on('download', (i, block) => {
49
+ t.alike(block, Buffer.from([i + /* a */ 0x61]))
50
+ })
51
+
52
+ replicate(a, b, t)
53
+
54
+ const r = b.download({ start: 0, end: a.length })
55
+ await r.downloaded()
56
+
57
+ for (let i = 0; i < 5; i++) {
58
+ t.alike(await b.get(i), await a.get(i))
59
+ }
60
+ })
61
+
62
+ t.test('without encryption key', async function (t) {
63
+ const b = await create(a.key)
64
+
65
+ replicate(a, b, t)
66
+
67
+ const r = b.download({ start: 0, end: a.length })
68
+ await r.downloaded()
69
+
70
+ for (let i = 0; i < 5; i++) {
71
+ t.unlike(await b.get(i), await a.get(i))
72
+ t.alike(await b.get(i), await a.core.blocks.get(i))
73
+ }
74
+ })
75
+ })
76
+
77
+ test('encrypted sessions', async function (t) {
78
+ const a = await create({ encryptionKey })
79
+
80
+ await a.append(['hello'])
81
+
82
+ const session = a.session()
83
+
84
+ t.alike(await session.get(0), Buffer.from('hello'))
85
+ })