client.js 11 KB

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