senecajs/seneca-apikey

View on GitHub
apikey.js

Summary

Maintainability
A
0 mins
Test Coverage
/* Copyright (c) 2020 Richard Rodger and other contributors, MIT License. */
'use strict'

const Assert = require('assert')
const Util = require('util')
const Crypto = require('crypto')

const intern = (apikey.intern = make_intern())

module.exports = apikey

module.exports.errors = {}
module.exports.doc = require('./apikey-doc')

module.exports.defaults = {
  test: false,
  keysize: 32, // does not include tag
  tagsize: 8,

  // Balance security and speed
  rounds: 11,

  salt: {
    bytelen: 16,
    format: 'hex',
  },

  pepper: '',

  generate_salt: intern.generate_salt,
}

function apikey(options) {
  var seneca = this
  var ctx = intern.make_ctx({}, options)

  // TODO: generate owner id from db id
  // TODO: publish keys, accept keys

  seneca
    .fix('sys:apikey')
    .message('generate:key', intern.make_msg('generate_key', ctx))
    .message('verify:key', intern.make_msg('verify_key', ctx))
}

function make_intern() {
  return {
    make_msg: function (msg_fn, ctx) {
      Assert(msg_fn)
      Assert(ctx)

      return require('./lib/' + msg_fn)(ctx)
    },

    make_ctx: function (initial_ctx, options) {
      Assert(initial_ctx)
      Assert(options)

      return Object.assign(
        {
          options,
          intern,

          // Standard entity canons
          sys_apikey: 'sys/apikey',

          // Internal version, not encoded in key
          hash_version: '001',

          // Base64 padding length
          base64padlen: intern.calculate_base64padlen(options.keysize),
        },
        initial_ctx
      )
    },

    make_hash: async function (spec) {
      Assert(spec)

      var start = process.hrtime()

      spec.salt = spec.salt || spec.options.generate_salt(spec.options)

      var out = {
        ok: true,
        pass: intern.runhash({
          src: intern.make_key_src(spec),
          rounds: spec.options.rounds,
        }),
        salt: spec.salt,
      }

      var dur = process.hrtime(start)
      out.tn_gen = dur[0] * 1e9 + dur[1]

      return out
    },

    verify_hash: async function (spec) {
      Assert(spec)

      var start = process.hrtime()

      var hash = intern.runhash({
        src: intern.make_key_src(spec),
        rounds: spec.options.rounds,
      })

      var out = {
        ok: hash === spec.pass,
      }

      out.why = out.ok ? void 0 : 'no-match'

      var dur = process.hrtime(start)
      out.tn_vfy = dur[0] * 1e9 + dur[1]

      return out
    },

    runhash: function (spec) {
      var out = spec.src

      for (var i = 0; i < spec.rounds; i++) {
        var shasum = Crypto.createHash('sha512')
        shasum.update(out, 'utf8')
        out = shasum.digest('hex')
      }

      return out
    },

    make_key_src: function (spec) {
      var fullcore =
        '~' +
        spec.tag +
        '~' +
        spec.version +
        '~' +
        spec.core +
        '~' +
        spec.scope +
        '~' +
        spec.owner +
        '~'

      // NOTE: remain compatible with @seneca/user
      return spec.options.pepper + fullcore + spec.salt
    },

    make_key_id: function (owner, tag) {
      return owner + '~' + tag
    },

    generate_salt: function (options) {
      return Crypto.randomBytes(options.salt.bytelen).toString(
        options.salt.format
      )
    },

    // base64 padding is removed, string is URL-safed
    generate_core: async function (keysize, base64padlen) {
      var core = (await Util.promisify(Crypto.randomBytes)(keysize)).toString(
        'base64'
      )
      var out = core.substring(0, core.length - base64padlen)
      out = out.replace(/\+/g, '-')
      out = out.replace(/\//g, '_')
      return out
    },

    /*
    // base64 padding is added, string is URL-unsafed
    parse_core: function(core, base64padlen) {
      var out = null == core ? '' : core
      out = out.replace('-','+')
      out = out.replace('_','/')
      out = out + '=='.substring(base64padlen)
      return out
    },
    */

    // NOTE: https://stackoverflow.com/questions/13378815/base64-length-calculation/13378842
    calculate_base64padlen: function (keysize) {
      var base64len = (4 * keysize) / 3
      var padlen = Math.round(3 * (Math.ceil(base64len) - base64len))
      return padlen
    },
  }
}