epicyon/auth.py

149 lines
5.4 KiB
Python
Raw Normal View History

2020-03-22 20:36:19 +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
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
"""
2020-03-22 20:36:19 +00:00
salt=hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
pwdhash=hashlib.pbkdf2_hmac('sha512', \
2020-03-22 20:53:47 +00:00
password.encode('utf-8'), \
salt, 100000)
2020-03-22 20:36:19 +00:00
pwdhash=binascii.hexlify(pwdhash)
return (salt+pwdhash).decode('ascii')
2020-03-22 21:16:02 +00:00
2019-07-03 18:24:44 +00:00
def verifyPassword(storedPassword: str,providedPassword: str) -> bool:
"""Verify a stored password against one provided by user
"""
2020-03-22 20:36:19 +00:00
salt=storedPassword[:64]
storedPassword=storedPassword[64:]
pwdhash=hashlib.pbkdf2_hmac('sha512', \
2020-03-22 20:53:47 +00:00
providedPassword.encode('utf-8'), \
salt.encode('ascii'), \
100000)
2020-03-22 20:36:19 +00:00
pwdhash=binascii.hexlify(pwdhash).decode('ascii')
return pwdhash==storedPassword
2019-07-03 18:24:44 +00:00
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-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
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]
2020-03-22 20:36:19 +00:00
base64Str=authHeader.split(' ')[1].replace('\n','')
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
2020-03-22 20:36:19 +00:00
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
2020-03-22 20:36:19 +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+':'):
storedPassword=line.split(':')[1].replace('\n','')
2020-03-22 20:36:19 +00:00
success=verifyPassword(storedPassword,providedPassword)
2019-07-04 08:56:15 +00:00
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)))