1
0
mirror of https://github.com/mail-in-a-box/mailinabox.git synced 2026-03-15 17:37:22 +01:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Joshua Tauberer
65861c68b7 Version 55 2021-10-18 20:40:51 -04:00
Joshua Tauberer
71a7a3e201 Upgrade to Roundcube 1.5 2021-10-18 20:40:51 -04:00
Richard Willis
1c3bca53bb Fix broken link in external-dns.html (#2045) 2021-10-18 07:36:48 -04:00
ukfhVp0zms
b643cb3478 Update calendar/contacts android app info (#2044)
DAVdroid has been renamed to DAVx⁵ and price increased from $3.69 to $5.99.
CardDAV-Sync free is no longer in beta.
CalDAV-Sync price increased from $2.89 to $2.99.
2021-10-13 19:09:05 -04:00
11 changed files with 81 additions and 190 deletions

View File

@@ -1,17 +1,13 @@
CHANGELOG
=========
In Development
--------------
Version 55 (October 18, 2021)
-----------------------------
Mail:
* "SMTPUTF8" is now disabled in Postfix. Because Dovecot still does not support SMTPUTF8, incoming mail to internationalized addresses was bouncing. This fixes incoming mail to internationalized domains (which was probably working prior to v0.40), but it will prevent sending outbound mail to addresses with internationalized local-parts.
* Upgraded to Roundcube 1.5 Release Candidate.
Firewall:
* Fail2ban's IPv6 support is enabled.
* Upgraded to Roundcube 1.5.
Control panel:
@@ -27,6 +23,7 @@ Control panel:
Other:
* Fail2ban's IPv6 support is enabled.
* The mail log tool now doesn't crash if there are email addresess in log messages with invalid UTF-8 characters.
* Additional nsd.conf files can be placed in /etc/nsd.conf.d.

View File

@@ -60,7 +60,7 @@ Clone this repository and checkout the tag corresponding to the most recent rele
$ git clone https://github.com/mail-in-a-box/mailinabox
$ cd mailinabox
$ git checkout v0.54
$ git checkout v55
Begin the installation.

View File

@@ -1740,7 +1740,7 @@ paths:
text/html:
schema:
type: string
/mfa/enable/totp:
/mfa/totp/enable:
post:
tags:
- MFA

View File

@@ -21,7 +21,7 @@ import auth, utils
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
from mailconfig import get_mail_user_privileges, add_remove_mail_user_privilege
from mailconfig import get_mail_aliases, get_mail_aliases_ex, get_mail_domains, add_mail_alias, remove_mail_alias
from mfa import get_public_mfa_state, provision_totp, provision_webauthn, validate_totp_secret, enable_mfa, disable_mfa
from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enable_mfa, disable_mfa
env = utils.load_environment()
@@ -468,7 +468,7 @@ def ssl_provision_certs():
def mfa_get_status():
# Anyone accessing this route is an admin, and we permit them to
# see the MFA status for any user if they submit a 'user' form
# field. But we don't always include provisioning info since a user can
# field. But we don't include provisioning info since a user can
# only provision for themselves.
email = request.form.get('user', request.user_email) # user field if given, otherwise the user making the request
try:
@@ -478,15 +478,14 @@ def mfa_get_status():
if email == request.user_email:
resp.update({
"new_mfa": {
"totp": provision_totp(email, env),
"webauthn": provision_webauthn(email, env)
"totp": provision_totp(email, env)
}
})
except ValueError as e:
return (str(e), 400)
return json_response(resp)
@app.route('/mfa/enable/totp', methods=['POST'])
@app.route('/mfa/totp/enable', methods=['POST'])
@authorized_personnel_only
def totp_post_enable():
secret = request.form.get('secret')
@@ -496,20 +495,7 @@ def totp_post_enable():
return ("Bad Input", 400)
try:
validate_totp_secret(secret)
enable_mfa(request.user_email, "totp", env, secret, token, label)
except ValueError as e:
return (str(e), 400)
return "OK"
@app.route('/mfa/enable/webauthn', methods=['POST'])
@authorized_personnel_only
def webauthn_post_enable():
attestationObject = request.form.get('attestationObject')
clientDataJSON = request.form.get('clientDataJSON')
if type(attestationObject) != str or type(clientDataJSON) != str:
return ("Bad Input", 400)
try:
enable_mfa(request.user_email, "webauthn", env, attestationObject, clientDataJSON)
enable_mfa(request.user_email, "totp", secret, token, label, env)
except ValueError as e:
return (str(e), 400)
return "OK"

View File

@@ -1,12 +1,9 @@
import base64
import hmac
import io
import json
import os
import pyotp
import qrcode
import pywarp
import pywarp.backends
from mailconfig import open_database
@@ -32,44 +29,25 @@ def get_public_mfa_state(email, env):
]
def get_hash_mfa_state(email, env):
# Get the current MFA credential secrets from which we form a hash
# so that we can reset user logins when any authentication information
# changes.
mfa_state = []
for s in get_mfa_state(email, env):
# Add TOTP id and secret to the state.
# Skip WebAuthn state if it's just a challenge.
if s["type"] == "webauthn":
try:
# Get the credential and only include it (not challenges) in the state.
s["secret"] = json.loads(s["secret"])["cred_pub_key"]
except:
# Skip this one --- there is no cred_pub_key.
continue
mfa_state.append({ "id": s["id"], "type": s["type"], "secret": s["secret"] })
return mfa_state
mfa_state = get_mfa_state(email, env)
return [
{ "id": s["id"], "type": s["type"], "secret": s["secret"] }
for s in mfa_state
]
def enable_mfa(email, type, env, *args):
def enable_mfa(email, type, secret, token, label, env):
if type == "totp":
secret, token, label = args
validate_totp_secret(secret)
# Sanity check with the provide current token.
totp = pyotp.TOTP(secret)
if not totp.verify(token, valid_window=1):
raise ValueError("Invalid token.")
conn, c = open_database(env, with_connection=True)
c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (get_user_id(email, c), type, secret, label))
conn.commit()
elif type == "webauthn":
attestationObject, clientDataJSON = args
rp = pywarp.RelyingPartyManager(
get_relying_party_name(env),
rp_id=env["PRIMARY_HOSTNAME"], # must match hostname the control panel is served from
credential_storage_backend=WebauthnStorageBackend(env))
rp.register(attestation_object=base64.b64decode(attestationObject), client_data_json=base64.b64decode(clientDataJSON), email=email.encode("utf8")) # encoding of email is a little funky here, pywarp calls .decode() with no args?
else:
raise ValueError("Invalid MFA type.")
conn, c = open_database(env, with_connection=True)
c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (get_user_id(email, c), type, secret, label))
conn.commit()
def set_mru_token(email, mfa_id, token, env):
conn, c = open_database(env, with_connection=True)
@@ -93,9 +71,6 @@ def validate_totp_secret(secret):
if len(secret) != 32:
raise ValueError("Secret should be a 32 characters base32 string")
def get_relying_party_name(env):
return env["PRIMARY_HOSTNAME"] + " Mail-in-a-Box Control Panel"
def provision_totp(email, env):
# Make a new secret.
secret = base64.b32encode(os.urandom(20)).decode('utf-8')
@@ -104,7 +79,7 @@ def provision_totp(email, env):
# Make a URI that we encode within a QR code.
uri = pyotp.TOTP(secret).provisioning_uri(
name=email,
issuer_name=get_relying_party_name(env)
issuer_name=env["PRIMARY_HOSTNAME"] + " Mail-in-a-Box Control Panel"
)
# Generate a QR code as a base64-encode PNG image.
@@ -119,55 +94,6 @@ def provision_totp(email, env):
"qr_code_base64": png_b64
}
class WebauthnStorageBackend(pywarp.backends.CredentialStorageBackend):
def __init__(self, env):
self.env = env
def get_record(self, email, conn=None, c=None):
# Get an existing record and parse the 'secret' column as JSON.
if conn is None: conn, c = open_database(self.env, with_connection=True)
c.execute('SELECT secret FROM mfa WHERE user_id=? AND type="webauthn"', (get_user_id(email, c),))
config = c.fetchone()
if config:
try:
return json.loads(config[0])
except:
pass
return { }
def update_record(self, email, fields):
# Update the webauthn record in the database for this user by
# merging the fields with the existing fields in the database.
conn, c = open_database(self.env, with_connection=True)
config = self.get_record(email, conn=conn, c=c)
if config:
# Merge and update.
config.update(fields)
config = json.dumps(config)
c.execute('UPDATE mfa SET secret=? WHERE user_id=? AND type="webauthn"', (config, get_user_id(email, c),))
conn.commit()
return
# Either there's no existing webauthn record or it's corrupted. Delete any existing record.
# Then add a new record.
c.execute('DELETE FROM mfa WHERE user_id=? AND type="webauthn"', (get_user_id(email, c),))
c.execute('INSERT INTO mfa (user_id, type, secret, label) VALUES (?, ?, ?, ?)', (
get_user_id(email, c), "webauthn",
json.dumps(fields),
"WebAuthn"))
conn.commit()
def save_challenge_for_user(self, email, challenge, type):
self.update_record(email, { type + "challenge": base64.b64encode(challenge).decode("ascii") })
def get_challenge_for_user(self, email, type):
challenge = self.get_record(email).get(type + "challenge")
if challenge: challenge = base64.b64decode(challenge.encode("ascii"))
return challenge
def provision_webauthn(email, env):
rp = pywarp.RelyingPartyManager(
get_relying_party_name(env),
rp_id=env["PRIMARY_HOSTNAME"], # must match hostname the control panel is served from
credential_storage_backend=WebauthnStorageBackend(env))
return rp.get_registration_options(email=email)
def validate_auth_mfa(email, request, env):
# Validates that a login request satisfies any MFA modes
# that have been enabled for the user's account. Returns

View File

@@ -38,7 +38,7 @@
<p class="alert" role="alert">
<span class="glyphicon glyphicon-info-sign"></span>
You may encounter zone file errors when attempting to create a TXT record with a long string.
<a href="http://tools.ietf.org/html/rfc4408#section-3.1.3">RFC 4408</a> states a TXT record is allowed to contain multiple strings, and this technique can be used to construct records that would exceed the 255-byte maximum length.
<a href="https://tools.ietf.org/html/rfc4408#section-3.1.3">RFC 4408</a> states a TXT record is allowed to contain multiple strings, and this technique can be used to construct records that would exceed the 255-byte maximum length.
You may need to adopt this technique when adding DomainKeys. Use a tool like <code>named-checkzone</code> to validate your zone file.
</p>

View File

@@ -1,10 +1,34 @@
<style>
.twofactor #totp-setup,
.twofactor #disable-2fa,
.twofactor #output-2fa {
display: none;
}
.twofactor.loaded .loading-indicator {
display: none;
}
.twofactor.disabled #disable-2fa,
.twofactor.enabled #totp-setup {
display: none;
}
.twofactor.disabled #totp-setup,
.twofactor.enabled #disable-2fa {
display: block;
}
.twofactor #totp-setup-qr img {
display: block;
width: 256px;
max-width: 100%;
height: auto;
}
.twofactor #output-2fa.visible {
display: block;
}
</style>
<h2>Two-Factor Authentication</h2>
@@ -27,11 +51,10 @@ and ensure every administrator account for this control panel does the same.</st
</div>
<div class="twofactor">
<div id="mfa-devices">
</div>
<div class="loading-indicator">Loading...</div>
<form id="totp-setup" style="display: none">
<h3>Add a TOTP Device</h3>
<form id="totp-setup">
<h3>Setup Instructions</h3>
<div class="form-group">
<p>1. Install <a href="https://freeotp.github.io/">FreeOTP</a> or <a href="https://www.pcworld.com/article/3225913/what-is-two-factor-authentication-and-which-2fa-apps-are-best.html">any
@@ -62,32 +85,24 @@ and ensure every administrator account for this control panel does the same.</st
</div>
</form>
<form id="webauthn-setup" style="display: none">
<h3>Add a WebAuthn Device</h3>
<p>If you have a WebAuthn device such as a YubiKey, plug it in and click Add WebAuthn Device.</p>
<button type="submit" class="btn" onclick="return do_enable_webauthn()">Add WebAuthn Device</button>
</form>
<div id="webauthn-setup" style="display: none">
</div>
<div id="output-2fa" class="panel panel-danger hidden">
<div class="panel-body"></div>
</div>
<div id="mfa-device-templates" style="display: none">
<form class="totp" style="margin: 1em 0; border: 1px solid #AAA; padding: 10px;">
<p>Two-factor authentication is active for your account<span class="mfa-device-label"></span>.</p>
<form id="disable-2fa">
<div class="form-group">
<button type="submit" class="btn btn-danger">Disable TOTP Device</button>
<p>Two-factor authentication is active for your account<span id="mfa-device-label"></span>.</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>
<p style="margin-bottom: 0">You will have to log into the admin panel again after disabling two-factor authentication.</p>
</form>
<div id="output-2fa" class="panel panel-danger">
<div class="panel-body"></div>
</div>
</div>
<script>
var el = {
disableForm: document.getElementById('disable-2fa'),
output: document.getElementById('output-2fa'),
totpSetupForm: document.getElementById('totp-setup'),
totpSetupToken: document.getElementById('totp-setup-token'),
@@ -95,7 +110,6 @@ and ensure every administrator account for this control panel does the same.</st
totpSetupLabel: document.getElementById('totp-setup-label'),
totpQr: document.getElementById('totp-setup-qr'),
totpSetupSubmit: document.querySelector('#totp-setup-submit'),
webauthnSetupForm: document.getElementById('webauthn-setup'),
wrapper: document.querySelector('.twofactor')
}
@@ -116,8 +130,6 @@ and ensure every administrator account for this control panel does the same.</st
}
function render_totp_setup(provisioned_totp) {
$(el.totpSetupForm).show();
var img = document.createElement('img');
img.src = "data:image/png;base64," + provisioned_totp.qr_code_base64;
@@ -135,50 +147,38 @@ and ensure every administrator account for this control panel does the same.</st
el.wrapper.classList.add('disabled');
}
function arrayBufferToBase64(a) { return btoa(String.fromCharCode(...new Uint8Array(a))); }
function base64ToArrayBuffer(b) { return Uint8Array.from(atob(b), c => c.charCodeAt(0)); }
function render_webauthn_setup(provisioning) {
$(el.webauthnSetupForm).show();
provisioning.challenge = base64ToArrayBuffer(provisioning.challenge);
provisioning.user.id = new TextEncoder().encode(provisioning.user.name);
window.mailinabix_mfa_webauthn_provision = provisioning;
}
function render_disable(mfa) {
var panel = $('#mfa-device-templates .' + mfa.type).clone();
$('#mfa-devices').append(panel);
panel.attr('data-mfa-id', mfa.id);
panel.on('submit', do_disable);
el.disableForm.addEventListener('submit', do_disable);
el.wrapper.classList.add('enabled');
if (mfa.label)
panel.find(".mfa-device-label").text(" on device '" + mfa.label + "'");
$("#mfa-device-label").text(" on device '" + mfa.label + "'");
}
function hide_error() {
el.output.querySelector('.panel-body').innerHTML = '';
el.output.classList.add('hidden');
el.output.classList.remove('visible');
}
function render_error(msg) {
el.output.querySelector('.panel-body').innerHTML = msg;
el.output.classList.remove('hidden');
el.output.classList.add('visible');
}
function reset_view() {
el.wrapper.classList.remove('loaded', 'disabled', 'enabled');
$('#mfa-devices > *').remove();
el.disableForm.removeEventListener('submit', do_disable);
hide_error();
$(el.totpSetupForm).hide();
el.totpSetupForm.reset();
el.totpSetupForm.removeEventListener('submit', do_enable_totp);
el.totpSetupSecret.setAttribute('value', '');
el.totpSetupToken.removeEventListener('input', update_setup_disabled);
el.totpSetupSubmit.setAttribute('disabled', '');
el.totpQr.innerHTML = '';
$(el.webauthnSetupForm).hide();
}
function show_mfa() {
@@ -191,16 +191,16 @@ and ensure every administrator account for this control panel does the same.</st
function(res) {
el.wrapper.classList.add('loaded');
var has_mfa = false;
res.enabled_mfa.forEach(function(mfa) {
if (mfa.type == "totp") {
render_disable(mfa);
has_mfa = true;
}
});
if (res.new_mfa.totp)
if (!has_mfa)
render_totp_setup(res.new_mfa.totp);
if (res.new_mfa.webauthn && 'credentials' in navigator)
render_webauthn_setup(res.new_mfa.webauthn);
}
);
}
@@ -212,7 +212,7 @@ and ensure every administrator account for this control panel does the same.</st
api(
'/mfa/disable',
'POST',
{ id: $(this).attr('data-mfa-id') },
{ type: 'totp' },
function() {
do_logout();
}
@@ -226,7 +226,7 @@ and ensure every administrator account for this control panel does the same.</st
hide_error();
api(
'/mfa/enable/totp',
'/mfa/totp/enable',
'POST',
{
token: $(el.totpSetupToken).val(),
@@ -239,22 +239,4 @@ and ensure every administrator account for this control panel does the same.</st
return false;
}
function do_enable_webauthn() {
navigator.credentials.create({ publicKey: window.mailinabix_mfa_webauthn_provision })
.then(function(creds) {
api(
'/mfa/enable/webauthn',
'POST',
{
attestationObject: arrayBufferToBase64(creds.response['attestationObject']),
clientDataJSON: arrayBufferToBase64(creds.response['clientDataJSON'])
},
function(res) { do_logout(); },
function(res) { render_error(res); }
);
});
return false;
}
</script>

View File

@@ -30,9 +30,9 @@
<table class="table">
<thead><tr><th>For...</th> <th>Use...</th></tr></thead>
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVdroid</a> ($3.69; free <a href="https://f-droid.org/packages/at.bitfire.davdroid/">here</a>)</td></tr>
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free beta</a> (free)</td></tr>
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.89)</td></tr>
<tr><td>Contacts and Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=at.bitfire.davdroid">DAVx⁵</a> ($5.99; free <a href="https://f-droid.org/packages/at.bitfire.davdroid/">here</a>)</td></tr>
<tr><td>Only Contacts</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.carddav.sync">CardDAV-Sync free</a> (free)</td></tr>
<tr><td>Only Calendar</td> <td><a href="https://play.google.com/store/apps/details?id=org.dmfs.caldav.lib">CalDAV-Sync</a> ($2.99)</td></tr>
</table>
<p>Use the following settings:</p>

View File

@@ -20,7 +20,7 @@ if [ -z "$TAG" ]; then
# want to display in status checks.
if [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/18\.04\.[0-9]/18.04/' )" == "Ubuntu 18.04 LTS" ]; then
# This machine is running Ubuntu 18.04.
TAG=v0.54
TAG=v55
elif [ "$(lsb_release -d | sed 's/.*:\s*//' | sed 's/14\.04\.[0-9]/14.04/' )" == "Ubuntu 14.04 LTS" ]; then
# This machine is running Ubuntu 14.04.

View File

@@ -50,7 +50,7 @@ hide_output $venv/bin/pip install --upgrade pip
hide_output $venv/bin/pip install --upgrade \
rtyaml "email_validator>=1.0.0" "exclusiveprocess" \
flask dnspython python-dateutil expiringdict \
qrcode[pil] pyotp pywarp \
qrcode[pil] pyotp \
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver b2sdk
# CONFIGURATION

View File

@@ -29,8 +29,8 @@ apt_install \
# Combine the Roundcube version number with the commit hash of plugins to track
# whether we have the latest version of everything.
VERSION=1.5-rc
HASH=a7cb2a39702536d769c7ff93f716e27f0b93f9d9
VERSION=1.5.0
HASH=2a9d11d9c10c8e8756120606c47eef702f00fe6d
PERSISTENT_LOGIN_VERSION=6b3fc450cae23ccb2f393d0ef67aa319e877e435 # version 5.2.0
HTML5_NOTIFIER_VERSION=68d9ca194212e15b3c7225eb6085dbcf02fd13d7 # version 0.6.4+
CARDDAV_VERSION=3.0.3