password.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. import os
  2. import hashlib
  3. import asyncio
  4. class password():
  5. """
  6. This class represents password in the system, it is also used to hash
  7. password, to protect it agains attacks.
  8. Methods
  9. -------
  10. get_salt_length() : int
  11. Return size in bytes of salt for passwords.
  12. get_algorithm() : str
  13. Return name of algorithm used to hash password.
  14. async comare(password: str) : bool
  15. Check that password given as parameter is same as hashed password
  16. stored by password object which that function run on.
  17. result() : str
  18. Get hashed result password, to store it in database.
  19. async from_plain_text(password: str) : password
  20. It create new password object from given plain password. It get
  21. random salt, and create new hash.
  22. async from_hash(hashed: str) : password
  23. It create new password object from already hashed password, it is
  24. usefull to compare password with plain text password.
  25. """
  26. def __init__(self, hashed: str) -> None:
  27. """
  28. It create new password from hashed content, it would not being
  29. called outside this class.
  30. Parameters
  31. ----------
  32. hashed : str
  33. Already hashed password
  34. """
  35. self.__hash = hashed
  36. @staticmethod
  37. def get_salt_length() -> int:
  38. """
  39. It return salt length for the password in the bytes. To add
  40. salt to password it must be string, and it is converted to hex
  41. values. That mean, size of string salt is twice of salt lenght
  42. in bytes.
  43. Returns
  44. -------
  45. int
  46. Lenght of the passwords salt.
  47. """
  48. return 12
  49. @staticmethod
  50. def get_algorithm() -> str:
  51. """
  52. That return name of the algorithm which would be used to hash
  53. password.
  54. Returns
  55. -------
  56. str
  57. Name of the password which would be used to hash password.
  58. """
  59. return "sha512"
  60. @staticmethod
  61. def get_iterations() -> int:
  62. """
  63. That return count of the iterations of the algorithm.
  64. Returns
  65. -------
  66. int
  67. Count of the iterations of the algorithm.
  68. """
  69. return 50000
  70. @staticmethod
  71. async def __generate_hash(
  72. content: str,
  73. salt: str,
  74. iterations: int
  75. ) -> str:
  76. """
  77. That function generate hash of the password from the password
  78. and its salt. That function is async, because hashing algorithm
  79. take a lot of time.
  80. Parameters
  81. ----------
  82. content : str
  83. Plain password.
  84. salt : str
  85. Salt to use with password.
  86. iterations : int
  87. Count of iterations for hashing algorithm.
  88. Returns
  89. -------
  90. str
  91. Hashed password.
  92. """
  93. content = content.encode("UTF-8")
  94. salt = salt.encode("UTF-8")
  95. hashed = await asyncio.to_thread(
  96. hashlib.pbkdf2_hmac,
  97. password.get_algorithm(),
  98. content,
  99. salt,
  100. iterations
  101. )
  102. hashed = hashed.hex()
  103. salt = salt.decode("UTF-8")
  104. iterations = str(iterations)
  105. return hashed + ":" + salt + ":" + iterations
  106. def __get_salt(self) -> str:
  107. """
  108. That function return salt used to hash password from that hash.
  109. It could raise error, when hash is not correct.
  110. Raises
  111. ------
  112. RuntimeError
  113. When salt syntax is not correct.
  114. Returns
  115. -------
  116. str
  117. Salt of the hash.
  118. """
  119. try:
  120. _, salt, _ = self.__hash.split(":")
  121. return salt
  122. except:
  123. raise RuntimeError("Hash is incorrect.")
  124. def __get_own_iterations(self) -> int:
  125. """
  126. That return count of iterations stored in the hash. It is required,
  127. to make old passwords work after change of the password iterations
  128. in the app.
  129. Raises
  130. ------
  131. RuntimeError
  132. When hash syntax is not correct.
  133. Returns
  134. -------
  135. int
  136. Count of iterations from hash.
  137. """
  138. try:
  139. _, _, iterations = self.__hash.split(":")
  140. return int(iterations)
  141. except:
  142. raise RuntimeError("Hash is incorrect.")
  143. async def compare(self, target: str) -> bool:
  144. """
  145. That function compare given plain text password with already
  146. hashed password. It is async, because it must calculate hash of
  147. the given plain text with hashed password salt, to compare result
  148. hashes.
  149. Parameters
  150. ----------
  151. str
  152. Plain text to compare hashed password with.
  153. Returns
  154. -------
  155. bool
  156. True when passwords are same, False when not.
  157. """
  158. salt = self.__get_salt()
  159. iterations = self.__get_own_iterations()
  160. hashed_target = await self.__generate_hash(
  161. target,
  162. salt,
  163. iterations
  164. )
  165. return self.__hash == hashed_target
  166. def result(self) -> str:
  167. """
  168. It return hash of the password.
  169. Returns
  170. -------
  171. str
  172. Hash of the password.
  173. """
  174. return self.__hash
  175. def __repr__(self) -> str:
  176. """
  177. It return part of the password hash, to make it readable while
  178. debugging.
  179. Returns
  180. -------
  181. str
  182. Part of the hashed password.
  183. """
  184. return "Password: \"" + self.__hash[0:20] + "\"."
  185. def __str__(self) -> str:
  186. """
  187. Return password hash.
  188. Returns
  189. -------
  190. str
  191. Password hash.
  192. """
  193. return self.__hash
  194. async def from_plain_text(content: str) -> object:
  195. """
  196. It create new hashed password from plain text. It is async,
  197. because hashing process take a lot of time.
  198. Parameters
  199. ----------
  200. content : str
  201. Plain text password to make hash from.
  202. Returns
  203. -------
  204. password
  205. New password object with that password as hash.
  206. """
  207. salt = os.urandom(password.get_salt_length())
  208. salt = salt.hex()
  209. iterations = password.get_iterations()
  210. hashed = await password.__generate_hash(
  211. content,
  212. salt,
  213. iterations
  214. )
  215. return password(hashed)
  216. async def from_hash(hashed: str) -> object:
  217. """
  218. It create new password object from already hashed password content
  219. It is async, to make it similar to from_plain_text.
  220. Parameters
  221. ----------
  222. hashed : str
  223. Already hashed password.
  224. Returns
  225. -------
  226. password
  227. New password object from given hash.
  228. """
  229. return password(hashed)