You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
394 lines
11 KiB
394 lines
11 KiB
'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; |
|
}; |
|
|
|
|
|
|
|
|