Modified the authentication stuff to grant access not based on plain password authentication but on a kind of 'timed cookies' so the password is not stored in plain text in some browser cookie.

Also modded some random string generation funtions for elegance.
This commit is contained in:
jaseg 2011-11-09 23:53:00 +01:00
parent 4fc4a35381
commit 082c732429
3 changed files with 39 additions and 14 deletions

View file

@ -489,9 +489,13 @@ Class('Pad', {
this.passwordHash = password == null ? null : hash(password, generateSalt()); this.passwordHash = password == null ? null : hash(password, generateSalt());
db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash);
}, },
getPasswordSalt: function()
{
return this.passwordHash.split("$")[1];
},
isCorrectPassword: function(password) isCorrectPassword: function(password)
{ {
return compare(this.passwordHash, password) return timeSensitiveCompare(this.passwordHash, password)
}, },
isPasswordProtected: function() isPasswordProtected: function()
{ {
@ -512,17 +516,21 @@ function hash(password, salt)
function generateSalt() function generateSalt()
{ {
var len = 86; var len = 86;
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"; var charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./";
var randomstring = ''; var randomstring = '';
for (var i = 0; i < len; i++) for (var i = 0; i < len; i++)
{ {
var rnum = Math.floor(Math.random() * chars.length); var rnum = Math.floor(Math.random() * charset.length);
randomstring += chars.substring(rnum, rnum + 1); randomstring += charset[rnum];
} }
return randomstring; return randomstring;
} }
function compare(hashStr, password) /* Compare the timed password hash with the saved value.
* If the hash was generated too far in the past, it is rejected. */
function timeSensitiveCompare(hashStr, password)
{ {
return hash(password, hashStr.split("$")[1]) === hashStr; var timestamp = password.split("$")[1];
return password === hash(hashStr, timestamp)
&& timestamp - new Date().getTime() > 0;
} }

View file

@ -51,6 +51,7 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
var groupID = padID.split("$")[0]; var groupID = padID.split("$")[0];
var padExists = false; var padExists = false;
var validSession = false; var validSession = false;
var pwsalt;
var sessionAuthor; var sessionAuthor;
var tokenAuthor; var tokenAuthor;
var isPublic; var isPublic;
@ -132,6 +133,9 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
//is it password protected? //is it password protected?
isPasswordProtected = pad.isPasswordProtected(); isPasswordProtected = pad.isPasswordProtected();
//get the password salt used by the hash function
pwsalt = pad.getPasswordSalt();
//is password correct? //is password correct?
if(isPasswordProtected && password && pad.isCorrectPassword(password)) if(isPasswordProtected && password && pad.isCorrectPassword(password))
{ {
@ -162,13 +166,14 @@ exports.checkAccess = function (padID, sessionID, token, password, callback)
else if(isPasswordProtected && passwordStatus == "wrong") else if(isPasswordProtected && passwordStatus == "wrong")
{ {
//--> deny access, ask for new password and tell them that the password is wrong //--> deny access, ask for new password and tell them that the password is wrong
statusObject = {accessStatus: "wrongPassword"}; //The salt can be safely shared since it is not secret. It does its job (improving resistence against rainbow table attacks) even when public.
statusObject = {accessStatus: "wrongPassword", passwordSalt: pwsalt};
} }
//- the pad is password protected but no password given //- the pad is password protected but no password given
else if(isPasswordProtected && passwordStatus == "notGiven") else if(isPasswordProtected && passwordStatus == "notGiven")
{ {
//--> ask for password //--> ask for password
statusObject = {accessStatus: "needPassword"}; statusObject = {accessStatus: "needPassword", passwordSalt: pwsalt};
} }
else else
{ {

View file

@ -72,7 +72,7 @@ function randomString()
for (var i = 0; i < string_length; i++) for (var i = 0; i < string_length; i++)
{ {
var rnum = Math.floor(Math.random() * chars.length); var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum, rnum + 1); randomstring += chars[rnum];
} }
return "t." + randomstring; return "t." + randomstring;
} }
@ -147,10 +147,22 @@ function getUrlVars()
return vars; return vars;
} }
function savePassword() function hash(password, salt)
{
return sha512(password + salt) + "$" + salt;
}
/* Generate the "timed hash" used to get access.
* The password is hashed with the database's salt, afterwards it is hashed again with a timestamp a few days in the future as "salt".
* The server checks the two hashe's equality as usual, but also checks whether this timestamp is still in the future (grant access)
* or if it has passed (deny access). This provides an saved-password expiry mechanism which is a) independent of the browser's cookie
* retention and b) provides some level of security against "cookie stealing" (be it by xss or otherwise): If Eve steals a cookie, she
* does "only" get a timed-hash lifetime access to the pad, but *not* the actual password.
*/
function savePassword(pwsalt)
{ {
//set the password cookie //set the password cookie
createCookie("password",$("#passwordinput").val(),null,document.location.pathname); createCookie("password",hash(hash($("#passwordinput").val(), pwsalt), new Date().getTime() + 14 * 24 * 3600 * 1000),null,document.location.pathname); //FIXME some means of configuring this threshold would be really great
//reload //reload
document.location=document.location; document.location=document.location;
} }
@ -214,13 +226,13 @@ function handshake()
{ {
$("#editorloadingbox").html("<b>You need a password to access this pad</b><br>" + $("#editorloadingbox").html("<b>You need a password to access this pad</b><br>" +
"<input id='passwordinput' type='password' name='password'>"+ "<input id='passwordinput' type='password' name='password'>"+
"<button type='button' onclick='savePassword()'>ok</button>"); "<button type='button' onclick='savePassword("+obj.passwordSalt+")'>ok</button>");
} }
else if(obj.accessStatus == "wrongPassword") else if(obj.accessStatus == "wrongPassword")
{ {
$("#editorloadingbox").html("<b>You're password was wrong</b><br>" + $("#editorloadingbox").html("<b>Your password was wrong</b><br>" +
"<input id='passwordinput' type='password' name='password'>"+ "<input id='passwordinput' type='password' name='password'>"+
"<button type='button' onclick='savePassword()'>ok</button>"); "<button type='button' onclick='savePassword("+obj.passwordSalt+")'>ok</button>");
} }
} }