auth.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. __filename__ = "auth.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "0.0.1"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import base64
  9. import hashlib
  10. import binascii
  11. import os
  12. import shutil
  13. import random
  14. def hashPassword(password: str) -> str:
  15. """Hash a password for storing
  16. """
  17. salt = hashlib.sha256(os.urandom(60)).hexdigest().encode('ascii')
  18. pwdhash = hashlib.pbkdf2_hmac('sha512', password.encode('utf-8'), salt, 100000)
  19. pwdhash = binascii.hexlify(pwdhash)
  20. return (salt + pwdhash).decode('ascii')
  21. def verifyPassword(storedPassword: str,providedPassword: str) -> bool:
  22. """Verify a stored password against one provided by user
  23. """
  24. salt = storedPassword[:64]
  25. storedPassword = storedPassword[64:]
  26. pwdhash = hashlib.pbkdf2_hmac('sha512', \
  27. providedPassword.encode('utf-8'), \
  28. salt.encode('ascii'), \
  29. 100000)
  30. pwdhash = binascii.hexlify(pwdhash).decode('ascii')
  31. return pwdhash == storedPassword
  32. def createBasicAuthHeader(nickname: str,password: str) -> str:
  33. """This is only used by tests
  34. """
  35. authStr=nickname.replace('\n','')+':'+password.replace('\n','')
  36. return 'Basic '+base64.b64encode(authStr.encode('utf-8')).decode('utf-8')
  37. def authorizeBasic(baseDir: str,path: str,authHeader: str,debug: bool) -> bool:
  38. """HTTP basic auth
  39. """
  40. if ' ' not in authHeader:
  41. if debug:
  42. print('DEBUG: Authorixation header does not contain a space character')
  43. return False
  44. if '/users/' not in path:
  45. if debug:
  46. print('DEBUG: Path for Authorization does not contain a user')
  47. return False
  48. pathUsersSection=path.split('/users/')[1]
  49. if '/' not in pathUsersSection:
  50. if debug:
  51. print('DEBUG: This is not a users endpoint')
  52. return False
  53. nicknameFromPath=pathUsersSection.split('/')[0]
  54. base64Str = authHeader.split(' ')[1].replace('\n','')
  55. plain = base64.b64decode(base64Str).decode('utf-8')
  56. if ':' not in plain:
  57. if debug:
  58. print('DEBUG: Basic Auth header does not contain a ":" separator for username:password')
  59. return False
  60. nickname = plain.split(':')[0]
  61. if nickname!=nicknameFromPath:
  62. if debug:
  63. print('DEBUG: Nickname given in the path ('+nicknameFromPath+ \
  64. ') does not match the one in the Authorization header ('+ \
  65. nickname+')')
  66. return False
  67. passwordFile=baseDir+'/accounts/passwords'
  68. if not os.path.isfile(passwordFile):
  69. if debug:
  70. print('DEBUG: passwords file missing')
  71. return False
  72. providedPassword = plain.split(':')[1]
  73. passfile = open(passwordFile, "r")
  74. for line in passfile:
  75. if line.startswith(nickname+':'):
  76. storedPassword=line.split(':')[1].replace('\n','')
  77. success = verifyPassword(storedPassword,providedPassword)
  78. if not success:
  79. if debug:
  80. print('DEBUG: Password check failed for '+nickname)
  81. return success
  82. print('DEBUG: Did not find credentials for '+nickname+' in '+passwordFile)
  83. return False
  84. def storeBasicCredentials(baseDir: str,nickname: str,password: str) -> bool:
  85. """Stores login credentials to a file
  86. """
  87. if ':' in nickname or ':' in password:
  88. return False
  89. nickname=nickname.replace('\n','').strip()
  90. password=password.replace('\n','').strip()
  91. if not os.path.isdir(baseDir+'/accounts'):
  92. os.mkdir(baseDir+'/accounts')
  93. passwordFile=baseDir+'/accounts/passwords'
  94. storeStr=nickname+':'+hashPassword(password)
  95. if os.path.isfile(passwordFile):
  96. if nickname+':' in open(passwordFile).read():
  97. with open(passwordFile, "r") as fin:
  98. with open(passwordFile+'.new', "w") as fout:
  99. for line in fin:
  100. if not line.startswith(nickname+':'):
  101. fout.write(line)
  102. else:
  103. fout.write(storeStr+'\n')
  104. os.rename(passwordFile+'.new', passwordFile)
  105. else:
  106. # append to password file
  107. with open(passwordFile, "a") as passfile:
  108. passfile.write(storeStr+'\n')
  109. else:
  110. with open(passwordFile, "w") as passfile:
  111. passfile.write(storeStr+'\n')
  112. return True
  113. def removePassword(baseDir: str,nickname: str) -> None:
  114. """Removes the password entry for the given nickname
  115. This is called during account removal
  116. """
  117. passwordFile=baseDir+'/accounts/passwords'
  118. if os.path.isfile(passwordFile):
  119. with open(passwordFile, "r") as fin:
  120. with open(passwordFile+'.new', "w") as fout:
  121. for line in fin:
  122. if not line.startswith(nickname+':'):
  123. fout.write(line)
  124. os.rename(passwordFile+'.new', passwordFile)
  125. def authorize(baseDir: str,path: str,authHeader: str,debug: bool) -> bool:
  126. """Authorize using http header
  127. """
  128. if authHeader.lower().startswith('basic '):
  129. return authorizeBasic(baseDir,path,authHeader,debug)
  130. return False
  131. def createPassword(length=10):
  132. validChars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  133. return ''.join((random.choice(validChars) for i in range(length)))