2
\$\begingroup\$

The following code should prove to offer sufficient security for public API so my main concern is about design flaws in process. It is meant to describe the process and eventually get included in client side of API.

Full code is available at github GitHub. As for time of publishing the question, it's in sync with code provided here.

Question

This prototype should provide minimal means for session-less communication with remote data provider in a way that data provider can be sure about:

  • identity of sender;
  • time of query;
  • immutability of query parameters;

package.json:

{
  "private": false,
  "name": "sign-data-proposal",
  "version": "0.1.0",
  "license": "MIT",
  "description": "Proposal for data signing in Piletilevi public API",
  "homepage": "https://github.com/Piletilevi/api/signData",
  "author": "Mihkel Putrinš <[email protected]>",
  "repository": {
    "type": "git",
    "url": "https://github.com/Piletilevi/api"
  },
  "bugs": {
    "email": "[email protected]",
    "url": "https://github.com/Piletilevi/api/issues"
  },
  "dependencies": {
    "nodeunit": "0.9.1"
  }
}

sign.js:

var crypto = require('crypto')

var EXPIRATION_SEC = 3

/*
 * Provided data gets signed with credentials
 * credentials = {
 *     "user": user,
 *     "key": key
 * }
 * Returns an object {
 *     "signedData": data_transformed_to_array,
 *     "user": user,
 *     "expire": "2016-03-28T12:56:07.278Z",
 *     "signature": signature
 * }
 */
function signData(credentials, data) {
    data = data || {}

    var signedData = []

    for (k in data) {
        if (data.hasOwnProperty(k)) {
            signedData.push({ key: k, val: data[k] })
        }
    }

    var expire = new Date()
    expire.setSeconds(expire.getSeconds() + EXPIRATION_SEC)
    expire = expire.toJSON()

    var buffStr = JSON.stringify({ user: credentials.user, expire: expire, signedData: signedData })
    policy = new Buffer(buffStr).toString('base64')
    var signature = crypto.createHmac('sha1', credentials.key).update(policy).digest('base64')

    return { signedData: signedData, user: credentials.user, expire: expire, signature: signature }
}

function validateData(credentials, data) {
    if (new Date(data.expire) < new Date()) {
        return 'Query expired: ' + data.expire + ' is less than ' + new Date().toJSON()
    }

    if (data.user !== credentials.user) {
        return 'User mismatch: "' + credentials.user + '" !== "' + data.user + '"'
    }

    var buffStr = JSON.stringify({ user: credentials.user, expire: data.expire, signedData: data.signedData })
    policy = new Buffer(buffStr).toString('base64')
    var signature = crypto.createHmac('sha1', credentials.key).update(policy).digest('base64')
    if (signature !== data.signature) {
        return 'Signature mismatch: "' + signature + '" !== "' + data.signature + '"'
    }

    return 'valid'
}

module.exports = {
    sign: signData,
    validate: validateData
}

test.sign.js:

var testCase  = require('nodeunit').testCase
var signer = require('./sign.js')

var sign = signer.sign
var validate = signer.validate

var data1 = {concertId : 1,
    showId : '',
    startDate : '2016-01-08',
    endDate : '2016-01-18',
    ownerId : '',
    promoterId : '',
    sellStartDate : '',
    sellEndDate : '',
  }
var user = 'BackOffice Server'
var key = 'secret known to both parties'
var credentials = { user:user, key:key }
var credentials_user = { user:'wrong user', key:key }
var credentials_key = { user:user, key:'wrong key' }

var signedData = sign(credentials, data1)
console.log(signedData)

module.exports = testCase({
    "Test 0.1": function(test) {
        test.ok(true)
        test.done()
    },

    "TC 1 - Success scenarios": testCase({
        "Test 1.1 - With data": function(test) {
            var validated = validate(credentials, signedData)
            test.ok(validated === 'valid')
            test.done()
        },
        "Test 1.2 - No data": function(test) {
            var signedData = sign(credentials)
            var validated = validate(credentials, signedData)
            test.ok(validated === 'valid')
            test.done()
        }
    }),

    "TC 2 - Failure scenarios": testCase({
        "TC 2.1": testCase({
            "Test 2.1.1 - Wrong key": function(test) {
                var validated = validate(credentials_key, signedData)
                test.ok(validated !== 'valid')
                test.done()
            },
            "Test 2.1.2 - Wrong user": function(test) {
                var validated = validate(credentials_user, signedData)
                test.ok(validated !== 'valid')
                test.done()
            }
        }),
        "Test 2.2 - Timing out": function(test) {
            console.log("Testing timeout in " + (new Date(signedData.expire) - new Date()) / 1000 + ' seconds.')
            setTimeout(function () {
                var validated = validate(credentials, signedData)
                test.ok(validated !== 'valid')
                test.done()
            }, new Date(signedData.expire) - new Date())
        }
    })
})
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.