Skip to content

Commit

Permalink
Use 'secrets' to generate the system API key and remove some debuggin…
Browse files Browse the repository at this point in the history
…g-related code

* Rename the 'master' API key to be called the 'system' API key
* Generate the key using the Python secrets module which is meant for this
* Remove some debugging helper code which will be obsoleted by the upcoming changes for session keys
  • Loading branch information
JoshData committed Sep 6, 2021
1 parent 700188c commit 53ec0f3
Show file tree
Hide file tree
Showing 2 changed files with 17 additions and 39 deletions.
29 changes: 12 additions & 17 deletions management/auth.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import base64, os, os.path, hmac, json
import base64, os, os.path, hmac, json, secrets

from flask import make_response

import utils
from mailconfig import get_mail_password, get_mail_user_privileges
Expand All @@ -9,7 +8,7 @@
DEFAULT_KEY_PATH = '/var/lib/mailinabox/api.key'
DEFAULT_AUTH_REALM = 'Mail-in-a-Box Management Server'

class KeyAuthService:
class AuthService:
"""Generate an API key for authenticating clients
Clients must read the key from the key file and send the key with all HTTP
Expand All @@ -18,16 +17,12 @@ class KeyAuthService:
"""
def __init__(self):
self.auth_realm = DEFAULT_AUTH_REALM
self.key = self._generate_key()
self.key_path = DEFAULT_KEY_PATH
self.init_system_api_key()

def write_key(self):
"""Write key to file so authorized clients can get the key
def init_system_api_key(self):
"""Write an API key to a local file so local processes can use the API"""

The key file is created with mode 0640 so that additional users can be
authorized to access the API by granting group/ACL read permissions on
the key file.
"""
def create_file_with_mode(path, mode):
# Based on answer by A-B-B: http://stackoverflow.com/a/15015748
old_umask = os.umask(0)
Expand All @@ -36,6 +31,8 @@ def create_file_with_mode(path, mode):
finally:
os.umask(old_umask)

self.key = secrets.token_hex(24)

os.makedirs(os.path.dirname(self.key_path), exist_ok=True)

with create_file_with_mode(self.key_path, 0o640) as key_file:
Expand Down Expand Up @@ -72,8 +69,9 @@ def parse_basic_auth(header):

if username in (None, ""):
raise ValueError("Authorization header invalid.")
elif username == self.key:
# The user passed the master API key which grants administrative privs.

if username == self.key:
# The user passed the system API key which grants administrative privs.
return (None, ["admin"])
else:
# The user is trying to log in with a username and either a password
Expand Down Expand Up @@ -136,8 +134,8 @@ def create_user_key(self, email, env):
# email address, current hashed password, and current MFA state, so that the
# key becomes invalid if any of that information changes.
#
# Use an HMAC to generate the API key using our master API key as a key,
# which also means that the API key becomes invalid when our master API key
# Use an HMAC to generate the API key using our system API key as a key,
# which also means that the API key becomes invalid when our system API key
# changes --- i.e. when this process is restarted.
#
# Raises ValueError via get_mail_password if the user doesn't exist.
Expand All @@ -153,6 +151,3 @@ def create_user_key(self, email, env):
hash_key = self.key.encode('ascii')
return hmac.new(hash_key, msg, digestmod="sha256").hexdigest()

def _generate_key(self):
raw_key = os.urandom(32)
return base64.b64encode(raw_key).decode('ascii')
27 changes: 5 additions & 22 deletions management/daemon.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/local/lib/mailinabox/env/bin/python3
#
# The API can be accessed on the command line, e.g. use `curl` like so:
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
#
# During development, you can start the Mail-in-a-Box control panel
# by running this script, e.g.:
#
Expand All @@ -22,7 +25,7 @@

env = utils.load_environment()

auth_service = auth.KeyAuthService()
auth_service = auth.AuthService()

# We may deploy via a symbolic link, which confuses flask's template finding.
me = __file__
Expand Down Expand Up @@ -724,30 +727,10 @@ def log_failed_login(request):
# Turn on Flask debugging.
app.debug = True

# Use a stable-ish master API key so that login sessions don't restart on each run.
# Use /etc/machine-id to seed the key with a stable secret, but add something
# and hash it to prevent possibly exposing the machine id, using the time so that
# the key is not valid indefinitely.
import hashlib
with open("/etc/machine-id") as f:
api_key = f.read()
api_key += "|" + str(int(time.time() / (60*60*2)))
hasher = hashlib.sha1()
hasher.update(api_key.encode("ascii"))
auth_service.key = hasher.hexdigest()

if "APIKEY" in os.environ: auth_service.key = os.environ["APIKEY"]

if not app.debug:
app.logger.addHandler(utils.create_syslog_handler())

# For testing on the command line, you can use `curl` like so:
# curl --user $(</var/lib/mailinabox/api.key): http://localhost:10222/mail/users
auth_service.write_key()

# For testing in the browser, you can copy the API key that's output to the
# debug console and enter that as the username
app.logger.info('API key: ' + auth_service.key)
#app.logger.info('API key: ' + auth_service.key)

# Start the application server. Listens on 127.0.0.1 (IPv4 only).
app.run(port=10222)

0 comments on commit 53ec0f3

Please sign in to comment.