From e9e6d94e3bf9b13aedc133ef4395759c81b70a56 Mon Sep 17 00:00:00 2001 From: Joshua Tauberer Date: Sat, 6 Jun 2015 12:33:31 +0000 Subject: [PATCH] the control panel auth hmac message should also include the user's password so that resetting a password in the database forces that user to log in to the control panel again; also use a sha256 hmac --- management/auth.py | 25 +++++++++++++++++-------- management/daemon.py | 2 +- management/templates/users.html | 7 ++++++- security.md | 2 ++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/management/auth.py b/management/auth.py index 04e605c..55f5966 100644 --- a/management/auth.py +++ b/management/auth.py @@ -88,8 +88,9 @@ class KeyAuthService: if email == "" or pw == "": raise ValueError("Enter an email address and password.") - # The password might be a user-specific API key. - if hmac.compare_digest(self.create_user_key(email), pw): + # The password might be a user-specific API key. create_user_key raises + # a ValueError if the user does not exist. + if hmac.compare_digest(self.create_user_key(email, env), pw): # OK. pass else: @@ -111,18 +112,26 @@ class KeyAuthService: # Login failed. raise ValueError("Invalid password.") - # Get privileges for authorization. This call should never fail on a valid user, - # but if the caller passed a user-specific API key then the user may no longer - # exist --- in that case, get_mail_user_privileges will return a tuple of an - # error message and an HTTP status code. + # Get privileges for authorization. This call should never fail because by this + # point we know the email address is a valid user. But on error the call will + # return a tuple of an error message and an HTTP status code. privs = get_mail_user_privileges(email, env) if isinstance(privs, tuple): raise ValueError(privs[0]) # Return a list of privileges. return privs - def create_user_key(self, email): - return hmac.new(self.key.encode('ascii'), b"AUTH:" + email.encode("utf8"), digestmod="sha1").hexdigest() + def create_user_key(self, email, env): + # 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 + # course has their own email address and password. We assume they do not have the master + # API key (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 + # in to the control panel again. This method raises a ValueError if the user does + # not exist, due to get_mail_password. + 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() def _generate_key(self): raw_key = os.urandom(32) diff --git a/management/daemon.py b/management/daemon.py index 7115967..00cfd71 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -118,7 +118,7 @@ def me(): # Is authorized as admin? Return an API key for future use. if "admin" in privs: - resp["api_key"] = auth_service.create_user_key(email) + resp["api_key"] = auth_service.create_user_key(email, env) # Return. return json_response(resp) diff --git a/management/templates/users.html b/management/templates/users.html index 8f0c87e..5f8f5f2 100644 --- a/management/templates/users.html +++ b/management/templates/users.html @@ -164,9 +164,14 @@ function do_add_user() { function users_set_password(elem) { var email = $(elem).parents('tr').attr('data-email'); + + var yourpw = ""; + if (api_credentials != null && email == api_credentials[0]) + yourpw = "

If you change your own password, you will be logged out of this control panel and will need to log in again.

"; + show_modal_confirm( "Archive User", - $("

Set a new password for " + email + "?

Passwords must be at least four characters and may not contain spaces.

"), + $("

Set a new password for " + email + "?

Passwords must be at least four characters and may not contain spaces." + yourpw + "

"), "Set Password", function() { api( diff --git a/security.md b/security.md index c3e82d8..bf34b05 100644 --- a/security.md +++ b/security.md @@ -56,6 +56,8 @@ The cipher and protocol selection are chosen to support the following clients: The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py)) +When using the web-based administrative control panel, after logging in an API key is placed in the browser's local storage (rather than, say, the user's actual password). The API key is an HMAC based on the user's email address and current password, and it is keyed by a secret known only to the control panel service. By resetting an administrator's password, any HMACs previously generated for that user will expire. + ### Console access Console access (e.g. via SSH) is configured by the system image used to create the box, typically from by a cloud virtual machine provider (e.g. Digital Ocean). Mail-in-a-Box does not set any console access settings, although it will warn the administrator in the System Status Checks if password-based login is turned on.