Sin descripción
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.

digest.js 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. var crypto = require('crypto');
  2. var util = require('util');
  3. var stringifyUri = require('./sip').stringifyUri;
  4. function unq(a) {
  5. if (a && a[0] === '"' && a[a.length - 1] === '"')
  6. return a.substr(1, a.length - 2);
  7. return a;
  8. }
  9. function q(a) {
  10. if (typeof a === 'string' && a[0] !== '"')
  11. return ['"', a, '"'].join('');
  12. return a;
  13. }
  14. function lowercase(a) {
  15. if (typeof a === 'string')
  16. return a.toLowerCase();
  17. return a;
  18. }
  19. function kd() {
  20. var hash = crypto.createHash('md5');
  21. var a = Array.prototype.join.call(arguments, ':');
  22. hash.update(a);
  23. return hash.digest('hex');
  24. }
  25. exports.kd = kd;
  26. function rbytes() {
  27. return kd(Math.random().toString(), Math.random().toString());
  28. }
  29. function calculateUserRealmPasswordHash(user, realm, password) {
  30. return kd(unq(user), unq(realm), unq(password));
  31. }
  32. exports.calculateUserRealmPasswordHash = calculateUserRealmPasswordHash;
  33. function calculateHA1(ctx) {
  34. var userhash = ctx.userhash || calculateUserRealmPasswordHash(ctx.user, ctx.realm, ctx.password);
  35. if (ctx.algorithm === 'md5-sess') return kd(userhash, ctx.nonce, ctx.cnonce);
  36. return userhash;
  37. }
  38. exports.calculateHA1 = calculateHA1;
  39. function calculateDigest(ctx) {
  40. switch (ctx.qop) {
  41. case 'auth-int':
  42. return kd(ctx.ha1, ctx.nonce, ctx.nc, ctx.cnonce, ctx.qop, kd(ctx.method, ctx.uri, kd(ctx.entity)));
  43. case 'auth':
  44. return kd(ctx.ha1, ctx.nonce, ctx.nc, ctx.cnonce, ctx.qop, kd(ctx.method, ctx.uri));
  45. }
  46. return kd(ctx.ha1, ctx.nonce, kd(ctx.method, ctx.uri));
  47. }
  48. exports.calculateDigest = calculateDigest;
  49. var nonceSalt = rbytes();
  50. function generateNonce(tag, timestamp) {
  51. var ts = (timestamp || new Date()).toISOString();
  52. return new Buffer.from([ts, kd(ts, tag, nonceSalt)].join(';'), 'ascii').toString('base64');
  53. }
  54. exports.generateNonce = generateNonce;
  55. function extractNonceTimestamp(nonce, tag) {
  56. var v = new Buffer.from(nonce, 'base64').toString('ascii').split(';');
  57. if (v.length != 2)
  58. return;
  59. var ts = new Date(v[0]);
  60. return generateNonce(tag, ts) === nonce && ts;
  61. }
  62. exports.extractNonceTimestamp = extractNonceTimestamp;
  63. function numberTo8Hex(n) {
  64. n = n.toString(16);
  65. return '00000000'.substr(n.length) + n;
  66. }
  67. function findDigestRealm(headers, realm) {
  68. if (!realm) return headers && headers[0];
  69. return headers && headers.filter(function (x) { return x.scheme.toLowerCase() === 'digest' && unq(x.realm) === realm; })[0];
  70. }
  71. function selectQop(challenge, preference) {
  72. if (!challenge)
  73. return;
  74. challenge = unq(challenge).split(',');
  75. if (!preference)
  76. return challenge[0];
  77. if (typeof (preference) === 'string')
  78. preference = preference.split(',');
  79. for (var i = 0; i !== preference.length; ++i)
  80. for (var j = 0; j !== challenge.length; ++j)
  81. if (challenge[j] === preference[i])
  82. return challenge[j];
  83. throw new Error('failed to negotiate protection quality');
  84. }
  85. exports.challenge = function (ctx, rs) {
  86. ctx.proxy = rs.status === 407;
  87. ctx.nonce = ctx.cnonce || rbytes();
  88. ctx.nc = 0;
  89. ctx.qop = ctx.qop || 'auth,auth-int';
  90. ctx.algorithm = ctx.algorithm || 'md5';
  91. var hname = ctx.proxy ? 'proxy-authenticate' : 'www-authenticate';
  92. (rs.headers[hname] || (rs.headers[hname] = [])).push(
  93. {
  94. scheme: 'Digest',
  95. realm: q(ctx.realm),
  96. //qop: q(ctx.qop),
  97. //algorithm: ctx.algorithm,
  98. nonce: q(ctx.nonce),
  99. //opaque: q(ctx.opaque)
  100. }
  101. );
  102. return rs;
  103. }
  104. exports.authenticateRequest = function (ctx, rq, creds) {
  105. var response = findDigestRealm(rq.headers[ctx.proxy ? 'proxy-authorization' : 'authorization'], ctx.realm);
  106. if (!response) return false;
  107. var cnonce = unq(response.cnonce);
  108. var uri = unq(response.uri);
  109. var qop = unq(lowercase(response.qop));
  110. ctx.nc = (ctx.nc || 0) + 1;
  111. if (!ctx.ha1) {
  112. ctx.userhash = creds.hash || calculateUserRealmPasswordHash(response.username || creds.user, ctx.realm, creds.password);
  113. ctx.ha1 = ctx.userhash;
  114. if (ctx.algorithm === 'md5-sess')
  115. ctx.ha1 = kd(ctx.userhash, ctx.nonce, cnonce);
  116. }
  117. var digest = calculateDigest({ ha1: ctx.ha1, method: rq.method, nonce: ctx.nonce, nc: numberTo8Hex(ctx.nc), cnonce: cnonce, qop: qop, uri: uri, entity: rq.content });
  118. if (digest === unq(response.response)) {
  119. ctx.cnonce = cnonce;
  120. ctx.uri = uri;
  121. ctx.qop = qop;
  122. return true;
  123. }
  124. return false;
  125. }
  126. exports.signResponse = function (ctx, rs) {
  127. var nc = numberTo8Hex(ctx.nc);
  128. rs.headers['authentication-info'] = {
  129. qop: ctx.qop,
  130. cnonce: q(ctx.cnonce),
  131. nc: nc,
  132. rspauth: q(calculateDigest({ ha1: ctx.ha1, method: '', nonce: ctx.nonce, nc: nc, cnonce: ctx.cnonce, qop: ctx.qop, uri: ctx.uri, entity: rs.content }))
  133. };
  134. return rs;
  135. }
  136. function initClientContext(ctx, rs, creds) {
  137. var challenge;
  138. if (rs.status === 407) {
  139. ctx.proxy = true;
  140. challenge = findDigestRealm(rs.headers['proxy-authenticate'], creds.realm);
  141. }
  142. else
  143. challenge = findDigestRealm(rs.headers['www-authenticate'], creds.realm);
  144. if (ctx.nonce !== unq(challenge.nonce)) {
  145. ctx.nonce = unq(challenge.nonce);
  146. ctx.algorithm = unq(lowercase(challenge.algorithm));
  147. ctx.qop = selectQop(lowercase(challenge.qop), ctx.qop);
  148. if (ctx.qop) {
  149. ctx.nc = 0;
  150. ctx.cnonce = rbytes();
  151. }
  152. ctx.realm = unq(challenge.realm);
  153. ctx.user = creds.user;
  154. ctx.userhash = creds.hash || calculateUserRealmPasswordHash(creds.user, ctx.realm, creds.password);
  155. ctx.ha1 = ctx.userhash;
  156. if (ctx.algorithm === 'md5-sess')
  157. ctx.ha1 = kd(ctx.ha1, ctx.nonce, ctx.cnonce);
  158. ctx.domain = unq(challenge.domain);
  159. }
  160. ctx.opaque = unq(challenge.opaque);
  161. }
  162. exports.signRequest = function (ctx, rq, rs, creds) {
  163. ctx = ctx || {};
  164. if (rs)
  165. initClientContext(ctx, rs, creds);
  166. var nc = ctx.nc !== undefined ? numberTo8Hex(++ctx.nc) : undefined;
  167. ctx.uri = stringifyUri(rq.uri);
  168. var signature = {
  169. scheme: 'Digest',
  170. realm: q(ctx.realm),
  171. username: q(ctx.user),
  172. nonce: q(ctx.nonce),
  173. uri: q(ctx.uri),
  174. nc: nc,
  175. algorithm: ctx.algorithm,
  176. cnonce: q(ctx.cnonce),
  177. qop: ctx.qop,
  178. opaque: q(ctx.opaque),
  179. response: q(calculateDigest({ ha1: ctx.ha1, method: rq.method, nonce: ctx.nonce, nc: nc, cnonce: ctx.cnonce, qop: ctx.qop, uri: ctx.uri, entity: rq.content }))
  180. };
  181. var hname = ctx.proxy ? 'proxy-authorization' : 'authorization';
  182. rq.headers[hname] = (rq.headers[hname] || []).filter(function (x) { return unq(x.realm) !== ctx.realm; });
  183. rq.headers[hname].push(signature);
  184. return ctx.qop ? ctx : null;
  185. }
  186. exports.authenticateResponse = function (ctx, rs) {
  187. var signature = rs.headers[ctx.proxy ? 'proxy-authentication-info' : 'authentication-info'];
  188. if (!signature) return undefined;
  189. var digest = calculateDigest({ ha1: ctx.ha1, method: '', nonce: ctx.nonce, nc: numberTo8Hex(ctx.nc), cnonce: ctx.cnonce, qop: ctx.qop, uri: ctx.uri, enity: rs.content });
  190. if (digest === unq(signature.rspauth)) {
  191. var nextnonce = unq(signature.nextnonce);
  192. if (nextnonce && nextnonce !== ctx.nonce) {
  193. ctx.nonce = nextnonce;
  194. ctx.nc = 0;
  195. if (ctx.algorithm === 'md5-sess')
  196. ctx.ha1 = kd(ctx.userhash, ctx.nonce, ctx.cnonce);
  197. }
  198. return true;
  199. }
  200. return false;
  201. }