Skip to content

Instantly share code, notes, and snippets.

@agranig
Created May 15, 2012 15:21
Show Gist options
  • Save agranig/2702600 to your computer and use it in GitHub Desktop.
Save agranig/2702600 to your computer and use it in GitHub Desktop.
demo for client-side SIP digest response generation
// load extern script crypto-md5.js from https://code.google.com/p/crypto-js/ project here
// socket.io callback for SIP digest authentication request
// it passes the full SIP header www-authenticate or proxy-authenticate as json object,
// one might want to construct the ctx differently though, depending on the data format
socket.on('authentication-request', function (data) {
console.log('received authentication request', data);
var ctx;
if(data.response.status == 401)
ctx = data.response.headers['www-authenticate'][0];
else if(data.response.status == 407)
ctx = data.response.headers['proxy-authenticate'][0];
else return; // TODO: send error back to server
// fetch credentials from DOM here and save it in "creds"
var signedRequest = sipAuthenticator.signRequest(ctx, data.request, data.response, creds);
this.emit('authentication-response', signedRequest);
});
var sipAuthenticator = {
// pretty much a port of sip.js/digest.js from here
unq: function(a) {
if(a && a[0] === '"' && a[a.length-1] === '"')
return a.substr(1, a.length - 2);
return a;
},
q: function(a) {
if(a && a[0] !== '"')
return ['"', a, '"'].join('');
return a;
},
lowercase: function(a) {
if(typeof a === 'string')
return a.toLowerCase();
return a;
},
kd: function() {
return Crypto.MD5(Array.prototype.join.call(arguments, ':'));
},
rbytes: function() {
return sipAuthenticator.kd(Math.random().toString(), Math.random().toString());
},
calculateUserRealmPasswordHash: function(user, realm, password) {
return sipAuthenticator.kd(sipAuthenticator.unq(user), sipAuthenticator.unq(realm), sipAuthenticator.unq(password));
},
calculateHA1: function(ctx) {
var userhash = ctx.userhash || sipAuthenticator.calculateUserRealmPasswordHash(ctx.user, ctx.realm, ctx.password);
if(ctx.algorithm === 'md5-sess') return sipAuthenticator.kd(userhash, ctx.nonce, ctx.cnonce);
return userhash;
},
calculateDigest: function(ctx) {
switch(ctx.qop) {
case 'auth-int':
return sipAuthenticator.kd(ctx.ha1, ctx.nonce, ctx.nc, ctx.cnonce, ctx.qop, sipAuthenticator.kd(ctx.method, ctx.uri, sipAuthenticator.kd(ctx.entity)));
case 'auth':
return sipAuthenticator.kd(ctx.ha1, ctx.nonce, ctx.nc, ctx.cnonce, ctx.qop, sipAuthenticator.kd(ctx.method, ctx.uri));
}
return sipAuthenticator.kd(ctx.ha1, ctx.nonce, sipAuthenticator.kd(ctx.method, ctx.uri));
},
numberTo8Hex: function(n) {
n = n.toString(16);
return '00000000'.substr(n.length) + n;
},
findDigestRealm: function(headers, realm) {
if(!realm) return headers && headers[0];
return headers && headers.filter(function(x) {
return x.scheme === 'Digest' && sipAuthenticator.unq(x.realm) === realm;
})[0];
},
selectQop: function(challenge, preference) {
if(!challenge)
return;
challenge = sipAuthenticator.unq(challenge).split(',');
if(!preference)
return challenge[0];
if(typeof(preference) === 'string')
preference = preference.split(',');
for(var i = 0; i !== preference.length; ++i)
for(var j = 0; j !== challenge.length; ++j)
if(challenge[j] === preference[i])
return challenge[j];
throw new Error('failed to negotiate protection quality');
},
initClientContext: function(ctx, rs, creds) {
var challenge;
if(rs.status === 407) {
ctx.proxy = true;
challenge = sipAuthenticator.findDigestRealm(rs.headers['proxy-authenticate'], creds.realm);
}
else
challenge = sipAuthenticator.findDigestRealm(rs.headers['www-authenticate'], creds.realm);
if(ctx.nonce !== sipAuthenticator.unq(challenge.nonce)) {
ctx.nonce = sipAuthenticator.unq(challenge.nonce);
ctx.algorithm = sipAuthenticator.unq(sipAuthenticator.lowercase(challenge.algorithm));
ctx.qop = sipAuthenticator.selectQop(sipAuthenticator.lowercase(challenge.qop), ctx.qop);
if(ctx.qop) {
ctx.nc = 0;
ctx.cnonce = sipAuthenticator.rbytes();
}
ctx.realm = sipAuthenticator.unq(challenge.realm);
ctx.user = creds.user;
ctx.userhash = creds.hash || sipAuthenticator.calculateUserRealmPasswordHash(creds.user, ctx.realm, creds.password);
ctx.ha1 = ctx.userhash;
if(ctx.algorithm === 'md5-sess')
ctx.ha1 = sipAuthenticator.kd(ctx.ha1, ctx.nonce, ctx.cnonce);
ctx.domain = sipAuthenticator.unq(challenge.domain);
}
ctx.opaque = sipAuthenticator.unq(challenge.opaque);
},
signRequest: function(ctx, rq, rs, creds) {
if(rs)
sipAuthenticator.initClientContext(ctx, rs, creds);
var nc = ctx.nc !== undefined ? sipAuthenticator.numberTo8Hex(++ctx.nc) : undefined;
ctx.uri = rq.uri;
var signature = {
scheme: 'Digest',
realm: sipAuthenticator.q(ctx.realm),
username: sipAuthenticator.q(ctx.user),
nonce: sipAuthenticator.q(ctx.nonce),
uri: sipAuthenticator.q(rq.uri),
nc: nc,
algorithm: sipAuthenticator.q(ctx.algorithm),
cnonce: sipAuthenticator.q(ctx.cnonce),
qop: sipAuthenticator.q(ctx.qop),
opaque: sipAuthenticator.q(ctx.opaque),
response: sipAuthenticator.q(sipAuthenticator.calculateDigest({ha1:ctx.ha1, method:rq.method, nonce:ctx.nonce, nc:nc, cnonce:ctx.cnonce, qop:ctx.qop, uri:ctx.uri, entity:rq.content}))
};
var hname = ctx.proxy ? 'proxy-authorization' : 'authorization';
rq.headers[hname] = (rq.headers[hname] || []).filter(function(x) {
return sipAuthenticator.unq(x.realm) !== ctx.realm;
});
rq.headers[hname].push(signature);
return rq;
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment