auth.py 5.8 KB

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