Florent Daigniere 2 years ago
parent 62c919da09
commit afbaabd8cd

@ -3,6 +3,56 @@ require('./app.css');
import logo from './mailu.png'; import logo from './mailu.png';
import modules from "./*.json"; 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.addEventListener("load", function(){
// When we get back a response from the server
// We create an array of lines and loop through them
const resp = this.responseText.split('\n');
const hashSub = hash.slice(5).toUpperCase();
for(index in resp){
// Check if the line matches the rest of the hash
if(resp[index].substring(0, 35) == hashSub){
var val = resp[index].split(":")[1]
if (val > 0) {
$("#pwned").value = val;
}
return; // If found no need to continue the loop
}
}
$("#pwned").value = 0;
});
req.open('GET', 'https://api.pwnedpasswords.com/range/'+hash.substr(0, 5));
req.setRequestHeader('Add-Padding', 'true');
req.send();
});
}
// TODO: conditionally (or lazy) load select2 and dataTable // TODO: conditionally (or lazy) load select2 and dataTable
$('document').ready(function() { $('document').ready(function() {
@ -75,5 +125,12 @@ $('document').ready(function() {
$('form :input').prop('disabled', true); $('form :input').prop('disabled', true);
} }
if (window.isSecureContext) {
$("#HIBPpw").change(function(){
hibpCheck($("#HIBPpw").value);
return true;
})
}
}); });

@ -7,5 +7,6 @@ class LoginForm(flask_wtf.FlaskForm):
csrf = False csrf = False
email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()]) email = fields.StringField(_('E-mail'), [validators.Email(), validators.DataRequired()])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pwned = fields.HiddenField(label='',default=-1)
submitWebmail = fields.SubmitField(_('Sign in')) submitWebmail = fields.SubmitField(_('Sign in'))
submitAdmin = fields.SubmitField(_('Sign in')) submitAdmin = fields.SubmitField(_('Sign in'))

@ -40,7 +40,10 @@ def login():
flask_login.login_user(user) flask_login.login_user(user)
response = flask.redirect(destination) response = flask.redirect(destination)
response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True) response.set_cookie('rate_limit', utils.limiter.device_cookie(username), max_age=31536000, path=flask.url_for('sso.login'), secure=app.config['SESSION_COOKIE_SECURE'], httponly=True)
flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip}.') flask.current_app.logger.info(f'Login succeeded for {username} from {client_ip} pwned={form.pwned.data}.')
breaches = int(form.pwned.data)
if breaches > 0:
flask.flash(f"Your password appears in {breaches} data breaches! Please change it.", "error")
return response return response
else: else:
utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip) utils.limiter.rate_limit_user(username, client_ip, device_cookie, device_cookie_username) if models.User.get(username) else utils.limiter.rate_limit_ip(client_ip)

@ -59,6 +59,7 @@ class DomainSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Initial admin'), [validators.DataRequired()]) localpart = fields.StringField(_('Initial admin'), [validators.DataRequired()])
pw = fields.PasswordField(_('Admin password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Admin password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
pwned = fields.HiddenField(label='',default=-1)
captcha = flask_wtf.RecaptchaField() captcha = flask_wtf.RecaptchaField()
submit = fields.SubmitField(_('Create')) submit = fields.SubmitField(_('Create'))
@ -79,6 +80,7 @@ class UserForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) localpart = fields.StringField(_('E-mail'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password')) pw = fields.PasswordField(_('Password'))
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
pwned = fields.HiddenField(label='',default=-1)
quota_bytes = fields_.IntegerSliderField(_('Quota'), default=10**9) quota_bytes = fields_.IntegerSliderField(_('Quota'), default=10**9)
enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True) enable_imap = fields.BooleanField(_('Allow IMAP access'), default=True)
enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True) enable_pop = fields.BooleanField(_('Allow POP3 access'), default=True)
@ -92,6 +94,7 @@ class UserSignupForm(flask_wtf.FlaskForm):
localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)]) localpart = fields.StringField(_('Email address'), [validators.DataRequired(), validators.Regexp(LOCALPART_REGEX)])
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')]) pw2 = fields.PasswordField(_('Confirm password'), [validators.EqualTo('pw')])
pwned = fields.HiddenField(label='',default=-1)
submit = fields.SubmitField(_('Sign up')) submit = fields.SubmitField(_('Sign up'))
class UserSignupFormCaptcha(UserSignupForm): class UserSignupFormCaptcha(UserSignupForm):
@ -111,6 +114,7 @@ class UserSettingsForm(flask_wtf.FlaskForm):
class UserPasswordForm(flask_wtf.FlaskForm): class UserPasswordForm(flask_wtf.FlaskForm):
pw = fields.PasswordField(_('Password'), [validators.DataRequired()]) pw = fields.PasswordField(_('Password'), [validators.DataRequired()])
pw2 = fields.PasswordField(_('Password check'), [validators.DataRequired()]) pw2 = fields.PasswordField(_('Password check'), [validators.DataRequired()])
pwned = fields.HiddenField(label='',default=-1)
submit = fields.SubmitField(_('Update password')) submit = fields.SubmitField(_('Update password'))

@ -93,6 +93,10 @@ def domain_signup(domain_name=None):
del form.pw del form.pw
del form.pw2 del form.pw2
if form.validate_on_submit(): if form.validate_on_submit():
breaches = int(form.pwned.data)
if breaches > 0:
flask.flash(f"This password appears in {breaches} data breaches! Please change it.", "error")
return flask.render_template('domain/signup.html', form=form)
conflicting_domain = models.Domain.query.get(form.name.data) conflicting_domain = models.Domain.query.get(form.name.data)
conflicting_alternative = models.Alternative.query.get(form.name.data) conflicting_alternative = models.Alternative.query.get(form.name.data)
conflicting_relay = models.Relay.query.get(form.name.data) conflicting_relay = models.Relay.query.get(form.name.data)

@ -28,6 +28,11 @@ def user_create(domain_name):
form.quota_bytes.validators = [ form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=domain.max_quota_bytes)] wtforms.validators.NumberRange(max=domain.max_quota_bytes)]
if form.validate_on_submit(): if form.validate_on_submit():
breaches = int(form.pwned.data)
if breaches > 0:
flask.flash(f"This password appears in {breaches} data breaches! Please change it.", "error")
return flask.render_template('user/create.html',
domain=domain, form=form)
if domain.has_email(form.localpart.data): if domain.has_email(form.localpart.data):
flask.flash('Email is already used', 'error') flask.flash('Email is already used', 'error')
else: else:
@ -60,6 +65,11 @@ def user_edit(user_email):
form.quota_bytes.validators = [ form.quota_bytes.validators = [
wtforms.validators.NumberRange(max=max_quota_bytes)] wtforms.validators.NumberRange(max=max_quota_bytes)]
if form.validate_on_submit(): if form.validate_on_submit():
breaches = int(form.pwned.data)
if breaches > 0:
flask.flash(f"This password appears in {breaches} data breaches! Please change it.", "error")
return flask.render_template('user/edit.html', form=form, user=user,
domain=user.domain, max_quota_bytes=max_quota_bytes)
form.populate_obj(user) form.populate_obj(user)
if form.pw.data: if form.pw.data:
user.set_password(form.pw.data) user.set_password(form.pw.data)
@ -119,6 +129,10 @@ def user_password(user_email):
if form.pw.data != form.pw2.data: if form.pw.data != form.pw2.data:
flask.flash('Passwords do not match', 'error') flask.flash('Passwords do not match', 'error')
else: else:
breaches = int(form.pwned.data)
if breaches > 0:
flask.flash(f"This password appears in {breaches} data breaches! Please change it.", "error")
return flask.render_template('user/password.html', form=form, user=user)
flask.session.regenerate() flask.session.regenerate()
user.set_password(form.pw.data) user.set_password(form.pw.data)
models.db.session.commit() models.db.session.commit()
@ -170,6 +184,10 @@ def user_signup(domain_name=None):
if domain.has_email(form.localpart.data) or models.Alias.resolve(form.localpart.data, domain_name): if domain.has_email(form.localpart.data) or models.Alias.resolve(form.localpart.data, domain_name):
flask.flash('Email is already used', 'error') flask.flash('Email is already used', 'error')
else: else:
breaches = int(form.pwned.data)
if breaches > 0:
flask.flash(f"This password appears in {breaches} data breaches! Please change it.", "error")
return flask.render_template('user/signup.html', domain=domain, form=form)
flask.session.regenerate() flask.session.regenerate()
user = models.User(domain=domain) user = models.User(domain=domain)
form.populate_obj(user) form.populate_obj(user)

Loading…
Cancel
Save