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:
parent
700188c443
commit
53ec0f39cb
|
@ -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')
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue