multifeed-sigrid
Version:
multifeed plugin providing signature based replication
173 lines (155 loc) • 5.8 kB
JavaScript
/*
* multifeed-sigrid - Signature based replication rules for noffle/multifeed
* Copyright (C) <2018> <Tony Ivanov>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var crypto = require('hypercore-crypto')
var debug = require('debug')('multifeed-sigrid')
var SIGNATURES_JSON = 'signatures.json'
var assert = require('assert')
/**
* @param key {String|Buffer} will only replicate feeds that are signed with
* this key
* @param storage {RandomAccess} A random-access storage used for persisting known signatures
* locally
* @param secret {String|Buffer} enables local writers to be signed for replication.
*/
function Sigrid(key, storage, secret) {
if (!(this instanceof Sigrid)) return new Sigrid(key, storage, secret)
assert.ok(key, 'Signature checking key is required')
assert.ok(storage, 'Storage is required')
this.key = ensureBuffer(key)
this._canSign = !!secret
this._secret = ensureBuffer(secret)
this._store = storage(SIGNATURES_JSON)
this._signatures = {}
}
Sigrid.prototype.init = function(multifeed) {
multifeed.key = this.key // overwrite multifeed's encryption key,
// might remove this for purposes other than
// personal-feed
multifeed.prependListener('feed', this._onFeed.bind(this))
this._reload()
}
Sigrid.prototype.have = function(local, next) {
var self = this
// only share verified feed-keys
var feedKeys = local.keys.filter(function(fkey){
return verify(fkey, self._signatures[fkey], self.key)
})
// extract their signatures
var sigs = feedKeys.map(function(key) { return self._signatures[key] })
next(feedKeys, {signatures: sigs})
}
Sigrid.prototype.want = function(remote, next) {
var self = this
var sigsDiscovered = false
var verified = remote.keys.filter(function(fkey, i) {
var valid = verify(fkey, self._signatures[fkey] || remote.signatures[i], self.key)
if (valid && !self._signatures[fkey]) {
self._signatures[fkey] = remote.signatures[i]
sigsDiscovered = true
}
return valid
})
if (sigsDiscovered) self._save(function (err) {
if (err) throw err
next(verified)
})
else next(verified)
}
/**
* @param feed {String|Buffer} containing feed's key
* @param signature {String|Buffer} containing signature
* @param [done] {Function} called when new signature is successfully stored
*/
Sigrid.prototype.setSignature = function(feed, signature, done) {
if (typeof done !== 'function') done = function(err) { if(err) throw err }
feed = stringKey(feed)
signature = stringKey(signature)
this._signatures = this._signatures || {}
this._signatures[feed] = signature
this._save(done)
}
Sigrid.prototype._save = function(done) {
if (typeof done !== 'function') done = function(err) { if(err) throw err }
this._signatures = this._signatures || {}
var self = this
var sigBuf = Buffer.from(JSON.stringify(self._signatures))
var sizeBuf = Buffer.alloc(4)
sizeBuf.writeUInt32LE(sigBuf.length)
self._store.write(0, sizeBuf, function(err) {
if (err) return done(err)
self._store.write(4, sigBuf, done)
})
}
Sigrid.prototype._reload = function (done) {
if (typeof done !== 'function') done = function(err) { if(err) throw err }
var self = this
self._signatures = {}
self._store.stat(function(err, stat) {
if ((err && err.code === 'ENOENT') || (stat && stat.size === 0)) {
return done(null, self._signatures)
} else if (err) return done(err)
self._store.read(0,4,function(err, chunk) {
if (err) {
debug('Loading signatures failed, this is normal for empty multifeeds', err.message)
return done()
}
var size = chunk.readUInt32LE()
self._store.read(4, size, function(err, chunk) {
if (err) return done(err)
self._signatures = JSON.parse(chunk.toString('utf8'))
debug('Signatures reloaded')
done(null, self._signatures)
})
})
})
}
Sigrid.prototype._onFeed = function(feed, name) {
if(feed.writable) this._createSignature(feed.key)
}
Sigrid.prototype._createSignature = function (buffer, done) {
if (typeof done !== 'function') done = function(err) { if(err) throw err }
if (!this._canSign) return done()
buffer = ensureBuffer(buffer)
// Sign the feed using provided private key
var sig = crypto.sign(buffer, this._secret)
debug("Signing new writer", buffer.toString('hex'), sig.toString('hex'))
// Verify the signature
if (!verify(buffer, sig, this.key)) {
return done(new Error('Invalid signature produced, have you provided a correct key pair?'))
}
// Store the signature
this.setSignature(buffer, sig, done)
}
module.exports = Sigrid
function verify (msg, sig, key) {
if (!msg || !sig || !key) return false
return crypto.verify(
ensureBuffer(msg),
ensureBuffer(sig),
ensureBuffer(key)
)
}
function stringKey(input) {
if (Buffer.isBuffer(input)) return input.toString('hex')
return input
}
function ensureBuffer(input) {
if (Buffer.isBuffer(input)) return input
if (typeof input === 'undefined' || input === null) return input
return Buffer.from(input, 'hex')
}