| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | __filename__ = "auth.py" | 
					
						
							|  |  |  | __author__ = "Bob Mottram" | 
					
						
							|  |  |  | __license__ = "AGPL3+" | 
					
						
							|  |  |  | __version__ = "0.0.1" | 
					
						
							|  |  |  | __maintainer__ = "Bob Mottram" | 
					
						
							|  |  |  | __email__ = "bob@freedombone.net" | 
					
						
							|  |  |  | __status__ = "Production" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import base64 | 
					
						
							|  |  |  | import hashlib | 
					
						
							|  |  |  | import binascii | 
					
						
							|  |  |  | import os | 
					
						
							|  |  |  | import shutil | 
					
						
							| 
									
										
										
										
											2019-07-05 11:27:18 +00:00
										 |  |  | import random | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def hashPassword(password: str) -> str: | 
					
						
							|  |  |  |     """Hash a password for storing
 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     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: | 
					
						
							|  |  |  |     """Verify a stored password against one provided by user
 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     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: | 
					
						
							|  |  |  |     """This is only used by tests
 | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     authStr=nickname.replace('\n','')+':'+password.replace('\n','') | 
					
						
							| 
									
										
										
										
											2019-07-03 19:15:42 +00:00
										 |  |  |     return 'Basic '+base64.b64encode(authStr.encode('utf-8')).decode('utf-8') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +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: | 
					
						
							|  |  |  |             print('DEBUG: Authorixation header does not contain a space character') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |         return False | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |     if '/users/' not in path: | 
					
						
							|  |  |  |         if debug: | 
					
						
							|  |  |  |             print('DEBUG: Path for Authorization does not contain a user') | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     pathUsersSection=path.split('/users/')[1] | 
					
						
							|  |  |  |     if '/' not in pathUsersSection: | 
					
						
							|  |  |  |         if debug: | 
					
						
							|  |  |  |             print('DEBUG: This is not a users endpoint') | 
					
						
							|  |  |  |         return False | 
					
						
							|  |  |  |     nicknameFromPath=pathUsersSection.split('/')[0] | 
					
						
							| 
									
										
										
										
											2019-07-03 18:28:43 +00:00
										 |  |  |     base64Str = authHeader.split(' ')[1].replace('\n','') | 
					
						
							| 
									
										
										
										
											2019-07-03 19:15:42 +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: | 
					
						
							|  |  |  |             print('DEBUG: Basic Auth header does not contain a ":" separator for username:password') | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |         return False | 
					
						
							|  |  |  |     nickname = plain.split(':')[0] | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |     if nickname!=nicknameFromPath: | 
					
						
							|  |  |  |         if debug: | 
					
						
							| 
									
										
										
										
											2019-07-06 17:00:22 +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 | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     passwordFile=baseDir+'/accounts/passwords' | 
					
						
							|  |  |  |     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 | 
					
						
							|  |  |  |     providedPassword = plain.split(':')[1] | 
					
						
							|  |  |  |     passfile = open(passwordFile, "r") | 
					
						
							|  |  |  |     for line in passfile: | 
					
						
							|  |  |  |         if line.startswith(nickname+':'): | 
					
						
							|  |  |  |             storedPassword=line.split(':')[1].replace('\n','') | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +00:00
										 |  |  |             success = verifyPassword(storedPassword,providedPassword) | 
					
						
							|  |  |  |             if not success: | 
					
						
							|  |  |  |                 if debug: | 
					
						
							|  |  |  |                     print('DEBUG: Password check failed for '+nickname) | 
					
						
							|  |  |  |             return success | 
					
						
							|  |  |  |     print('DEBUG: Did not find credentials for '+nickname+' in '+passwordFile) | 
					
						
							| 
									
										
										
										
											2019-07-03 18:24:44 +00:00
										 |  |  |     return False | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 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 | 
					
						
							|  |  |  |     nickname=nickname.replace('\n','').strip() | 
					
						
							|  |  |  |     password=password.replace('\n','').strip() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if not os.path.isdir(baseDir+'/accounts'): | 
					
						
							|  |  |  |         os.mkdir(baseDir+'/accounts') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     passwordFile=baseDir+'/accounts/passwords' | 
					
						
							|  |  |  |     storeStr=nickname+':'+hashPassword(password) | 
					
						
							|  |  |  |     if os.path.isfile(passwordFile): | 
					
						
							|  |  |  |         if nickname+':' in open(passwordFile).read(): | 
					
						
							|  |  |  |             with open(passwordFile, "r") as fin: | 
					
						
							|  |  |  |                 with open(passwordFile+'.new', "w") as fout: | 
					
						
							|  |  |  |                     for line in fin: | 
					
						
							|  |  |  |                         if not line.startswith(nickname+':'): | 
					
						
							|  |  |  |                             fout.write(line) | 
					
						
							|  |  |  |                         else: | 
					
						
							|  |  |  |                             fout.write(storeStr+'\n') | 
					
						
							|  |  |  |             os.rename(passwordFile+'.new', passwordFile) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             # append to password file | 
					
						
							|  |  |  |             with open(passwordFile, "a") as passfile: | 
					
						
							|  |  |  |                 passfile.write(storeStr+'\n') | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         with open(passwordFile, "w") as passfile: | 
					
						
							|  |  |  |             passfile.write(storeStr+'\n') | 
					
						
							|  |  |  |     return True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-05 09:49:57 +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 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2019-07-05 09:49:57 +00:00
										 |  |  |     passwordFile=baseDir+'/accounts/passwords' | 
					
						
							|  |  |  |     if os.path.isfile(passwordFile): | 
					
						
							|  |  |  |         with open(passwordFile, "r") as fin: | 
					
						
							|  |  |  |             with open(passwordFile+'.new', "w") as fout: | 
					
						
							|  |  |  |                 for line in fin: | 
					
						
							|  |  |  |                     if not line.startswith(nickname+':'): | 
					
						
							|  |  |  |                         fout.write(line) | 
					
						
							|  |  |  |         os.rename(passwordFile+'.new', passwordFile) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +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 '): | 
					
						
							| 
									
										
										
										
											2019-07-04 08:56:15 +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
										 |  |  | 
 | 
					
						
							|  |  |  | def createPassword(length=10): | 
					
						
							|  |  |  |     validChars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' | 
					
						
							|  |  |  |     return ''.join((random.choice(validChars) for i in range(length))) |