mirror of
				https://github.com/mail-in-a-box/mailinabox.git
				synced 2025-10-30 18:50:53 +00:00 
			
		
		
		
	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
	
	Block a user