371 lines
10 KiB
JavaScript
Executable file
371 lines
10 KiB
JavaScript
Executable file
// 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 || Math.floor((Utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
|
|
|
|
// 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.hasOwnProperty('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="' + Utils.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 recieved 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 attributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']);
|
|
if (attributes instanceof Error) {
|
|
return false;
|
|
}
|
|
|
|
// Validate server timestamp (not used to update clock since it is done via the SNPT client)
|
|
|
|
if (attributes.ts) {
|
|
var tsm = Crypto.calculateTsMac(attributes.ts, credentials);
|
|
if (tsm !== attributes.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.hasOwnProperty('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
|
|
|
|
/*
|
|
* credentials is an object with the following keys: 'id, 'key', 'algorithm'.
|
|
* options is an object with the following optional keys: 'ext', 'localtimeOffsetMsec'
|
|
*/
|
|
/*
|
|
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 || 0);
|
|
|
|
// 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 Utils.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 || Math.floor((Utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
|
|
|
|
// 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;
|
|
};
|
|
|
|
|
|
|