plugin-contentaddress.js

/**
 * @module kadence/contentaddress
 */

'use strict';

const { createHash } = require('node:crypto');
const merge = require('merge');
const assert = require('node:assert');


/**
 * Enforces that any {@link KademliaNode~entry} stored in the DHT must be
 * content-addressable (keyed by the hash of it's value).
 */
class ContentAddressPlugin {

  static get DEFAULTS() {
    return {
      keyAlgorithm: 'ripemd160',
      valueEncoding: 'base64'
    };
  }

  /**
   * @constructor
   * @param {AbstractNode} node
   * @param {object} [options]
   * @param {string} [options.keyAlgorithm="rmd160"] - Algorithm for hashing
   * @param {string} [options.valueEncoding="base64"] - Text encoding of value
   */
  constructor(node, options) {
    this.node = node;
    this.opts = merge(ContentAddressPlugin.DEFAULTS, options);

    this.node.use('STORE', (req, res, next) => this.validate(req, res, next));
    this._wrapIterativeStore();
  }

  /**
   * @private
   */
  _wrapIterativeStore() {
    let iterativeStore = this.node.iterativeStore.bind(this.node);

    this.node.iterativeStore = (key, value, callback) => {
      try {
        const buffer = Buffer.from(value, this.opts.valueEncoding);
        const hash = createHash(this.opts.keyAlgorithm).update(buffer)
          .digest('hex');

        assert(key === hash);
      } catch (err) {
        return callback(new Error('Item failed validation check'));
      }

      iterativeStore(key, value, callback);
    };
  }

  /**
   * Validate the the key matches the hash of the value
   * @param {AbstractNode~request} request
   * @param {AbstractNode~response} response
   * @param {AbstractNode~next} next
   */
  validate(request, response, next) {
    let buffer, hash, [key, item] = request.params;

    try {
      buffer = Buffer.from(item.value, this.opts.valueEncoding);
      hash = createHash(this.opts.keyAlgorithm).update(buffer).digest('hex');

      assert(key === hash);
    } catch (err) {
      return next(new Error('Item failed validation check'));
    }

    next();
  }

}

/**
 * Registers a {@link module:kadence/contentaddress~ContentAddressPlugin} with
 * a {@link KademliaNode}
 * @param {object} [options]
 * @param {string} [options.keyAlgorithm="rmd160"] - Algorithm for hashing
 * @param {string} [options.valueEncoding="base64"] - Text encoding of value
 */
module.exports = function(options) {
  return function(node) {
    return new ContentAddressPlugin(node, options);
  }
};

module.exports.ContentAddressPlugin = ContentAddressPlugin;