Skip to content

Commit

Permalink
Edge cases, check for well known passwords
Browse files Browse the repository at this point in the history
  • Loading branch information
zemkat committed Sep 12, 2018
1 parent 99b6aac commit ebfba71
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 7 deletions.
3 changes: 3 additions & 0 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ are not as thoroughly tested.
The web server hosting Grima must support https, and Grima must be accessed
over https for authentication to work properly.

Grima must be run with Chrome, Firefox, or Safari; authentication will
not work properly with Edge or Internet Explorer.

Grima requires libxml to parse Alma's replies, libcurl to make its API queries,
and PDO to make some very minimal database queries (for users and apikeys). The
default database provider is an sqlite3 file in `/tmp/grima`. Grima has been
Expand Down
96 changes: 89 additions & 7 deletions grimas/Login/Login.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
(function(){

// Edge lacks:
// * TextEncoder for UTF-8 bytes to use strings in crypto
// * crypto.subtle.digest("SHA-1") to query well known passwords
// * crypto.subtle.deriveBits("PBKDF2") to hide human password from server

if (document.readyState == "complete") {
setup();
} else {
Expand All @@ -9,35 +14,105 @@ if (document.readyState == "complete") {
function setup() {
for ( const elt of document.querySelectorAll( "input[name='password']" ) ) {
elt.form.addEventListener("submit", onSubmit);
elt.addEventListener("input", onInput );
}
}

function onInput( event ) {
const elt = event.target;
if ( elt.timer ) {
clearTimeout( elt.timer );
}
markPasswordGood( elt );
elt.timer = setTimeout( () => checkForWellKnownPassword(elt).catch(()=>{}), 300 );
}

function onSubmit( event ) {
const form = event.target;
if (form instanceof HTMLFormElement) {
const passwordElt = form.elements.namedItem("password");
const usernameElt = form.elements.namedItem("username");
const institutionElt = form.elements.namedItem("institutionElt");
const institutionElt = form.elements.namedItem("institution");
if ( (passwordElt && passwordElt.value)
&& (!passwordElt.value.startsWith( "PBKDF2-" ))
) {
event.preventDefault();
const username = usernameElt ? usernameElt.value : "user";
const institution = institutionElt ? institutionElt.value : "institution";
const password = passwordElt.value;
hash( password, `grima-clientside-login-v1:${institution}:${username}` )
.then( (hash) => {
const checkPromise = checkForWellKnownPassword( passwordElt )
const hashPromise = hash( password, `grima-clientside-login-v1:${institution}:${username}` )
Promise.all( [ checkPromise, hashPromise ] )
.then( ([_,hash]) => {
passwordElt.value = hash;
form.submit();
} )
.catch( err => {
passwordElt.setCustomValidity( err.toString() );
} );
.catch( err => markPasswordBad( elt, err.toString(), "Cannot send this password" ) )
return false;
}
}
}

function markPasswordBad( elt, validityMessage, buttonText ) {
elt.setCustomValidity( validityMessage.toString() );
for (const btnelt of elt.form.querySelectorAll('input[type="submit"]') ) {
btnelt.classList.add( "btn-danger" );
btnelt.value = buttonText;
}
}

function markPasswordGood( elt ) {
elt.setCustomValidity( "" );
for (const btnelt of elt.form.querySelectorAll('input[type="submit"]') ) {
btnelt.classList.remove( "btn-danger" );
btnelt.value = "Submit";
}
}

const checked = new Map();
function checkForWellKnownPassword( elt ) {
if ( (elt instanceof HTMLInputElement)
&& (window.crypto)
&& (window.crypto.subtle)
) {
if (checked.has(elt.value)) {
const wellKnown = checked.get(elt.value);
if (wellKnown) {
const err = `That password has been used by ${wellKnown} compromised accounts.`;
const btn = "Don't send such a well-known password to server";
markPasswordBad( elt, err, btn );
return Promise.reject( err );
} else {
markPasswordGood( elt );
return Promise.resolve();
}
}
return window.crypto.subtle
.digest("SHA-1", bin(elt.value) )
.then( sha1 => hex(sha1) )
.then( sha1 => fetch( `https://api.pwnedpasswords.com/range/${sha1.substring(0,5)}`)
.then( response => response.text() )
.then( text => {
for (const line of text.split(/\r\n/g)) {
const [ rest, wellKnown ] = line.split(/:/g);
if (sha1.substring(5) === rest.toLowerCase()) {
const err = `That password has been used by ${wellKnown} compromised accounts.`;
const btn = "Don't send such a well-known password to server";
markPasswordBad( elt, err, btn );
checked.set( elt.value, wellKnown );
return Promise.reject(err);
}
}
markPasswordGood( elt );
checked.set( elt.value, 0 );
return Promise.resolve();
})
);
} else {
return Promise.resolve();
}
}

function hash( password, salt_seed ) {
const name = "PBKDF2";
const hash = "SHA-512";
Expand All @@ -50,12 +125,19 @@ function hash( password, salt_seed ) {
}
}
return window.crypto.subtle
.digest( "SHA-512", bin( salt_seed ) )
.digest( hash, bin( salt_seed ) )
.then( salt => window.crypto.subtle
.importKey( "raw", bin(password), {name}, false, ["deriveBits"])
.then( (pw) => window.crypto.subtle
.deriveBits( {name, salt, iterations, hash}, pw, 128 ) )
.then( (key) => `PBKDF2-${hash}\$${iterations}\$${hex(salt)}\$${hex(key)}` ) )
.catch( (err) => {
if (err.name === "PBKDF2") {
return Promise.reject( "Client side crypto not available. Please don't use Edge.");
} else {
throw err;
}
});
}

function bin(str) {
Expand Down

0 comments on commit ebfba71

Please sign in to comment.