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
						
					
					
				| // 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;
 | |
| };
 | |
| 
 | |
| 
 | |
| 
 |