1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2024-11-22 02:17:26 +00:00

Add TOTP secret to user_key hash

thanks @downtownallday
* this invalidates all user_keys after TOTP status is changed for user
* after changing TOTP state, a login is required
* due to the forced login, we can't and don't need to store the code used for setup in `mru_code`
This commit is contained in:
Felix Spöttel 2020-09-12 16:34:06 +02:00
parent 2ea97f0643
commit dcb93d071c
6 changed files with 43 additions and 26 deletions

View File

@ -3,7 +3,7 @@ import base64, os, os.path, hmac
from flask import make_response from flask import make_response
import utils, totp import utils, totp
from mailconfig import get_mail_password, get_mail_user_privileges from mailconfig import get_mail_password, get_mail_user_privileges, get_mfa_state
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key' DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server' DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'
@ -136,15 +136,23 @@ class KeyAuthService:
def create_user_key(self, email, env): def create_user_key(self, email, env):
# Store an HMAC with the client. The hashed message of the HMAC will be the user's # Store an HMAC with the client. The hashed message of the HMAC will be the user's
# email address & hashed password and the key will be the master API key. The user of # email address & hashed password and the key will be the master API key. If TOTP
# course has their own email address and password. We assume they do not have the master # is active, the key will also include the TOTP secret. The user of course has their
# API key (unless they are trusted anyway). The HMAC proves that they authenticated # own email address and password. We assume they do not have the master API key
# with us in some other way to get the HMAC. Including the password means that when # (unless they are trusted anyway). The HMAC proves that they authenticated with us
# in some other way to get the HMAC. Including the password means that when
# a user's password is reset, the HMAC changes and they will correctly need to log # a user's password is reset, the HMAC changes and they will correctly need to log
# in to the control panel again. This method raises a ValueError if the user does # in to the control panel again. This method raises a ValueError if the user does
# not exist, due to get_mail_password. # not exist, due to get_mail_password.
msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8") msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8")
return hmac.new(self.key.encode('ascii'), msg, digestmod="sha256").hexdigest()
mfa_state = get_mfa_state(email, env)
hash_key = self.key.encode('ascii')
if mfa_state['type'] == 'totp':
hash_key = hash_key + mfa_state['secret'].encode('ascii')
return hmac.new(hash_key, msg, digestmod="sha256").hexdigest()
def _generate_key(self): def _generate_key(self):
raw_key = os.urandom(32) raw_key = os.urandom(32)

View File

@ -439,7 +439,7 @@ def totp_post_enable():
return json_response({ "error": 'bad_input' }, 400) return json_response({ "error": 'bad_input' }, 400)
if totp.validate(secret, token): if totp.validate(secret, token):
create_totp_credential(email, secret, token, env) create_totp_credential(email, secret, env)
return json_response({}) return json_response({})
return json_response({ "error": 'token_mismatch' }, 400) return json_response({ "error": 'token_mismatch' }, 400)

View File

@ -565,11 +565,11 @@ def get_mfa_state(email, env):
'mru_token': '' if mru_token is None else mru_token 'mru_token': '' if mru_token is None else mru_token
} }
def create_totp_credential(email, secret, token, env): def create_totp_credential(email, secret, env):
validate_totp_secret(secret) validate_totp_secret(secret)
conn, c = open_database(env, with_connection=True) conn, c = open_database(env, with_connection=True)
c.execute('INSERT INTO totp_credentials (user_email, secret, mru_token) VALUES (?, ?, ?)', (email, secret, token)) c.execute('INSERT INTO totp_credentials (user_email, secret) VALUES (?, ?)', (email, secret))
conn.commit() conn.commit()
return "OK" return "OK"

View File

@ -363,6 +363,16 @@ function api(url, method, data, callback, callback_error, headers) {
var current_panel = null; var current_panel = null;
var switch_back_to_panel = null; var switch_back_to_panel = null;
function do_logout() {
api_credentials = ["", ""];
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
show_panel('login');
}
function show_panel(panelid) { function show_panel(panelid) {
if (panelid.getAttribute) if (panelid.getAttribute)
// we might be passed an HTMLElement <a>. // we might be passed an HTMLElement <a>.

View File

@ -166,15 +166,6 @@ function do_login() {
}); });
} }
function do_logout() {
api_credentials = ["", ""];
if (typeof localStorage != 'undefined')
localStorage.removeItem("miab-cp-credentials");
if (typeof sessionStorage != 'undefined')
sessionStorage.removeItem("miab-cp-credentials");
show_panel('login');
}
function show_login() { function show_login() {
$('#loginForm').removeClass('is-twofactor'); $('#loginForm').removeClass('is-twofactor');
$('#loginOtpInput').val(''); $('#loginOtpInput').val('');

View File

@ -37,31 +37,35 @@
<div class="loading-indicator">Loading...</div> <div class="loading-indicator">Loading...</div>
<form id="totp-setup"> <form id="totp-setup">
<p>After enabling two factor authentication, any login to the admin panel will require you to enter a time-limited 6-digit number from an authenticator app after entering your normal credentials.</p>
<div class="form-group"> <div class="form-group">
<h3>Setup</h3> <h3>Setup Instructions</h3>
<p>After enabling two factor authentication, any login to the admin panel will require you to enter a time-limited 6-digit number from an authenticator app after entering your normal credentials.</p>
<p>1. Scan the QR code or enter the secret into an authenticator app (e.g. Google Authenticator)</p> <p>1. Scan the QR code or enter the secret into an authenticator app (e.g. Google Authenticator)</p>
<div id="totp-setup-qr"></div> <div id="totp-setup-qr"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="otp">2. Enter the code displayed in the Authenticator app</label> <label for="otp">2. Enter the code displayed in the Authenticator app</label>
<p>You will have to log into the admin panel again after enabling two-factor authentication.</p>
<input type="text" id="totp-setup-token" class="form-control" placeholder="6-digit code" /> <input type="text" id="totp-setup-token" class="form-control" placeholder="6-digit code" />
</div> </div>
<input type="hidden" id="totp-setup-secret" /> <input type="hidden" id="totp-setup-secret" />
<div class="form-group"> <div class="form-group">
<button id="totp-setup-submit" disabled type="submit" class="btn">Enable two factor authentication</button> <button id="totp-setup-submit" disabled type="submit" class="btn">Enable two-factor authentication</button>
</div> </div>
</form> </form>
<form id="disable-2fa"> <form id="disable-2fa">
<div class="form-group"> <div class="form-group">
<p>Two factor authentication is active.</p> <p>Two-factor authentication is active for your account. You can disable it by clicking below button.</p>
<p>You will have to log into the admin panel again after disabling two-factor authentication.</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-danger">Disable two-factor authentication</button>
</div> </div>
<button type="submit" class="btn btn-danger">Disable two factor authentication</button>
</form> </form>
<div id="output-2fa" class="panel panel-danger"> <div id="output-2fa" class="panel panel-danger">
@ -173,7 +177,9 @@
'/mfa/totp/disable', '/mfa/totp/disable',
'POST', 'POST',
{}, {},
function() { show_two_factor_auth(); } function() {
do_logout();
}
); );
return false; return false;
@ -190,7 +196,9 @@
token: $(el.totpSetupToken).val(), token: $(el.totpSetupToken).val(),
secret: $(el.totpSetupSecret).val() secret: $(el.totpSetupSecret).val()
}, },
function(res) { show_two_factor_auth(); }, function(res) {
do_logout();
},
function(res) { function(res) {
var errorMessage = 'Something went wrong.'; var errorMessage = 'Something went wrong.';
var parsed; var parsed;