'use strict' | |
// Load modules | |
; | |
var _typeof = function (obj) { | |
return obj && typeof Symbol !== 'undefined' && obj.constructor === Symbol ? 'symbol' : typeof obj; | |
}; | |
var Url = require('url'); | |
var Hoek = require('hoek'); | |
var Cryptiles = require('cryptiles'); | |
var Crypto = require('./crypto'); | |
var Utils = require('./utils'); | |
// Declare internals | |
var internals = {}; | |
// Generate an Authorization header for a given request | |
/* | |
uri: 'http://example.com/resource?a=b' or object from Url.parse() | |
method: HTTP verb (e.g. 'GET', 'POST') | |
options: { | |
// Required | |
credentials: { | |
id: 'dh37fgj492je', | |
key: 'aoijedoaijsdlaksjdl', | |
algorithm: 'sha256' // 'sha1', 'sha256' | |
}, | |
// Optional | |
ext: 'application-specific', // Application specific data sent via the ext attribute | |
timestamp: Date.now(), // A pre-calculated timestamp | |
nonce: '2334f34f', // A pre-generated nonce | |
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) | |
payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided) | |
contentType: 'application/json', // Payload content-type (ignored if hash provided) | |
hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash | |
app: '24s23423f34dx', // Oz application id | |
dlg: '234sz34tww3sd' // Oz delegated-by application id | |
} | |
*/ | |
exports.header = function (uri, method, options) { | |
var result = { | |
field: '', | |
artifacts: {} | |
}; | |
// Validate inputs | |
if (!uri || typeof uri !== 'string' && (typeof uri === 'undefined' ? 'undefined' : _typeof(uri)) !== 'object' || !method || typeof method !== 'string' || !options || (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object') { | |
result.err = 'Invalid argument type'; | |
return result; | |
} | |
// Application time | |
var timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); | |
// Validate credentials | |
var credentials = options.credentials; | |
if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { | |
result.err = 'Invalid credential object'; | |
return result; | |
} | |
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { | |
result.err = 'Unknown algorithm'; | |
return result; | |
} | |
// Parse URI | |
if (typeof uri === 'string') { | |
uri = Url.parse(uri); | |
} | |
// Calculate signature | |
var artifacts = { | |
ts: timestamp, | |
nonce: options.nonce || Cryptiles.randomString(6), | |
method: method, | |
resource: uri.pathname + (uri.search || ''), // Maintain trailing '?' | |
host: uri.hostname, | |
port: uri.port || (uri.protocol === 'http:' ? 80 : 443), | |
hash: options.hash, | |
ext: options.ext, | |
app: options.app, | |
dlg: options.dlg | |
}; | |
result.artifacts = artifacts; | |
// Calculate payload hash | |
if (!artifacts.hash && (options.payload || options.payload === '')) { | |
artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType); | |
} | |
var mac = Crypto.calculateMac('header', credentials, artifacts); | |
// Construct header | |
var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed | |
var header = 'Hawk id="' + credentials.id + '", ts="' + artifacts.ts + '", nonce="' + artifacts.nonce + (artifacts.hash ? '", hash="' + artifacts.hash : '') + (hasExt ? '", ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) : '') + '", mac="' + mac + '"'; | |
if (artifacts.app) { | |
header = header + ', app="' + artifacts.app + (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"'; | |
} | |
result.field = header; | |
return result; | |
}; | |
// Validate server response | |
/* | |
res: node's response object | |
artifacts: object received from header().artifacts | |
options: { | |
payload: optional payload received | |
required: specifies if a Server-Authorization header is required. Defaults to 'false' | |
} | |
*/ | |
exports.authenticate = function (res, credentials, artifacts, options) { | |
artifacts = Hoek.clone(artifacts); | |
options = options || {}; | |
if (res.headers['www-authenticate']) { | |
// Parse HTTP WWW-Authenticate header | |
var wwwAttributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']); | |
if (wwwAttributes instanceof Error) { | |
return false; | |
} | |
// Validate server timestamp (not used to update clock since it is done via the SNPT client) | |
if (wwwAttributes.ts) { | |
var tsm = Crypto.calculateTsMac(wwwAttributes.ts, credentials); | |
if (tsm !== wwwAttributes.tsm) { | |
return false; | |
} | |
} | |
} | |
// Parse HTTP Server-Authorization header | |
if (!res.headers['server-authorization'] && !options.required) { | |
return true; | |
} | |
var attributes = Utils.parseAuthorizationHeader(res.headers['server-authorization'], ['mac', 'ext', 'hash']); | |
if (attributes instanceof Error) { | |
return false; | |
} | |
artifacts.ext = attributes.ext; | |
artifacts.hash = attributes.hash; | |
var mac = Crypto.calculateMac('response', credentials, artifacts); | |
if (mac !== attributes.mac) { | |
return false; | |
} | |
if (!options.payload && options.payload !== '') { | |
return true; | |
} | |
if (!attributes.hash) { | |
return false; | |
} | |
var calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']); | |
return calculatedHash === attributes.hash; | |
}; | |
// Generate a bewit value for a given URI | |
/* | |
uri: 'http://example.com/resource?a=b' or object from Url.parse() | |
options: { | |
// Required | |
credentials: { | |
id: 'dh37fgj492je', | |
key: 'aoijedoaijsdlaksjdl', | |
algorithm: 'sha256' // 'sha1', 'sha256' | |
}, | |
ttlSec: 60 * 60, // TTL in seconds | |
// Optional | |
ext: 'application-specific', // Application specific data sent via the ext attribute | |
localtimeOffsetMsec: 400 // Time offset to sync with server time | |
}; | |
*/ | |
exports.getBewit = function (uri, options) { | |
// Validate inputs | |
if (!uri || typeof uri !== 'string' && (typeof uri === 'undefined' ? 'undefined' : _typeof(uri)) !== 'object' || !options || (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object' || !options.ttlSec) { | |
return ''; | |
} | |
options.ext = options.ext === null || options.ext === undefined ? '' : options.ext; // Zero is valid value | |
// Application time | |
var now = Utils.now(options.localtimeOffsetMsec); | |
// Validate credentials | |
var credentials = options.credentials; | |
if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { | |
return ''; | |
} | |
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { | |
return ''; | |
} | |
// Parse URI | |
if (typeof uri === 'string') { | |
uri = Url.parse(uri); | |
} | |
// Calculate signature | |
var exp = Math.floor(now / 1000) + options.ttlSec; | |
var mac = Crypto.calculateMac('bewit', credentials, { | |
ts: exp, | |
nonce: '', | |
method: 'GET', | |
resource: uri.pathname + (uri.search || ''), // Maintain trailing '?' | |
host: uri.hostname, | |
port: uri.port || (uri.protocol === 'http:' ? 80 : 443), | |
ext: options.ext | |
}); | |
// Construct bewit: id\exp\mac\ext | |
var bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + options.ext; | |
return Hoek.base64urlEncode(bewit); | |
}; | |
// Generate an authorization string for a message | |
/* | |
host: 'example.com', | |
port: 8000, | |
message: '{"some":"payload"}', // UTF-8 encoded string for body hash generation | |
options: { | |
// Required | |
credentials: { | |
id: 'dh37fgj492je', | |
key: 'aoijedoaijsdlaksjdl', | |
algorithm: 'sha256' // 'sha1', 'sha256' | |
}, | |
// Optional | |
timestamp: Date.now(), // A pre-calculated timestamp | |
nonce: '2334f34f', // A pre-generated nonce | |
localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided) | |
} | |
*/ | |
exports.message = function (host, port, message, options) { | |
// Validate inputs | |
if (!host || typeof host !== 'string' || !port || typeof port !== 'number' || message === null || message === undefined || typeof message !== 'string' || !options || (typeof options === 'undefined' ? 'undefined' : _typeof(options)) !== 'object') { | |
return null; | |
} | |
// Application time | |
var timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); | |
// Validate credentials | |
var credentials = options.credentials; | |
if (!credentials || !credentials.id || !credentials.key || !credentials.algorithm) { | |
// Invalid credential object | |
return null; | |
} | |
if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) { | |
return null; | |
} | |
// Calculate signature | |
var artifacts = { | |
ts: timestamp, | |
nonce: options.nonce || Cryptiles.randomString(6), | |
host: host, | |
port: port, | |
hash: Crypto.calculatePayloadHash(message, credentials.algorithm) | |
}; | |
// Construct authorization | |
var result = { | |
id: credentials.id, | |
ts: artifacts.ts, | |
nonce: artifacts.nonce, | |
hash: artifacts.hash, | |
mac: Crypto.calculateMac('message', credentials, artifacts) | |
}; | |
return result; | |
}; |