client.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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 || Math.floor((Utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
  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.hasOwnProperty('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="' + Utils.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 recieved 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 attributes = Utils.parseAuthorizationHeader(res.headers['www-authenticate'], ['ts', 'tsm', 'error']);
  113. if (attributes 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 (attributes.ts) {
  118. var tsm = Crypto.calculateTsMac(attributes.ts, credentials);
  119. if (tsm !== attributes.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.hasOwnProperty('payload')) {
  140. return true;
  141. }
  142. if (!attributes.hash) {
  143. return false;
  144. }
  145. var calculatedHash = Crypto.calculatePayloadHash(options.payload, credentials.algorithm, res.headers['content-type']);
  146. return (calculatedHash === attributes.hash);
  147. };
  148. // Generate a bewit value for a given URI
  149. /*
  150. * credentials is an object with the following keys: 'id, 'key', 'algorithm'.
  151. * options is an object with the following optional keys: 'ext', 'localtimeOffsetMsec'
  152. */
  153. /*
  154. uri: 'http://example.com/resource?a=b' or object from Url.parse()
  155. options: {
  156. // Required
  157. credentials: {
  158. id: 'dh37fgj492je',
  159. key: 'aoijedoaijsdlaksjdl',
  160. algorithm: 'sha256' // 'sha1', 'sha256'
  161. },
  162. ttlSec: 60 * 60, // TTL in seconds
  163. // Optional
  164. ext: 'application-specific', // Application specific data sent via the ext attribute
  165. localtimeOffsetMsec: 400 // Time offset to sync with server time
  166. };
  167. */
  168. exports.getBewit = function (uri, options) {
  169. // Validate inputs
  170. if (!uri ||
  171. (typeof uri !== 'string' && typeof uri !== 'object') ||
  172. !options ||
  173. typeof options !== 'object' ||
  174. !options.ttlSec) {
  175. return '';
  176. }
  177. options.ext = (options.ext === null || options.ext === undefined ? '' : options.ext); // Zero is valid value
  178. // Application time
  179. var now = Utils.now() + (options.localtimeOffsetMsec || 0);
  180. // Validate credentials
  181. var credentials = options.credentials;
  182. if (!credentials ||
  183. !credentials.id ||
  184. !credentials.key ||
  185. !credentials.algorithm) {
  186. return '';
  187. }
  188. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  189. return '';
  190. }
  191. // Parse URI
  192. if (typeof uri === 'string') {
  193. uri = Url.parse(uri);
  194. }
  195. // Calculate signature
  196. var exp = Math.floor(now / 1000) + options.ttlSec;
  197. var mac = Crypto.calculateMac('bewit', credentials, {
  198. ts: exp,
  199. nonce: '',
  200. method: 'GET',
  201. resource: uri.pathname + (uri.search || ''), // Maintain trailing '?'
  202. host: uri.hostname,
  203. port: uri.port || (uri.protocol === 'http:' ? 80 : 443),
  204. ext: options.ext
  205. });
  206. // Construct bewit: id\exp\mac\ext
  207. var bewit = credentials.id + '\\' + exp + '\\' + mac + '\\' + options.ext;
  208. return Utils.base64urlEncode(bewit);
  209. };
  210. // Generate an authorization string for a message
  211. /*
  212. host: 'example.com',
  213. port: 8000,
  214. message: '{"some":"payload"}', // UTF-8 encoded string for body hash generation
  215. options: {
  216. // Required
  217. credentials: {
  218. id: 'dh37fgj492je',
  219. key: 'aoijedoaijsdlaksjdl',
  220. algorithm: 'sha256' // 'sha1', 'sha256'
  221. },
  222. // Optional
  223. timestamp: Date.now(), // A pre-calculated timestamp
  224. nonce: '2334f34f', // A pre-generated nonce
  225. localtimeOffsetMsec: 400, // Time offset to sync with server time (ignored if timestamp provided)
  226. }
  227. */
  228. exports.message = function (host, port, message, options) {
  229. // Validate inputs
  230. if (!host || typeof host !== 'string' ||
  231. !port || typeof port !== 'number' ||
  232. message === null || message === undefined || typeof message !== 'string' ||
  233. !options || typeof options !== 'object') {
  234. return null;
  235. }
  236. // Application time
  237. var timestamp = options.timestamp || Math.floor((Utils.now() + (options.localtimeOffsetMsec || 0)) / 1000)
  238. // Validate credentials
  239. var credentials = options.credentials;
  240. if (!credentials ||
  241. !credentials.id ||
  242. !credentials.key ||
  243. !credentials.algorithm) {
  244. // Invalid credential object
  245. return null;
  246. }
  247. if (Crypto.algorithms.indexOf(credentials.algorithm) === -1) {
  248. return null;
  249. }
  250. // Calculate signature
  251. var artifacts = {
  252. ts: timestamp,
  253. nonce: options.nonce || Cryptiles.randomString(6),
  254. host: host,
  255. port: port,
  256. hash: Crypto.calculatePayloadHash(message, credentials.algorithm)
  257. };
  258. // Construct authorization
  259. var result = {
  260. id: credentials.id,
  261. ts: artifacts.ts,
  262. nonce: artifacts.nonce,
  263. hash: artifacts.hash,
  264. mac: Crypto.calculateMac('message', credentials, artifacts)
  265. };
  266. return result;
  267. };