diff --git a/core/admin/assets/assets/app.js b/core/admin/assets/assets/app.js index 03ea6215..cac55971 100644 --- a/core/admin/assets/assets/app.js +++ b/core/admin/assets/assets/app.js @@ -3,6 +3,56 @@ require('./app.css'); import logo from './mailu.png'; import modules from "./*.json"; +// Inspired from https://github.com/mehdibo/hibp-js/blob/master/hibp.js +function sha1(string) { + var buffer = new TextEncoder("utf-8").encode(string); + return crypto.subtle.digest("SHA-1", buffer).then(function (buffer) { + // Get the hex code + var hexCodes = []; + var view = new DataView(buffer); + for (var i = 0; i < view.byteLength; i += 4) { + // Using getUint32 reduces the number of iterations needed (we process 4 bytes each time) + var value = view.getUint32(i); + // toString(16) will give the hex representation of the number without padding + var stringValue = value.toString(16); + // We use concatenation and slice for padding + var padding = '00000000'; + var paddedValue = (padding + stringValue).slice(-padding.length); + hexCodes.push(paddedValue); + } + // Join all the hex strings into one + return hexCodes.join(""); + }); +} + +function hibpCheck(pwd) { + // We hash the pwd first + sha1(pwd).then(function(hash){ + // We send the first 5 chars of the hash to hibp's API + const req = new XMLHttpRequest(); + req.open('GET', 'https://api.pwnedpasswords.com/range/'+hash.substr(0, 5)); + req.setRequestHeader('Add-Padding', 'true'); + req.addEventListener("load", function(){ + // When we get back a response from the server + // We create an array of lines and loop through them + const lines = this.responseText.split("\n"); + const hashSub = hash.slice(5).toUpperCase(); + for (var i in lines){ + // Check if the line matches the rest of the hash + if (lines[i].substring(0, 35) == hashSub){ + const val = parseInt(lines[i].trimEnd("\r").split(":")[1]); + if (val > 0) { + $("#pwned").val(val); + } + return; // If found no need to continue the loop + } + } + $("#pwned").val(0); + }); + req.send(); + }); +} + // TODO: conditionally (or lazy) load select2 and dataTable $('document').ready(function() { @@ -75,5 +125,19 @@ $('document').ready(function() { $('form :input').prop('disabled', true); } + if (window.isSecureContext) { + $("#pw").on("change paste", function(){ + hibpCheck($(this).val()); + return true; + }); + $("#pw").closest("form").submit(function(event){ + if (parseInt($("#pwned").val()) < 0) { + event.preventDefault(); + hibpCheck($("#pw").val()); + event.trigger(); + } + }); + } + }); diff --git a/core/admin/mailu/sso/forms.py b/core/admin/mailu/sso/forms.py index 5cf38dbe..ca124c02 100644 --- a/core/admin/mailu/sso/forms.py +++ b/core/admin/mailu/sso/forms.py @@ -7,5 +7,6 @@ class LoginForm(flask_wtf.FlaskForm): csrf = False email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) + pwned = fields.HiddenField(label='', default=-1) submitWebmail = fields.SubmitField(_('Sign in')) submitAdmin = fields.SubmitField(_('Sign in')) diff --git a/core/admin/mailu/sso/templates/form_sso.html b/core/admin/mailu/sso/templates/form_sso.html index d2451597..d713251e 100644 --- a/core/admin/mailu/sso/templates/form_sso.html +++ b/core/admin/mailu/sso/templates/form_sso.html @@ -3,6 +3,7 @@ {%- block content %} {%- call macros.card() %}