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
|
|
|
|
|
|
|
|
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-03 19:32:07 +00:00
|
|
|
def nicknameFromBasicAuth(authHeader: str) -> str:
|
|
|
|
"""Returns the nickname from basic auth header
|
|
|
|
"""
|
|
|
|
if ' ' not in authHeader:
|
|
|
|
return None
|
|
|
|
base64Str = authHeader.split(' ')[1].replace('\n','')
|
|
|
|
plain = base64.b64decode(base64Str).decode('utf-8')
|
|
|
|
if ':' not in plain:
|
|
|
|
return None
|
|
|
|
return plain.split(':')[0]
|
|
|
|
|
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:
|
|
|
|
print('DEBUG: Nickname given in the path ('+nicknameFromPath+') does not match the one in the Authorization header ('+nickname+')')
|
|
|
|
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:
|
|
|
|
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-04 08:56:15 +00:00
|
|
|
def authorize(baseDir: str,path: str,authHeader: str,debug: bool) -> bool:
|
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
|