This repository has been archived by the owner on Apr 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 169
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
10daa13
commit 66dd8f9
Showing
2 changed files
with
342 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,340 @@ | ||
'use strict' | ||
|
||
// Load modules | ||
|
||
; | ||
|
||
function _typeof(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; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters