client.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369
  1. // Load modules
  2. var Url = require('url');
  3. var Hoek = require('hoek');
  4. var Cryptiles = require('cryptiles');
  5. var Crypto = require('./crypto');
  6. var Utils = require('./utils');
  7. // Declare internals
  8. var internals = {};
  9. // Generate an Authorization header for a given request
  10. /*
  11. uri: 'http://example.com/resource?a=b' or object from Url.parse()
  12. method: HTTP verb (e.g. 'GET', 'POST')
  13. options: {
  14. // Required
  15. credentials: {
  16. id: 'dh37fgj492je',
  17. key: 'aoijedoaijsdlaksjdl',
  18. algorithm: 'sha256' // 'sha1', 'sha256'
  19. },
  20. // Optional
  21. ext: 'application-specific', // Application specific data sent via the ext attribute
  22. timestamp: Date.now(), // A pre-calculated timestamp
  23. nonce: '2334f34f', // A pre-generated nonce
  24. localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
  25. payload: '{"some":"payload"}', // UTF-8 encoded string for body hash generation (ignored if hash provided)
  26. contentType: 'application/json', // Payload content-type (ignored if hash provided)
  27. hash: 'U4MKKSmiVxk37JCCrAVIjV=', // Pre-calculated payload hash
  28. app: '24s23423f34dx', // Oz application id
  29. dlg: '234sz34tww3sd' // Oz delegated-by application id
  30. }
  31. */
  32. exports.header = function (uri, method, options) {
  33. var result = {
  34. field: '',
  35. artifacts: {}
  36. };
  37. // Validate inputs
  38. if (!uri || (typeof uri !== 'string' && typeof uri !== 'object') ||
  39. !method || typeof method !== 'string' ||
  40. !options || typeof options !== 'object') {
  41. result.err = 'Invalid argument type';
  42. return result;
  43. }
  44. // Application time
  45. var timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec);
  46. // Validate credentials
  47. var credentials = options.credentials;
  48. if (!credentials ||
  49. !credentials.id ||
  50. !credentials.key ||
  51. !credentials.algorithm) {
  52. result.err = 'Invalid credential object';
  53. return result;
  54. }
  55. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  56. result.err = 'Unknown algorithm';
  57. return result;
  58. }
  59. // Parse URI
  60. if (typeof uri === 'string') {
  61. uri = Url.parse(uri);
  62. }
  63. // Calculate signature
  64. var artifacts = {
  65. ts: timestamp,
  66. nonce: options.nonce || Cryptiles.randomString(6),
  67. method: method,
  68. resource: uri.pathname + (uri.search || ''), // Maintain trailing '?'
  69. host: uri.hostname,
  70. port: uri.port || (uri.protocol === 'http:' ? 80 : 443),
  71. hash: options.hash,
  72. ext: options.ext,
  73. app: options.app,
  74. dlg: options.dlg
  75. };
  76. result.artifacts = artifacts;
  77. // Calculate payload hash
  78. if (!artifacts.hash &&
  79. (options.payload || options.payload === '')) {
  80. artifacts.hash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, options.contentType);
  81. }
  82. var mac = Crypto.calculateMac('header', credentials, artifacts);
  83. // Construct header
  84. var hasExt = artifacts.ext !== null && artifacts.ext !== undefined && artifacts.ext !== ''; // Other falsey values allowed
  85. var header = 'Hawk id="' + credentials.id +
  86. '", ts="' + artifacts.ts +
  87. '", nonce="' + artifacts.nonce +
  88. (artifacts.hash ? '", hash="' + artifacts.hash : '') +
  89. (hasExt ? '", ext="' + Hoek.escapeHeaderAttribute(artifacts.ext) : '') +
  90. '", mac="' + mac + '"';
  91. if (artifacts.app) {
  92. header += ', app="' + artifacts.app +
  93. (artifacts.dlg ? '", dlg="' + artifacts.dlg : '') + '"';
  94. }
  95. result.field = header;
  96. return result;
  97. };
  98. // Validate server response
  99. /*
  100. res: node's response object
  101. artifacts: object received from header().artifacts
  102. options: {
  103. payload: optional payload received
  104. required: specifies if a Server-Authorization header is required. Defaults to 'false'
  105. }
  106. */
  107. exports.authenticate = function (res, credentials, artifacts, options) {
  108. artifacts = Hoek.clone(artifacts);
  109. options = options || {};
  110. if (res.headers['www-authenticate']) {
  111. // Parse HTTP WWW-Authenticate header
  112. var wwwAttributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']);
  113. if (wwwAttributes instanceof Error) {
  114. return false;
  115. }
  116. // Validate server timestamp (not used to update clock since it is done via the SNPT client)
  117. if (wwwAttributes.ts) {
  118. var tsm = Crypto.calculateTsMac(wwwAttributes.ts, credentials);
  119. if (tsm !== wwwAttributes.tsm) {
  120. return false;
  121. }
  122. }
  123. }
  124. // Parse HTTP Server-Authorization header
  125. if (!res.headers['server-authorization'] &&
  126. !options.required) {
  127. return true;
  128. }
  129. var attributes = Utils.parseAuthorizationHeader(res.headers['server-authorization'], ['mac', 'ext', 'hash']);
  130. if (attributes instanceof Error) {
  131. return false;
  132. }
  133. artifacts.ext = attributes.ext;
  134. artifacts.hash = attributes.hash;
  135. var mac = Crypto.calculateMac('response', credentials, artifacts);
  136. if (mac !== attributes.mac) {
  137. return false;
  138. }
  139. if (!options.payload &&
  140. options.payload !== '') {
  141. return true;
  142. }
  143. if (!attributes.hash) {
  144. return false;
  145. }
  146. var calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']);
  147. return (calculatedHash === attributes.hash);
  148. };
  149. // Generate a bewit value for a given URI
  150. /*
  151. uri: 'http://example.com/resource?a=b' or object from Url.parse()
  152. options: {
  153. // Required
  154. credentials: {
  155. id: 'dh37fgj492je',
  156. key: 'aoijedoaijsdlaksjdl',
  157. algorithm: 'sha256' // 'sha1', 'sha256'
  158. },
  159. ttlSec: 60 * 60, // TTL in seconds
  160. // Optional
  161. ext: 'application-specific', // Application specific data sent via the ext attribute
  162. localtimeOffsetMsec: 400 // Time offset to sync with server time
  163. };
  164. */
  165. exports.getBewit = function (uri, options) {
  166. // Validate inputs
  167. if (!uri ||
  168. (typeof uri !== 'string' && typeof uri !== 'object') ||
  169. !options ||
  170. typeof options !== 'object' ||
  171. !options.ttlSec) {
  172. return '';
  173. }
  174. options.ext = (options.ext === null || options.ext === undefined ? '' : options.ext); // Zero is valid value
  175. // Application time
  176. var now = Utils.now(options.localtimeOffsetMsec);
  177. // Validate credentials
  178. var credentials = options.credentials;
  179. if (!credentials ||
  180. !credentials.id ||
  181. !credentials.key ||
  182. !credentials.algorithm) {
  183. return '';
  184. }
  185. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  186. return '';
  187. }
  188. // Parse URI
  189. if (typeof uri === 'string') {
  190. uri = Url.parse(uri);
  191. }
  192. // Calculate signature
  193. var exp = Math.floor(now / 1000) + options.ttlSec;
  194. var mac = Crypto.calculateMac('bewit', credentials, {
  195. ts: exp,
  196. nonce: '',
  197. method: 'GET',
  198. resource: uri.pathname + (uri.search || ''), // Maintain trailing '?'
  199. host: uri.hostname,
  200. port: uri.port || (uri.protocol === 'http:' ? 80 : 443),
  201. ext: options.ext
  202. });
  203. // Construct bewit: id\exp\mac\ext
  204. var bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + options.ext;
  205. return Hoek.base64urlEncode(bewit);
  206. };
  207. // Generate an authorization string for a message
  208. /*
  209. host: 'example.com',
  210. port: 8000,
  211. message: '{"some":"payload"}', // UTF-8 encoded string for body hash generation
  212. options: {
  213. // Required
  214. credentials: {
  215. id: 'dh37fgj492je',
  216. key: 'aoijedoaijsdlaksjdl',
  217. algorithm: 'sha256' // 'sha1', 'sha256'
  218. },
  219. // Optional
  220. timestamp: Date.now(), // A pre-calculated timestamp
  221. nonce: '2334f34f', // A pre-generated nonce
  222. localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
  223. }
  224. */
  225. exports.message = function (host, port, message, options) {
  226. // Validate inputs
  227. if (!host || typeof host !== 'string' ||
  228. !port || typeof port !== 'number' ||
  229. message === null || message === undefined || typeof message !== 'string' ||
  230. !options || typeof options !== 'object') {
  231. return null;
  232. }
  233. // Application time
  234. var timestamp = options.timestamp || Utils.nowSecs(options.localtimeOffsetMsec);
  235. // Validate credentials
  236. var credentials = options.credentials;
  237. if (!credentials ||
  238. !credentials.id ||
  239. !credentials.key ||
  240. !credentials.algorithm) {
  241. // Invalid credential object
  242. return null;
  243. }
  244. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  245. return null;
  246. }
  247. // Calculate signature
  248. var artifacts = {
  249. ts: timestamp,
  250. nonce: options.nonce || Cryptiles.randomString(6),
  251. host: host,
  252. port: port,
  253. hash: Crypto.calculatePayloadHash(message, credentials.algorithm)
  254. };
  255. // Construct authorization
  256. var result = {
  257. id: credentials.id,
  258. ts: artifacts.ts,
  259. nonce: artifacts.nonce,
  260. hash: artifacts.hash,
  261. mac: Crypto.calculateMac('message', credentials, artifacts)
  262. };
  263. return result;
  264. };