diff --git a/node/db/Pad.js b/node/db/Pad.js index 7807d4643..806c0c609 100644 --- a/node/db/Pad.js +++ b/node/db/Pad.js @@ -496,9 +496,13 @@ Class('Pad', { this.passwordHash = password == null ? null : hash(password, generateSalt()); db.setSub("pad:"+this.id, ["passwordHash"], this.passwordHash); }, + getPasswordSalt: function() + { + return this.passwordHash.split("$")[1]; + }, isCorrectPassword: function(password) { - return compare(this.passwordHash, password) + return timeSensitiveCompare(this.passwordHash, password) }, isPasswordProtected: function() { @@ -519,17 +523,21 @@ function hash(password, salt) function generateSalt() { var len = 86; - var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"; + var charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz./"; var randomstring = ''; for (var i = 0; i < len; i++) { - var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); + var rnum = Math.floor(Math.random() * charset.length); + randomstring += charset[rnum]; } 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(); } diff --git a/node/db/SecurityManager.js b/node/db/SecurityManager.js index 52d5afcbe..6e34f1f10 100644 --- a/node/db/SecurityManager.js +++ b/node/db/SecurityManager.js @@ -91,6 +91,7 @@ exports.checkAccess = function (padID, sessionID, token, password, callback) var groupID = padID.split("$")[0]; var padExists = false; var validSession = false; + var pwsalt; var sessionAuthor; var tokenAuthor; var isPublic; @@ -171,6 +172,9 @@ exports.checkAccess = function (padID, sessionID, token, password, callback) //is it password protected? isPasswordProtected = pad.isPasswordProtected(); + + //get the password salt used by the hash function + pwsalt = pad.getPasswordSalt(); //is password correct? if(isPasswordProtected && password && pad.isCorrectPassword(password)) @@ -202,13 +206,14 @@ exports.checkAccess = function (padID, sessionID, token, password, callback) else if(isPasswordProtected && passwordStatus == "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 else if(isPasswordProtected && passwordStatus == "notGiven") { //--> ask for password - statusObject = {accessStatus: "needPassword"}; + statusObject = {accessStatus: "needPassword", passwordSalt: pwsalt}; } else { diff --git a/static/js/pad2.js b/static/js/pad2.js index debd25512..2d10cc943 100644 --- a/static/js/pad2.js +++ b/static/js/pad2.js @@ -73,7 +73,7 @@ function randomString() for (var i = 0; i < string_length; i++) { var rnum = Math.floor(Math.random() * chars.length); - randomstring += chars.substring(rnum, rnum + 1); + randomstring += chars[rnum]; } return "t." + randomstring; } @@ -150,10 +150,22 @@ function getUrlVars() 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 - 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 document.location=document.location; } @@ -255,13 +267,13 @@ function handshake() { $("#editorloadingbox").html("You need a password to access this pad
" + ""+ - ""); + ""); } else if(obj.accessStatus == "wrongPassword") { - $("#editorloadingbox").html("You're password was wrong
" + + $("#editorloadingbox").html("Your password was wrong
" + ""+ - ""); + ""); } }