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.
		
		
		
		
		
			
		
			
				
					370 lines
				
				10 KiB
			
		
		
			
		
	
	
					370 lines
				
				10 KiB
			| 
											3 years ago
										 | // 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; | ||
|  | }; | ||
|  | 
 | ||
|  | 
 | ||
|  | 
 |