Use 'secrets' to generate the system API key and remove some debugging-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
This commit is contained in:
Joshua Tauberer 2021-08-22 15:02:38 -04:00
parent 700188c443
commit 53ec0f39cb
2 changed files with 17 additions and 39 deletions

View File

@ -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 import utils
from mailconfig import get_mail_password, get_mail_user_privileges from mailconfig import get_mail_password, get_mail_user_privileges
@ -9,7 +8,7 @@ from mfa import get_hash_mfa_state, validate_auth_mfa
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'
class KeyAuthService: class AuthService:
"""Generate an API key for authenticating clients """Generate an API key for authenticating clients
Clients must read the key from the key file and send the key with all HTTP Clients must read the key from the key file and send the key with all HTTP
@ -18,16 +17,12 @@ class KeyAuthService:
""" """
def __init__(self): def __init__(self):
self.auth_realm = DEFAULT_AUTH_REALM self.auth_realm = DEFAULT_AUTH_REALM
self.key = self._generate_key()
self.key_path = DEFAULT_KEY_PATH self.key_path = DEFAULT_KEY_PATH
self.init_system_api_key()
def write_key(self): def init_system_api_key(self):
"""Write key to file so authorized clients can get the key """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): def create_file_with_mode(path, mode):
# Based on answer by A-B-B: http://stackoverflow.com/a/15015748 # Based on answer by A-B-B: http://stackoverflow.com/a/15015748
old_umask = os.umask(0) old_umask = os.umask(0)
@ -36,6 +31,8 @@ class KeyAuthService:
finally: finally:
os.umask(old_umask) os.umask(old_umask)
self.key = secrets.token_hex(24)
os.makedirs(os.path.dirname(self.key_path), exist_ok=True) os.makedirs(os.path.dirname(self.key_path), exist_ok=True)
with create_file_with_mode(self.key_path, 0o640) as key_file: with create_file_with_mode(self.key_path, 0o640) as key_file:
@ -72,8 +69,9 @@ class KeyAuthService:
if username in (None, ""): if username in (None, ""):
raise ValueError("Authorization header invalid.") 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"]) return (None, ["admin"])
else: else:
# The user is trying to log in with a username and either a password # The user is trying to log in with a username and either a password
@ -136,8 +134,8 @@ class KeyAuthService:
# email address, current hashed password, and current MFA state, so that the # email address, current hashed password, and current MFA state, so that the
# key becomes invalid if any of that information changes. # 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, # 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 master API key # which also means that the API key becomes invalid when our system API key
# changes --- i.e. when this process is restarted. # changes --- i.e. when this process is restarted.
# #
# Raises ValueError via get_mail_password if the user doesn't exist. # Raises ValueError via get_mail_password if the user doesn't exist.
@ -153,6 +151,3 @@ class KeyAuthService:
hash_key = self.key.encode('ascii') hash_key = self.key.encode('ascii')
return hmac.new(hash_key, msg, digestmod="sha256").hexdigest() 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')

View File

@ -1,5 +1,8 @@
#!/usr/local/lib/mailinabox/env/bin/python3 #!/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 # During development, you can start the Mail-in-a-Box control panel
# by running this script, e.g.: # by running this script, e.g.:
# #
@ -22,7 +25,7 @@ from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enab
env = utils.load_environment() 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. # We may deploy via a symbolic link, which confuses flask's template finding.
me = __file__ me = __file__
@ -724,30 +727,10 @@ if __name__ == '__main__':
# Turn on Flask debugging. # Turn on Flask debugging.
app.debug = True 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: if not app.debug:
app.logger.addHandler(utils.create_syslog_handler()) app.logger.addHandler(utils.create_syslog_handler())
# For testing on the command line, you can use `curl` like so: #app.logger.info('API key: ' + auth_service.key)
# 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)
# Start the application server. Listens on 127.0.0.1 (IPv4 only). # Start the application server. Listens on 127.0.0.1 (IPv4 only).
app.run(port=10222) app.run(port=10222)