@coboxcoop/space
Version:
a peer-to-peer private and encrypted space
519 lines (444 loc) • 16.2 kB
JavaScript
const { describe } = require('tape-plus')
const crypto = require('@coboxcoop/crypto')
const RAM = require('random-access-memory')
const Corestore = require('corestore')
const collect = require('collect-stream')
const debug = require('@coboxcoop/logger')('@coboxcoop/space')
const path = require('path')
const fs = require('fs')
const mkdirp = require('mkdirp')
const randomWords = require('random-words')
const sinon = require('sinon')
const proxyquire = require('proxyquire')
const mount = require('kappa-drive-mount')
const { Header } = require('hypertrie/lib/messages')
const SpaceAbout = require('@coboxcoop/schemas').encodings.space.about
const PeerAbout = require('@coboxcoop/schemas').encodings.peer.about
const Space = require('../')
const { replicate, tmp, cleanup } = require('./util')
describe('@coboxcoop/space: Space', (context) => {
let sandbox
context.beforeEach((c) => {
sandbox = sinon.createSandbox()
})
context.afterEach((c) => {
sandbox.restore()
})
context('constructor() - generates new encryption_key', (assert, next) => {
var storage = tmp()
var address = crypto.address().toString('hex')
var identity = crypto.boxKeyPair()
identity.name = randomWords(1).pop()
var spy = sandbox.spy(crypto)
var ProxySpace = proxyquire('../', { '@coboxcoop/crypto': spy })
var space = ProxySpace(storage, address, identity)
assert.ok(space, 'space created')
assert.same(space.address, Buffer.from(address, 'hex'), 'has address')
assert.same(space.identity.name, identity.name, 'has name')
assert.ok(spy.encryptionKey.calledOnce, 'calls crypto.encryptionKey()')
assert.ok(spy.encoder.calledOnce, 'uses encryption_key in encoder')
cleanup(storage, next)
})
context('constructor() - loads encryption_key from path', (assert, next) => {
var storage = tmp()
var address = crypto.address()
var encryptionKey = crypto.encryptionKey()
var loadStub = sandbox.stub().returns(encryptionKey)
var saveStub = sandbox.stub().returns(true)
var ProxySpace = proxyquire('../', { '@coboxcoop/keys': {
loadKey: loadStub,
saveKey: saveStub
}})
var identity = crypto.boxKeyPair()
var space = ProxySpace(storage, address, identity)
assert.ok(loadStub.calledWith(space.storage), 'key is loaded')
assert.ok(saveStub.calledWith(space.storage, 'encryption_key', encryptionKey), 'key is saved')
next()
})
context('constructor() - saves encryption_key given as argument', (assert, next) => {
var storage = tmp()
var address = crypto.address().toString('hex')
var encryptionKey = crypto.encryptionKey().toString('hex')
var space = Space(storage, address, crypto.boxKeyPair(), { encryptionKey })
var key = fs.readFileSync(path.join(space.storage, 'encryption_key'))
assert.ok(space, 'space loaded')
assert.ok(space.address, address, 'same address')
assert.same(key.toString('hex'), encryptionKey.toString('hex'), 'encryption_key stored correctly')
cleanup(storage, next)
})
// context('reload', (assert, next) => {
// var storage = tmp()
// var address = crypto.address().toString('hex')
// var encryptionKey = crypto.encryptionKey().toString('hex')
// var space = Space(storage, address, {}, {
// encryptionKey,
// corestore: new Corestore(RAM)
// })
// write('world', () => {
// space.close((err) => {
// assert.error(err, 'no error')
// var newSpace = Space(storage, address, {}, {
// encryptionKey,
// corestore: new Corestore(RAM)
// })
// newSpace.ready((err) => {
// assert.error(err, 'no error')
// newSpace.drive.readFile('/hello.txt', (err, data) => {
// assert.same(data.toString(), 'world', 'reloads and reads')
// done()
// })
// })
// })
// })
// function write (data, cb) {
// space.ready((err) => {
// assert.error(err, 'no error')
// space.drive.writeFile('/hello.txt', data, (err) => {
// assert.error(err, 'no error')
// cb()
// })
// })
// }
// function done () {
// cleanup(storage, next)
// }
// })
context('destroy()', (assert, next) => {
var storage1 = tmp(),
storage2 = tmp(),
address = crypto.address(),
encryptionKey = crypto.encryptionKey()
var space1 = Space(storage1, address, { encryptionKey })
space1.ready(() => {
space1.destroy((err) => {
assert.error(err, 'no error')
cleanup([storage1, storage2], next)
})
})
})
context('deriveKeyPair()', (assert, next) => {
var storage = tmp()
var address = crypto.address()
var parentKey = crypto.masterKey()
var deriveKeyPair = (id, ctxt) => crypto.keyPair(parentKey, id, ctxt)
var corestore = new Corestore(RAM, { masterKey: parentKey })
var space = Space(path.join(storage, 'spaces'), address, crypto.boxKeyPair(), {
corestore,
deriveKeyPair,
name: randomWords(1).pop()
})
space.ready(() => {
var logKp = deriveKeyPair(0, space.address)
assert.same(space.log._feed.key, logKp.publicKey, 'Log key derived correctly')
assert.same(space.log._feed.secretKey, logKp.secretKey, 'Log secret key derived correctly')
var metadataKp = deriveKeyPair(1, space.address)
assert.same(space.drive.metadata.key, metadataKp.publicKey, 'Metadata key derived correctly')
assert.same(space.drive.metadata.secretKey, metadataKp.secretKey, 'Metadata secret key derived correctly')
var contentKp = deriveKeyPair(2, space.address)
assert.same(space.drive.content.key, contentKp.publicKey, 'Content key derived correctly')
assert.same(space.drive.content.secretKey, contentKp.secretKey, 'Content secret key derived correctly')
cleanup(storage, next)
})
})
context('replicate() - basic', (assert, next) => {
var storage1 = tmp(),
storage2 = tmp(),
address = crypto.address(),
name1 = randomWords(1).pop(),
name2 = randomWords(1).pop(),
identity = crypto.boxKeyPair(),
encryptionKey = crypto.encryptionKey()
var space1 = Space(storage1, address, identity, { encryptionKey, name: name1 })
var space2 = Space(storage2, address, identity, { encryptionKey, name: name2 })
var dog = 'dog'
var cat = 'cat'
space1.ready(() => {
space2.ready(() => {
space1.writer((err, feed1) => {
assert.error(err, 'no error')
space2.writer((err, feed2) => {
assert.error(err, 'no error')
feed1.append(dog, (err, seq) => {
assert.error(err, 'no error')
feed2.append(cat, (err, seq) => {
assert.error(err, 'no error')
replicate(space1, space2, (err) => {
assert.error(err, 'no error on replicate')
var dup2 = space2.feed(feed1.key)
assert.ok(dup2, 'gets feed')
assert.equal(feed1.key.toString('hex'), dup2.key.toString('hex'), 'feed keys match')
assert.equal(dup2.length, 1, 'contains one message')
assert.notOk(dup2.secretKey, 'duplicate has no secret key')
var dup1 = space1.feed(feed2.key)
assert.ok(dup1, 'gets feed')
assert.equal(dup1.length, 1, 'contains one message')
cleanup([storage1, storage2], next)
})
})
})
})
})
})
})
})
context('replicate() - log', (assert, next) => {
var storage1 = tmp(),
storage2 = tmp(),
timestamp = Date.now(),
address = crypto.address(),
encryptionKey = crypto.encryptionKey(),
alice = crypto.boxKeyPair(),
bob = crypto.boxKeyPair()
var aliceSpace = Space(storage1, address, alice, {
encryptionKey,
corestore: new Corestore(storage1)
})
var bobSpace = Space(storage2, address, bob, {
encryptionKey,
corestore: new Corestore(storage2)
})
var spaceAbout = {
type: 'space/about',
version: '1.0.0',
author: alice.publicKey.toString('hex'),
timestamp: Date.now(),
content: { name: "Space is the Place" }
}
var aliceAbout = {
type: 'peer/about',
version: '1.0.0',
author: alice.publicKey.toString('hex'),
timestamp: Date.now() + 1,
content: { name: 'Alice' }
}
var bobAbout = {
type: 'peer/about',
version: '1.0.0',
author: bob.publicKey.toString('hex'),
timestamp: Date.now() + 2,
content: { name: 'Bob' }
}
publish(aliceSpace, spaceAbout, { valueEncoding: SpaceAbout }, () => {
publish(aliceSpace, aliceAbout, { valueEncoding: PeerAbout }, () => {
sync(() => {
checkBobReplicatedAlice(() => {
publish(bobSpace, bobAbout, { valueEncoding: PeerAbout }, () => {
sync(() => {
checkAliceReplicatedBob()
})
})
})
})
})
})
function publish (space, message, opts, cb) {
space.ready((err) => {
assert.error(err, 'no error')
space.log.publish(message, opts, (err, msg) => {
assert.error(err, 'no error')
cb(err)
})
})
}
function sync (cb) {
aliceSpace.ready(() => bobSpace.ready(() => {
replicate(aliceSpace, bobSpace, (err) => {
assert.error(err, 'no error on replicate')
aliceSpace.ready(() => bobSpace.ready(cb))
})
}))
}
function checkBobReplicatedAlice (cb) {
getMessages(bobSpace, (err, msgs) => {
assert.error(err, 'no error')
assert.same(msgs.length, 2, 'gets two messages')
var values = msgs.map((msg) => msg.value)
assert.same(values[0], spaceAbout, 'replicated space/about')
assert.same(values[1], aliceAbout, 'replicated peer/about')
cb()
})
}
function checkAliceReplicatedBob () {
getMessages(aliceSpace, (err, msgs) => {
assert.error(err, 'no error')
assert.same(msgs.length, 3, 'gets three messages')
var values = msgs.map((msg) => msg.value)
assert.same(values[2], bobAbout, 'replicated peer/about')
next()
})
}
function getMessages (space, cb) {
var query = [{ $filter: { value: { timestamp: { $gt: 0 } } } }]
collect(space.log.read({ query }), cb)
}
})
context('replicate() - drive', (assert, next) => {
var storage1 = tmp(),
storage2 = tmp(),
address = crypto.address(),
encryptionKey = crypto.encryptionKey(),
name1 = randomWords(1).pop(),
name2 = randomWords(1).pop(),
identity = crypto.boxKeyPair()
var space1 = Space(storage1, address, identity, { encryptionKey, name: name1 }),
space2 = Space(storage2, address, identity, { encryptionKey, name: name2 })
write(space1, 'world', () => {
sync(() => {
write(space2, 'mundo', () => {
sync(() => check(done))
})
})
})
function done () {
space1.destroy((err) => {
assert.error(err, 'no error')
space2.destroy((err) => {
assert.error(err, 'no error')
cleanup([storage1, storage2], next)
})
})
}
function write (space, data, cb) {
space.ready((err) => {
assert.error(err, 'no error')
space.drive.writeFile('/hello.txt', data, (err) => {
assert.error(err, 'no error')
cb()
})
})
}
function sync (cb) {
space1.ready(() => space2.ready(() => {
replicate(space1, space2, (err) => {
assert.error(err, 'no error')
space1.ready(() => space2.ready(cb))
})
}))
}
function check (cb) {
space1.drive.readFile('/hello.txt', (err, data) => {
assert.error(err, 'no error')
assert.same(data, Buffer.from('mundo'), 'gets latest value')
write(space2, 'verden', () => sync(() => {
space1.drive.readFile('/hello.txt', (err, data) => {
assert.error(err, 'no error')
assert.same(data, Buffer.from('verden'), 'gets latest value')
cb()
})
}))
})
}
})
context('replicate() - log and drive', (assert, next) => {
var storage1 = tmp(),
storage2 = tmp(),
timestamp = Date.now(),
address = crypto.address(),
encryptionKey = crypto.encryptionKey(),
alice = crypto.boxKeyPair(),
bob = crypto.boxKeyPair()
var aliceSpace = Space(storage1, address, alice, {
encryptionKey,
corestore: new Corestore(storage1)
})
var bobSpace = Space(storage2, address, bob, {
encryptionKey,
corestore: new Corestore(storage2)
})
var spaceAbout = {
type: 'space/about',
version: '1.0.0',
author: alice.publicKey.toString('hex'),
timestamp: Date.now(),
content: { name: "Space is the Place" }
}
var aliceAbout = {
type: 'peer/about',
version: '1.0.0',
author: alice.publicKey.toString('hex'),
timestamp: Date.now() + 1,
content: { name: 'Alice' }
}
var bobAbout = {
type: 'peer/about',
version: '1.0.0',
author: bob.publicKey.toString('hex'),
timestamp: Date.now() + 2,
content: { name: 'Bob' }
}
publish(aliceSpace, spaceAbout, { valueEncoding: SpaceAbout }, () => {
publish(aliceSpace, aliceAbout, { valueEncoding: PeerAbout }, () => {
write(aliceSpace, 'hello bob', () => {
sync(() => {
checkBobReplicatedAlice(() => {
publish(bobSpace, bobAbout, { valueEncoding: PeerAbout }, () => {
write(bobSpace, 'hi alice', () => {
sync(() => {
checkAliceReplicatedBob()
})
})
})
})
})
})
})
})
function publish (space, message, opts, cb) {
space.ready((err) => {
assert.error(err, 'no error')
space.log.publish(message, opts, (err, msg) => {
assert.error(err, 'no error')
cb(err)
})
})
}
function write (space, data, cb) {
space.ready((err) => {
assert.error(err, 'no error')
space.drive.writeFile('/hello.txt', data, (err) => {
assert.error(err, 'no error')
cb()
})
})
}
function sync (cb) {
aliceSpace.ready(() => bobSpace.ready(() => {
replicate(aliceSpace, bobSpace, (err) => {
assert.error(err, 'no error on replicate')
aliceSpace.ready(() => bobSpace.ready(cb))
})
}))
}
function checkBobReplicatedAlice (cb) {
getMessages(bobSpace, (err, msgs) => {
assert.error(err, 'no error')
assert.same(msgs.length, 2, 'gets two messages')
var values = msgs.map((msg) => msg.value)
assert.same(values[0], spaceAbout, 'replicated space/about')
assert.same(values[1], aliceAbout, 'replicated peer/about')
bobSpace.drive.readFile('/hello.txt', (err, data) => {
assert.error(err, 'no error')
assert.same(data.toString(), 'hello bob', 'replicated drive')
cb()
})
})
}
function checkAliceReplicatedBob () {
getMessages(aliceSpace, (err, msgs) => {
assert.error(err, 'no error')
assert.same(msgs.length, 3, 'gets three messages')
var values = msgs.map((msg) => msg.value)
assert.same(values[2], bobAbout, 'replicated peer/about')
aliceSpace.drive.readFile('/hello.txt', (err, data) => {
assert.error(err, 'no error')
assert.same(data.toString(), 'hi alice', 'replicated drive')
next()
})
})
}
function getMessages (space, cb) {
var query = [{ $filter: { value: { timestamp: { $gt: 0 } } } }]
collect(space.log.read({ query }), cb)
}
})
})