| 'use strict'; |
| |
| // Load modules |
| |
| const Url = require('url'); |
| const Hoek = require('hoek'); |
| const Cryptiles = require('cryptiles'); |
| const Crypto = require('./crypto'); |
| const Utils = require('./utils'); |
| |
| |
| // Declare internals |
| |
| const 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() / 1000, // A pre-calculated timestamp in seconds |
| 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) { |
| |
| const 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 |
| |
| const timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); |
| |
| // Validate credentials |
| |
| const 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 |
| |
| const artifacts = { |
| ts: timestamp, |
| nonce: options.nonce || Cryptiles.randomString(6), |
| 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); |
| } |
| |
| const mac = Crypto.calculateMac('header', credentials, artifacts); |
| |
| // Construct header |
| |
| const hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed |
| let 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, callback) { |
| |
| artifacts = Hoek.clone(artifacts); |
| options = options || {}; |
| |
| let wwwAttributes = null; |
| let serverAuthAttributes = null; |
| |
| const finalize = function (err) { |
| |
| if (callback) { |
| const headers = { |
| 'www-authenticate': wwwAttributes, |
| 'server-authorization': serverAuthAttributes |
| }; |
| |
| return callback(err, headers); |
| } |
| |
| return !err; |
| }; |
| |
| if (res.headers['www-authenticate']) { |
| |
| // Parse HTTP WWW-Authenticate header |
| |
| wwwAttributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']); |
| if (wwwAttributes instanceof Error) { |
| wwwAttributes = null; |
| return finalize(new Error('Invalid WWW-Authenticate header')); |
| } |
| |
| // Validate server timestamp (not used to update clock since it is done via the SNPT client) |
| |
| if (wwwAttributes.ts) { |
| const tsm = Crypto.calculateTsMac(wwwAttributes.ts, credentials); |
| if (tsm !== wwwAttributes.tsm) { |
| return finalize(new Error('Invalid server timestamp hash')); |
| } |
| } |
| } |
| |
| // Parse HTTP Server-Authorization header |
| |
| if (!res.headers['server-authorization'] && |
| !options.required) { |
| |
| return finalize(); |
| } |
| |
| serverAuthAttributes = Utils.parseAuthorizationHeader(res.headers['server-authorization'], ['mac', 'ext', 'hash']); |
| if (serverAuthAttributes instanceof Error) { |
| serverAuthAttributes = null; |
| return finalize(new Error('Invalid Server-Authorization header')); |
| } |
| |
| artifacts.ext = serverAuthAttributes.ext; |
| artifacts.hash = serverAuthAttributes.hash; |
| |
| const mac = Crypto.calculateMac('response', credentials, artifacts); |
| if (mac !== serverAuthAttributes.mac) { |
| return finalize(new Error('Bad response mac')); |
| } |
| |
| if (!options.payload && |
| options.payload !== '') { |
| |
| return finalize(); |
| } |
| |
| if (!serverAuthAttributes.hash) { |
| return finalize(new Error('Missing response hash attribute')); |
| } |
| |
| const calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']); |
| if (calculatedHash !== serverAuthAttributes.hash) { |
| return finalize(new Error('Bad response payload mac')); |
| } |
| |
| return finalize(); |
| }; |
| |
| |
| // 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 |
| |
| const now = Utils.now(options.localtimeOffsetMsec); |
| |
| // Validate credentials |
| |
| const 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 |
| |
| const exp = Math.floor(now / 1000) + options.ttlSec; |
| const 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 |
| |
| const 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() / 1000, // A pre-calculated timestamp in seconds |
| 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 |
| |
| const timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec); |
| |
| // Validate credentials |
| |
| const 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 |
| |
| const artifacts = { |
| ts: timestamp, |
| nonce: options.nonce || Cryptiles.randomString(6), |
| host, |
| port, |
| hash: Crypto.calculatePayloadHash(message, credentials.algorithm) |
| }; |
| |
| // Construct authorization |
| |
| const result = { |
| id: credentials.id, |
| ts: artifacts.ts, |
| nonce: artifacts.nonce, |
| hash: artifacts.hash, |
| mac: Crypto.calculateMac('message', credentials, artifacts) |
| }; |
| |
| return result; |
| }; |
| |
| |
| |