-
Notifications
You must be signed in to change notification settings - Fork 749
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
429 additions
and
321 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,131 +1,168 @@ | ||
|
||
/** | ||
* @fileoverview SIP Digest Authentication | ||
* @fileoverview DigestAuthentication | ||
*/ | ||
|
||
/** | ||
* SIP Digest Authentication. | ||
* @augments JsSIP. | ||
* @function Digest Authentication | ||
* @param {JsSIP.UA} ua | ||
* @param {JsSIP.OutgoingRequest} request | ||
* @param {JsSIP.IncomingResponse} response | ||
*/ | ||
(function(JsSIP) { | ||
var DigestAuthentication, | ||
LOG_PREFIX = JsSIP.name() +' | '+ 'DIGEST AUTHENTICATION' +' | '; | ||
|
||
DigestAuthentication = function (ua, request, response) { | ||
var authenticate, realm, qop, nonce, opaque, | ||
username = ua.configuration.authorization_user, | ||
password = ua.configuration.password; | ||
DigestAuthentication = function(ua) { | ||
this.username = ua.configuration.authorization_user; | ||
this.password = ua.configuration.password; | ||
this.cnonce = null; | ||
this.nc = 0; | ||
this.ncHex = '00000000'; | ||
this.response = null; | ||
}; | ||
|
||
|
||
if(response.status_code === 401) { | ||
authenticate = response.parseHeader('www-authenticate'); | ||
/** | ||
* 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. | ||
* | ||
* @param {JsSIP.OutgoingRequest} request | ||
* @param {JsSIP.IncomingResponse} response | ||
*/ | ||
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') { | ||
console.warn(LOG_PREFIX + 'challenge with Digest algorithm different than "MD5", authentication aborted'); | ||
return false; | ||
} | ||
} else { | ||
authenticate = response.parseHeader('proxy-authenticate'); | ||
this.algorithm = 'MD5'; | ||
} | ||
|
||
realm = authenticate.realm.replace(/"/g,''); | ||
qop = authenticate.qop || null; | ||
nonce = authenticate.nonce.replace(/"/g,''); | ||
opaque = authenticate.opaque; | ||
|
||
this.password = password; | ||
this.method = request.method; | ||
if (! this.realm) { | ||
console.warn(LOG_PREFIX + 'challenge without Digest realm, authentication aborted'); | ||
return false; | ||
} | ||
|
||
this.username = username; | ||
this.realm = realm; | ||
this.nonce = nonce; | ||
this.uri = request.ruri; | ||
this.qop = qop; | ||
this.response = null; | ||
this.algorithm = "MD5"; | ||
this.opaque = opaque; | ||
this.cnonce = null; | ||
this.nc = 0; | ||
}; | ||
if (! this.nonce) { | ||
console.warn(LOG_PREFIX + 'challenge without Digest nonce, authentication aborted'); | ||
return false; | ||
} | ||
|
||
DigestAuthentication.prototype.authenticate = function(password) { | ||
var ha1, ha2; | ||
// '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. | ||
console.warn(LOG_PREFIX + 'challenge without Digest qop different than "auth" or "auth-int", authentication aborted'); | ||
return false; | ||
} | ||
} else { | ||
this.qop = null; | ||
} | ||
|
||
password = password || this.password; | ||
// Fill other attributes. | ||
|
||
this.method = request.method; | ||
this.uri = request.ruri; | ||
this.cnonce = JsSIP.Utils.createRandomToken(12); | ||
this.nc += 1; | ||
this.updateNcHex(); | ||
|
||
// nc-value = 8LHEX. Max value = 'FFFFFFFF' | ||
// nc-value = 8LHEX. Max value = 'FFFFFFFF'. | ||
if (this.nc === 4294967296) { | ||
console.log(LOG_PREFIX + 'maximum "nc" value has been reached, resetting "nc"'); | ||
this.nc = 1; | ||
this.ncHex = '00000001'; | ||
} | ||
|
||
// Calculate the Digest "response" value. | ||
this.calculateResponse(); | ||
|
||
return true; | ||
}; | ||
|
||
|
||
/** | ||
* Generate 'response' value. | ||
* @private | ||
*/ | ||
DigestAuthentication.prototype.calculateResponse = function() { | ||
var ha1, ha2; | ||
|
||
// HA1 = MD5(A1) = MD5(username:realm:password) | ||
ha1 = JsSIP.Utils.calculateMD5(this.username + ":" + this.realm + ":" + password); | ||
ha1 = JsSIP.Utils.calculateMD5(this.username + ":" + this.realm + ":" + this.password); | ||
|
||
if (this.qop === 'auth' || this.qop === null) { | ||
if (this.qop === 'auth') { | ||
// HA2 = MD5(A2) = MD5(method:digestURI) | ||
ha2 = JsSIP.Utils.calculateMD5(this.method + ":" + this.uri); | ||
// response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2) | ||
this.response = JsSIP.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 = JsSIP.Utils.calculateMD5(this.method + ":" + this.uri + ":" + JsSIP.Utils.calculateMD5(this.body ? this.body : "")); | ||
} | ||
|
||
if(this.qop === 'auth' || this.qop === 'auth-int') { | ||
// response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2) | ||
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.decimalToHex(this.nc) + ":" + this.cnonce + ":" + this.qop + ":" + ha2); | ||
} else { | ||
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + this.ncHex + ":" + this.cnonce + ":auth-int:" + ha2); | ||
|
||
} else if (this.qop === null) { | ||
// HA2 = MD5(A2) = MD5(method:digestURI) | ||
ha2 = JsSIP.Utils.calculateMD5(this.method + ":" + this.uri); | ||
// response = MD5(HA1:nonce:HA2) | ||
this.response = JsSIP.Utils.calculateMD5(ha1 + ":" + this.nonce + ":" + ha2); | ||
} | ||
|
||
return this.toString(); | ||
}; | ||
|
||
|
||
DigestAuthentication.prototype.update = function(response) { | ||
var authenticate, nonce; | ||
/** | ||
* Return the Proxy-Authorization or WWW-Authorization header value. | ||
*/ | ||
DigestAuthentication.prototype.toString = function() { | ||
var auth_params = []; | ||
|
||
if(response.status_code === 401) { | ||
authenticate = response.parseHeader('www-authenticate'); | ||
} else { | ||
authenticate = response.parseHeader('proxy-authenticate'); | ||
if (! this.response) { | ||
throw new Error('response field does not exist, cannot generate Authorization header'); | ||
} | ||
|
||
nonce = authenticate.nonce.replace(/"/g,''); | ||
|
||
if(nonce !== this.nonce) { | ||
this.nc = 0; | ||
this.nonce = nonce; | ||
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); | ||
} | ||
|
||
this.realm = authenticate.realm.replace(/"/g,''); | ||
this.qop = authenticate.qop || null; | ||
this.opaque = authenticate.opaque; | ||
}; | ||
|
||
|
||
DigestAuthentication.prototype.toString = function() { | ||
var authorization = 'Digest '; | ||
|
||
authorization += 'username="' + this.username + '",'; | ||
authorization += 'realm="' + this.realm + '",'; | ||
authorization += 'nonce="' + this.nonce + '",'; | ||
authorization += 'uri="' + this.uri + '",'; | ||
authorization += 'response="' + this.response + '",'; | ||
authorization += this.opaque ? 'opaque="' + this.opaque + '",': ''; | ||
authorization += this.qop ? 'qop=' + this.qop + ',' : ''; | ||
authorization += this.qop ? 'cnonce="' + this.cnonce + '",' : ''; | ||
authorization += this.qop ? 'nc=' + this.decimalToHex(this.nc) + ',': ''; | ||
authorization += 'algorithm=MD5'; | ||
|
||
return authorization; | ||
return 'Digest ' + auth_params.join(', '); | ||
}; | ||
|
||
|
||
DigestAuthentication.prototype.decimalToHex = function(decimal) { | ||
var hex = Number(decimal).toString(16); | ||
return '00000000'.substr(0, 8-hex.length) + hex; | ||
/** | ||
* Generate the 'nc' value as required by Digest in this.ncHex by reading this.nc. | ||
* @private | ||
*/ | ||
DigestAuthentication.prototype.updateNcHex = function() { | ||
var hex = Number(this.nc).toString(16); | ||
this.ncHex = '00000000'.substr(0, 8-hex.length) + hex; | ||
}; | ||
|
||
JsSIP.DigestAuthentication = DigestAuthentication; | ||
}(JsSIP)); | ||
}(JsSIP)); |
Oops, something went wrong.
87357de
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🍰