| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | __filename__ = "auth.py" | 
					
						
							|  |  |  | __author__ = "Bob Mottram" | 
					
						
							|  |  |  | __license__ = "AGPL3+" | 
					
						
							|  |  |  | __version__ = "1.1.0" | 
					
						
							|  |  |  | __maintainer__ = "Bob Mottram" | 
					
						
							|  |  |  | __email__ = "bob@freedombone.net" | 
					
						
							|  |  |  | __status__ = "Production" | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | import base64 | 
					
						
							|  |  |  | import hashlib | 
					
						
							|  |  |  | import binascii | 
					
						
							|  |  |  | import os | 
					
						
							| 
									
										
										
										
											2019-07-05 11:27:18 +00:00
										 |  |  | import random | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | def hashPassword(password: str) -> str: | 
					
						
							|  |  |  |     """Hash a password for storing
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii') | 
					
						
							|  |  |  |     pwdhash = hashlib.pbkdf2_hmac('sha512', | 
					
						
							|  |  |  |                                   password.encode('utf-8'), | 
					
						
							|  |  |  |                                   salt, 100000) | 
					
						
							|  |  |  |     pwdhash = binascii.hexlify(pwdhash) | 
					
						
							|  |  |  |     return (salt + pwdhash).decode('ascii') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def verifyPassword(storedPassword: str, providedPassword: str) -> bool: | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     """Verify a stored password against one provided by user
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     salt = storedPassword[:64] | 
					
						
							|  |  |  |     storedPassword = storedPassword[64:] | 
					
						
							|  |  |  |     pwdhash = hashlib.pbkdf2_hmac('sha512', | 
					
						
							|  |  |  |                                   providedPassword.encode('utf-8'), | 
					
						
							|  |  |  |                                   salt.encode('ascii'), | 
					
						
							|  |  |  |                                   100000) | 
					
						
							|  |  |  |     pwdhash = binascii.hexlify(pwdhash).decode('ascii') | 
					
						
							|  |  |  |     return pwdhash == storedPassword | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def createBasicAuthHeader(nickname: str, password: str) -> str: | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     """This is only used by tests
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-05-22 11:32:38 +00:00
										 |  |  |     authStr = \ | 
					
						
							|  |  |  |         nickname.replace('\n', '').replace('\r', '') + \ | 
					
						
							|  |  |  |         ':' + \ | 
					
						
							|  |  |  |         password.replace('\n', '').replace('\r', '') | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     return 'Basic ' + base64.b64encode(authStr.encode('utf-8')).decode('utf-8') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def authorizeBasic(baseDir: str, path: str, authHeader: str, | 
					
						
							|  |  |  |                    debug: bool) -> bool: | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     """HTTP basic auth
 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     if ' ' not in authHeader: | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |         if debug: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |             print('DEBUG: Authorixation header does not ' + | 
					
						
							| 
									
										
										
										
											2020-03-30 19:09:45 +00:00
										 |  |  |                   'contain a space character') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2019-10-17 22:26:47 +00:00
										 |  |  |     if '/users/' not in path and \ | 
					
						
							|  |  |  |        '/channel/' not in path and \ | 
					
						
							|  |  |  |        '/profile/' not in path: | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |         if debug: | 
					
						
							|  |  |  |             print('DEBUG: Path for Authorization does not contain a user') | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     pathUsersSection = path.split('/users/')[1] | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |     if '/' not in pathUsersSection: | 
					
						
							|  |  |  |         if debug: | 
					
						
							|  |  |  |             print('DEBUG: This is not a users endpoint') | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     nicknameFromPath = pathUsersSection.split('/')[0] | 
					
						
							| 
									
										
										
										
											2020-05-22 11:32:38 +00:00
										 |  |  |     base64Str = \ | 
					
						
							|  |  |  |         authHeader.split(' ')[1].replace('\n', '').replace('\r', '') | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     plain = base64.b64decode(base64Str).decode('utf-8') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     if ':' not in plain: | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |         if debug: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |             print('DEBUG: Basic Auth header does not contain a ":" ' + | 
					
						
							| 
									
										
										
										
											2020-03-30 19:09:45 +00:00
										 |  |  |                   'separator for username:password') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     nickname = plain.split(':')[0] | 
					
						
							|  |  |  |     if nickname != nicknameFromPath: | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |         if debug: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |             print('DEBUG: Nickname given in the path (' + nicknameFromPath + | 
					
						
							|  |  |  |                   ') does not match the one in the Authorization header (' + | 
					
						
							|  |  |  |                   nickname + ')') | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     passwordFile = baseDir+'/accounts/passwords' | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     if not os.path.isfile(passwordFile): | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |         if debug: | 
					
						
							|  |  |  |             print('DEBUG: passwords file missing') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     providedPassword = plain.split(':')[1] | 
					
						
							|  |  |  |     passfile = open(passwordFile, "r") | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     for line in passfile: | 
					
						
							|  |  |  |         if line.startswith(nickname+':'): | 
					
						
							| 
									
										
										
										
											2020-05-22 11:32:38 +00:00
										 |  |  |             storedPassword = \ | 
					
						
							|  |  |  |                 line.split(':')[1].replace('\n', '').replace('\r', '') | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |             success = verifyPassword(storedPassword, providedPassword) | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |             if not success: | 
					
						
							|  |  |  |                 if debug: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |                     print('DEBUG: Password check failed for ' + nickname) | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |             return success | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     print('DEBUG: Did not find credentials for ' + nickname + | 
					
						
							|  |  |  |           ' in ' + passwordFile) | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def storeBasicCredentials(baseDir: str, nickname: str, password: str) -> bool: | 
					
						
							| 
									
										
										
										
											2019-07-05 09:51:58 +00:00
										 |  |  |     """Stores login credentials to a file
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     if ':' in nickname or ':' in password: | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-05-22 11:32:38 +00:00
										 |  |  |     nickname = nickname.replace('\n', '').replace('\r', '').strip() | 
					
						
							|  |  |  |     password = password.replace('\n', '').replace('\r', '').strip() | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     if not os.path.isdir(baseDir + '/accounts'): | 
					
						
							|  |  |  |         os.mkdir(baseDir + '/accounts') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     passwordFile = baseDir + '/accounts/passwords' | 
					
						
							|  |  |  |     storeStr = nickname + ':' + hashPassword(password) | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     if os.path.isfile(passwordFile): | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |         if nickname + ':' in open(passwordFile).read(): | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |             with open(passwordFile, "r") as fin: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |                 with open(passwordFile + '.new', "w") as fout: | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |                     for line in fin: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |                         if not line.startswith(nickname + ':'): | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |                             fout.write(line) | 
					
						
							|  |  |  |                         else: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |                             fout.write(storeStr + '\n') | 
					
						
							|  |  |  |             os.rename(passwordFile + '.new', passwordFile) | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |         else: | 
					
						
							|  |  |  |             # append to password file | 
					
						
							|  |  |  |             with open(passwordFile, "a") as passfile: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |                 passfile.write(storeStr + '\n') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     else: | 
					
						
							|  |  |  |         with open(passwordFile, "w") as passfile: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |             passfile.write(storeStr + '\n') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def removePassword(baseDir: str, nickname: str) -> None: | 
					
						
							| 
									
										
										
										
											2019-07-05 09:51:58 +00:00
										 |  |  |     """Removes the password entry for the given nickname
 | 
					
						
							|  |  |  |     This is called during account removal | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     passwordFile = baseDir + '/accounts/passwords' | 
					
						
							| 
									
										
										
										
											2019-07-05 09:49:57 +00:00
										 |  |  |     if os.path.isfile(passwordFile): | 
					
						
							|  |  |  |         with open(passwordFile, "r") as fin: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |             with open(passwordFile + '.new', "w") as fout: | 
					
						
							| 
									
										
										
										
											2019-07-05 09:49:57 +00:00
										 |  |  |                 for line in fin: | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |                     if not line.startswith(nickname + ':'): | 
					
						
							| 
									
										
										
										
											2019-07-05 09:49:57 +00:00
										 |  |  |                         fout.write(line) | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |         os.rename(passwordFile + '.new', passwordFile) | 
					
						
							| 
									
										
										
										
											2019-07-05 09:49:57 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def authorize(baseDir: str, path: str, authHeader: str, debug: bool) -> bool: | 
					
						
							| 
									
										
										
										
											2019-07-05 09:51:58 +00:00
										 |  |  |     """Authorize using http header
 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     if authHeader.lower().startswith('basic '): | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |         return authorizeBasic(baseDir, path, authHeader, debug) | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     return False | 
					
						
							| 
									
										
										
										
											2019-07-05 11:27:18 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-05 11:27:18 +00:00
										 |  |  | def createPassword(length=10): | 
					
						
							| 
									
										
										
										
											2020-04-01 19:29:56 +00:00
										 |  |  |     validChars = 'abcdefghijklmnopqrstuvwxyz' + \ | 
					
						
							|  |  |  |         'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' | 
					
						
							| 
									
										
										
										
											2019-07-05 11:27:18 +00:00
										 |  |  |     return ''.join((random.choice(validChars) for i in range(length))) |