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
|
||||
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_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
|
||||
|
@ -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)
|
||||
|
@ -36,6 +31,8 @@ class KeyAuthService:
|
|||
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:
|
||||
|
@ -72,8 +69,9 @@ class KeyAuthService:
|
|||
|
||||
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
|
||||
|
@ -136,8 +134,8 @@ class KeyAuthService:
|
|||
# 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.
|
||||
|
@ -153,6 +151,3 @@ class KeyAuthService:
|
|||
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')
|
||||
|
|
|
@ -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.:
|
||||
#
|
||||
|
@ -22,7 +25,7 @@ from mfa import get_public_mfa_state, provision_totp, validate_totp_secret, enab
|
|||
|
||||
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__
|
||||
|
@ -724,30 +727,10 @@ if __name__ == '__main__':
|
|||
# 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)
|
||||
|
|
Loading…
Reference in New Issue