auth.py 5.5 KB

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