module.exports = DigestAuthentication; /** * Dependencies. */ var debug = require('debug')('JsSIP:DigestAuthentication'); var Utils = require('./Utils'); function DigestAuthentication(ua) { this.username = ua.configuration.authorization_user; this.password = ua.configuration.password; this.cnonce = null; this.nc = 0; this.ncHex = '00000000'; this.response = null; } /** * Performs Digest authentication given a SIP request and the challenge * received in a response to that request. * Returns true if credentials were successfully generated, false otherwise. */ DigestAuthentication.prototype.authenticate = function(request, challenge) { // Inspect and validate the challenge. this.algorithm = challenge.algorithm; this.realm = challenge.realm; this.nonce = challenge.nonce; this.opaque = challenge.opaque; this.stale = challenge.stale; if (this.algorithm) { if (this.algorithm !== 'MD5') { debug('challenge with Digest algorithm different than "MD5", authentication aborted'); return false; } } else { this.algorithm = 'MD5'; } if (! this.realm) { debug('challenge without Digest realm, authentication aborted'); return false; } if (! this.nonce) { debug('challenge without Digest nonce, authentication aborted'); return false; } // 'qop' can contain a list of values (Array). Let's choose just one. if (challenge.qop) { if (challenge.qop.indexOf('auth') > -1) { this.qop = 'auth'; } else if (challenge.qop.indexOf('auth-int') > -1) { this.qop = 'auth-int'; } else { // Otherwise 'qop' is present but does not contain 'auth' or 'auth-int', so abort here. debug('challenge without Digest qop different than "auth" or "auth-int", authentication aborted'); return false; } } else { this.qop = null; } // Fill other attributes. this.method = request.method; this.uri = request.ruri; this.cnonce = Utils.createRandomToken(12); this.nc += 1; this.updateNcHex(); // nc-value = 8LHEX. Max value = 'FFFFFFFF'. if (this.nc === 4294967296) { this.nc = 1; this.ncHex = '00000001'; } // Calculate the Digest "response" value. this.calculateResponse(); return true; }; /** * Generate Digest 'response' value. */ DigestAuthentication.prototype.calculateResponse = function() { var ha1, ha2; // HA1 = MD5(A1) = MD5(username:realm:password) ha1 = Utils.calculateMD5(this.username + ":" + this.realm + ":" + this.password); if (this.qop === 'auth') { // HA2 = MD5(A2) = MD5(method:digestURI) ha2 = Utils.calculateMD5(this.method + ":" + this.uri); // response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2) this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth:" + ha2); } else if (this.qop === 'auth-int') { // HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody)) ha2 = Utils.calculateMD5(this.method + ":" + this.uri + ":" + Utils.calculateMD5(this.body ? this.body : "")); // response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2) this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth-int:" + ha2); } else if (this.qop === null) { // HA2 = MD5(A2) = MD5(method:digestURI) ha2 = Utils.calculateMD5(this.method + ":" + this.uri); // response = MD5(HA1:nonce:HA2) this.response = Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + ha2); } }; /** * Return the Proxy-Authorization or WWW-Authorization header value. */ DigestAuthentication.prototype.toString = function() { var auth_params = []; if (! this.response) { throw new Error('response field does not exist, cannot generate Authorization header'); } auth_params.push('algorithm=' + this.algorithm); auth_params.push('username="' + this.username + '"'); auth_params.push('realm="' + this.realm + '"'); auth_params.push('nonce="' + this.nonce + '"'); auth_params.push('uri="' + this.uri + '"'); auth_params.push('response="' + this.response + '"'); if (this.opaque) { auth_params.push('opaque="' + this.opaque + '"'); } if (this.qop) { auth_params.push('qop=' + this.qop); auth_params.push('cnonce="' + this.cnonce + '"'); auth_params.push('nc=' + this.ncHex); } return 'Digest ' + auth_params.join(', '); }; /** * Generate the 'nc' value as required by Digest in this.ncHex by reading this.nc. */ DigestAuthentication.prototype.updateNcHex = function() { var hex = Number(this.nc).toString(16); this.ncHex = '00000000'.substr(0, 8-hex.length) + hex; };