Skip to content

Commit

Permalink
Implement credential reuse
Browse files Browse the repository at this point in the history
  • Loading branch information
jmillan committed Nov 23, 2012
2 parents 351ca06 + 45a3b35 commit 85ca354
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 63 deletions.
117 changes: 71 additions & 46 deletions src/DigestAuthentication.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,69 +10,94 @@
* @param {JsSIP.UA} ua
* @param {JsSIP.OutgoingRequest} request
* @param {JsSIP.IncomingResponse} response
* @returns {String}
*/
JsSIP.DigestAuthentication = function (ua, request, response) {
var authenticate, ha1, ha2, param,
authorization = {},
digest = '',
nc = "00000001",
cnonce = Math.random().toString(36).substr(2, 12),
credentials = {
username: ua.configuration.authorization_user,
password: ua.configuration.password
};
var authenticate, credentials, realm, qop, nonce, opaque,
username = ua.configuration.authorization_user,
password = ua.configuration.password;

if(response.status_code === 401) {
authenticate = response.parseHeader('www-authenticate');
} else {
authenticate = response.parseHeader('proxy-authenticate');
}

response = {
realm: authenticate.realm.replace(/"/g,''),
qop: authenticate.qop || null,
nonce: authenticate.nonce.replace(/"/g,'')
};
realm = authenticate.realm.replace(/"/g,'');
qop = authenticate.qop || null;
nonce = authenticate.nonce.replace(/"/g,'');
opaque = authenticate.opaque;

this.password = password;
this.method = request.method;

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;
};

JsSIP.DigestAuthentication.prototype.authenticate = function(password) {
var ha1, ha2, nc;

password = password || this.password;

this.cnonce = Math.random().toString(36).substr(2, 12);
this.nc += 1;

// nc-value = 8LHEX. Max value = 'FFFFFFFF'
if (this.nc === 4294967296) {
console.log('Maximum "nc" value has been reached. Reseting "nc"');
this.nc = 1;
}

// HA1 = MD5(A1) = MD5(username:realm:password)
ha1 = JsSIP.utils.MD5(credentials.username + ":" + response.realm + ":" + credentials.password);

switch(response.qop) {
case 'auth-int':
// HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody))
ha2 = JsSIP.utils.MD5(request.method + ":" + request.ruri + ":" + JsSIP.utils.MD5(request.body ? request.body : ""));
break;
default:
// HA2 = MD5(A2) = MD5(method:digestURI)
ha2 = JsSIP.utils.MD5(request.method + ":" + request.ruri);
ha1 = JsSIP.utils.MD5(this.username + ":" + this.realm + ":" + password);

if (this.qop === 'auth' || this.qop === null) {
// HA2 = MD5(A2) = MD5(method:digestURI)
ha2 = JsSIP.utils.MD5(this.method + ":" + this.uri);

} else if (this.qop === 'auth-int') {
// HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody))
ha2 = JsSIP.utils.MD5(this.method + ":" + this.uri + ":" + JsSIP.utils.MD5(this.body ? this.body : ""));
}

if(response.qop) {
if(this.qop === 'auth' || this.qop === 'auth-int') {
// response = MD5(HA1:nonce:nonceCount:credentialsNonce:qop:HA2)
response = JsSIP.utils.MD5(ha1 + ":" + response.nonce + ":" + nc + ":" + cnonce + ":" + response.qop + ":" + ha2);
this.response = JsSIP.utils.MD5(ha1 + ":" + this.nonce + ":" + this.decimalToHex(this.nc) + ":" + this.cnonce + ":" + this.qop + ":" + ha2);
} else {
// response = MD5(HA1:nonce:HA2)
response = JsSIP.utils.MD5(ha1 + ":" + response.nonce + ":" + ha2);
this.response = JsSIP.utils.MD5(ha1 + ":" + this.nonce + ":" + ha2);
}

// Fill the Authorization object
authorization.username = '"' + credentials.username + '"';
authorization.realm = authenticate.realm;
authorization.nonce = authenticate.nonce;
authorization.uri = '"' + request.ruri + '"';
authorization.qop = authenticate.qop || null;
authorization.response = '"' + response + '"';
authorization.algorithm = "MD5";
authorization.opaque = authenticate.opaque || null;
authorization.cnonce = authenticate.qop ? '"' + cnonce + '"' : null;
authorization.nc = authenticate.qop ? nc : null;

for(param in authorization) {
if(authorization[param] !== null) {
digest += ',' + param + '=' + authorization[param];
}
}
return this.toString();
};

JsSIP.DigestAuthentication.prototype.toString = function() {
var authorization = 'Digest ';

authorization += 'username="' + this.username + '", ';
authorization += 'realm="' + this.realm + '", ';
authorization += 'nonce="' + this.nonce + '", ';
authorization += 'uri="' + this.uri + '", ';
authorization += this.qop ? 'qop=' + this.qop + ', ' : '';
authorization += 'response="' + this.response + '", ';
authorization += 'algorithm=MD5, ';
authorization += this.opaque ? 'opaque="' + this.opaque + '", ': '';
authorization += this.qop ? 'cnonce="' + this.cnonce + '", ' : '';
authorization += this.qop ? 'nc=' + this.decimalToHex(this.nc) : '';

return authorization;
};


return 'Digest ' + digest.substr(1);
JsSIP.DigestAuthentication.prototype.decimalToHex = function(decimal) {
var hex = Number(decimal).toString(16);
return '00000000'.substr(0, 8-hex.length) + hex;
};
54 changes: 37 additions & 17 deletions src/RequestSender.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,31 @@ JsSIP.RequestSender = function(applicant, ua) {
this.applicant = applicant;
this.method = applicant.request.method;
this.request = applicant.request;
this.credentials = null;
this.challenged = false;
this.staled = false;

// If ua is in closing process or even closed just allow sending Bye and ACK
if (ua.status === JsSIP.c.UA_STATUS_USER_CLOSED && (this.method !== JsSIP.c.BYE || this.method !== JsSIP.c.ACK)) {
this.onTransportError();
}

this.credentials = ua.getCredentials(this.request);
};

/**
* Create the client transaction and send the message.
*/
JsSIP.RequestSender.prototype = {
send: function() {
if (this.credentials) {
if (this.request.method === JsSIP.c.REGISTER) {
this.request.setHeader('authorization', this.credentials.authenticate());
} else {
this.request.setHeader('proxy-authorization', this.credentials.authenticate());
}
}

switch(this.method) {
case "INVITE":
this.clientTransaction = new JsSIP.Transactions.InviteClientTransaction(this, this.request, this.ua.transport);
Expand Down Expand Up @@ -65,36 +77,44 @@ JsSIP.RequestSender.prototype = {
* @param {JsSIP.IncomingResponse} response
*/
receiveResponse: function(response) {
var authorization, cseq,
var cseq, challenge,
status_code = response.status_code;

/*
* Authentication
* Authenticate once. _challenged_ flag used to avoid infinite authentications.
*/
if ((status_code === 401 || status_code === 407) && !this.challenged && this.ua.configuration.password !== null) {
authorization = JsSIP.DigestAuthentication(this.ua, this.request, response);
if ((status_code === 401 || status_code === 407) && this.ua.configuration.password !== null) {

if (status_code === 401) {
this.request.setHeader('authorization', authorization);
challenge = response.s('WWW-Authenticate');
} else {
this.request.setHeader('proxy-authorization', authorization);
challenge = response.s('Authenticate');
}

if (response.method === JsSIP.c.REGISTER) {
cseq = this.applicant.cseq += 1;
} else if (this.request.dialog){
cseq = this.request.dialog.local_seqnum += 1;
} else {
cseq = this.request.headers.CSeq.toString().split(' ')[0];
cseq = parseInt(cseq,10) +1;
}
if ( !this.challenged || (this.challenged && !this.staled && challenge.stale) ) {
if (!this.credentials) {
this.credentials = new JsSIP.DigestAuthentication(this.ua, this.request, response);
}

this.request.setHeader('cseq', cseq +' '+ this.method);
this.challenged = true;
this.send();
if (response.method === JsSIP.c.REGISTER) {
cseq = this.applicant.cseq += 1;
} else if (this.request.dialog){
cseq = this.request.dialog.local_seqnum += 1;
} else {
cseq = this.request.headers.CSeq.toString().split(' ')[0];
cseq = parseInt(cseq,10) +1;
}

this.request.setHeader('cseq', cseq +' '+ this.method);
this.challenged = true;
this.send();
}
} else {
this.applicant.receiveResponse(response);
if (this.challenged && response.status_code >= 200) {
this.ua.saveCredentials(this.credentials);
}
this.applicant.receiveResponse(response);
}
}
};
24 changes: 24 additions & 0 deletions src/UA.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ JsSIP.UA = function(configuration) {
'newMessage'
];

this.cache = {
credentials: {}
};

this.configuration = {};
this.dialogs = {};
this.registrator = null;
Expand Down Expand Up @@ -234,6 +238,26 @@ JsSIP.UA.prototype.start = function() {
}
};


JsSIP.UA.prototype.saveCredentials = function(credentials) {
this.cache.credentials[credentials.realm] = this.cache.credentials[credentials.realm] || {};
this.cache.credentials[credentials.realm][credentials.uri] = credentials;
};

JsSIP.UA.prototype.getCredentials = function(request) {
var realm, credentials;

realm = JsSIP.grammar.parse(request.headers['To'].toString(), 'To').host;

if (this.cache.credentials[realm] && this.cache.credentials[realm][request.ruri]) {
credentials = this.cache.credentials[realm][request.ruri];
credentials.method = request.method;
}

return credentials;
};


//==========================
// Event Handlers
//==========================
Expand Down

0 comments on commit 85ca354

Please sign in to comment.