__filename__="auth.py" __author__="Bob Mottram" __license__="AGPL3+" __version__="1.1.0" __maintainer__="Bob Mottram" __email__="bob@freedombone.net" __status__="Production" import base64 import hashlib import binascii import os import shutil import random 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','') return 'Basic '+base64.b64encode(authStr.encode('utf-8')).decode('utf-8') def authorizeBasic(baseDir: str,path: str,authHeader: str,debug: bool) -> bool: """HTTP basic auth """ if ' ' not in authHeader: if debug: print('DEBUG: Authorixation header does not '+ \ 'contain a space character') return False if '/users/' not in path and \ '/channel/' not in path and \ '/profile/' 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] base64Str=authHeader.split(' ')[1].replace('\n','') plain=base64.b64decode(base64Str).decode('utf-8') if ':' not in plain: if debug: print('DEBUG: Basic Auth header does not contain a ":" '+ \ 'separator for username:password') return False nickname=plain.split(':')[0] if nickname!=nicknameFromPath: if debug: print('DEBUG: Nickname given in the path ('+nicknameFromPath+ \ ') does not match the one in the Authorization header ('+ \ nickname+')') return False passwordFile=baseDir+'/accounts/passwords' if not os.path.isfile(passwordFile): if debug: print('DEBUG: passwords file missing') return False providedPassword=plain.split(':')[1] passfile=open(passwordFile, "r") for line in passfile: if line.startswith(nickname+':'): storedPassword=line.split(':')[1].replace('\n','') 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) return False def storeBasicCredentials(baseDir: str,nickname: str,password: str) -> bool: """Stores login credentials to a file """ 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 def removePassword(baseDir: str,nickname: str) -> None: """Removes the password entry for the given nickname This is called during account removal """ 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) def authorize(baseDir: str,path: str,authHeader: str,debug: bool) -> bool: """Authorize using http header """ if authHeader.lower().startswith('basic '): return authorizeBasic(baseDir,path,authHeader,debug) return False def createPassword(length=10): validChars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' return ''.join((random.choice(validChars) for i in range(length)))