// Load modules 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 !== 'object') || !method || typeof method !== 'string' || !options || 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 += ', 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 !== 'object') || !options || 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 !== '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; };