crypto_engine.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. # ============================================================================
  2. # This file is part of Pwman3.
  3. #
  4. # Pwman3 is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License, version 2
  6. # as published by the Free Software Foundation;
  7. #
  8. # Pwman3 is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with Pwman3; if not, write to the Free Software
  15. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  16. # ============================================================================
  17. # Copyright (C) 2016 Oz Nahum <nahumoz@gmail.com>
  18. # ============================================================================
  19. from __future__ import print_function
  20. import base64
  21. import binascii
  22. import ctypes
  23. import os
  24. import random
  25. import string
  26. import sys
  27. import time
  28. from cryptography.fernet import Fernet
  29. from cryptography.hazmat.backends import default_backend
  30. from cryptography.hazmat.primitives import hashes
  31. from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  32. from pwman.util.callback import Callback
  33. if sys.version_info.major > 2: # pragma: no cover
  34. raw_input = input
  35. def encode_AES(cipher, clear_text):
  36. if not isinstance(clear_text, bytes):
  37. clear_text = clear_text.encode()
  38. return base64.b64encode(cipher.encrypt(clear_text))
  39. def decode_AES(cipher, encoded_text):
  40. if not isinstance(encoded_text, bytes):
  41. encoded_text = encoded_text.encode()
  42. encoded_text = base64.b64decode(encoded_text)
  43. return cipher.decrypt(encoded_text).rstrip()
  44. def generate_password(pass_len=8, uppercase=True, lowercase=True, digits=True,
  45. special_chars=True):
  46. allowed = ''
  47. if lowercase:
  48. allowed = allowed + string.ascii_lowercase
  49. if uppercase:
  50. allowed = allowed + string.ascii_uppercase
  51. if digits:
  52. allowed = allowed + string.digits
  53. if special_chars:
  54. allowed = allowed + string.punctuation
  55. password = ''.join(random.SystemRandom().choice(allowed)
  56. for _ in range(pass_len))
  57. return password
  58. def zerome(string):
  59. """
  60. securely erase strings ...
  61. for windows: ctypes.cdll.msvcrt.memset
  62. """
  63. bufsize = len(string) + 1
  64. offset = sys.getsizeof(string) - bufsize
  65. ctypes.memset(id(string) + offset, 0, bufsize)
  66. class CryptoException(Exception):
  67. pass
  68. def get_digest(password, salt):
  69. """
  70. Get a digest based on clear text password
  71. """
  72. kdf = PBKDF2HMAC(
  73. algorithm=hashes.SHA256(),
  74. length=32,
  75. salt=salt,
  76. iterations=5000,
  77. backend=default_backend()
  78. )
  79. key = base64.urlsafe_b64encode(kdf.derive(password))
  80. return key
  81. def get_cipher(password, salt):
  82. """
  83. Create a chiper object from a hashed password
  84. """
  85. dig = get_digest(password, salt)
  86. return Fernet(dig)
  87. def prepare_data(text, block_size):
  88. """
  89. prepare data before encryption so the lenght matches the expected
  90. lenght by the algorithm.
  91. """
  92. num_blocks = len(text)//block_size + 1
  93. newdatasize = block_size*num_blocks
  94. return text.ljust(newdatasize)
  95. class CryptoEngine(object): # pagma: no cover
  96. _timeoutcount = 0
  97. _instance = None
  98. _callback = None
  99. @classmethod
  100. def get(cls, timeout=-1):
  101. if CryptoEngine._instance:
  102. return CryptoEngine._instance
  103. CryptoEngine._instance = CryptoEngine(timeout)
  104. return CryptoEngine._instance
  105. def __init__(self, salt=None, digest=None, algorithm='AES',
  106. timeout=-1, reader=None):
  107. """
  108. Initialise the Cryptographic Engine
  109. """
  110. self._algo = algorithm
  111. self._digest = digest if digest else None
  112. self._salt = salt if salt else None
  113. self._timeout = timeout
  114. self._cipher = None
  115. self._reader = reader
  116. self._callback = None
  117. self._getsecret = None # This is set in callback.setter
  118. def authenticate(self, password):
  119. """
  120. salt and digest are stored in a file or a database
  121. """
  122. dig = get_digest(password, self._salt)
  123. if binascii.hexlify(dig) == self._digest or dig == self._digest:
  124. CryptoEngine._timeoutcount = time.time()
  125. self._cipher = get_cipher(password, self._salt)
  126. return True
  127. return False
  128. def _auth(self):
  129. """
  130. Read password from the user, if the password is correct,
  131. finish the execution an return the password and salt which
  132. are read from the file.
  133. """
  134. salt = self._salt
  135. tries = 0
  136. while tries < 5:
  137. passwd = self._getsecret("Please type in your master password")
  138. if not isinstance(passwd, bytes):
  139. passwd = passwd.encode()
  140. if self.authenticate(passwd):
  141. return passwd, salt
  142. print("You entered a wrong password...")
  143. tries += 1
  144. raise CryptoException("You entered wrong password 5 times..")
  145. def encrypt(self, text):
  146. if not self._is_authenticated():
  147. p, s = self._auth()
  148. cipher = get_cipher(p, s)
  149. self._cipher = cipher
  150. del(p)
  151. return encode_AES(self._cipher, text)
  152. def decrypt(self, cipher_text):
  153. if not self._is_authenticated():
  154. p, s = self._auth()
  155. cipher = get_cipher(p, s)
  156. self._cipher = cipher
  157. del(p)
  158. return decode_AES(self._cipher, cipher_text)
  159. def forget(self):
  160. """
  161. discard cipher
  162. """
  163. self._cipher = None
  164. def _is_authenticated(self):
  165. if not self._digest and not self._salt:
  166. self._create_password()
  167. if not self._is_timedout() and self._cipher is not None:
  168. return True
  169. return False
  170. def _is_timedout(self):
  171. if self._timeout > 0:
  172. if (time.time() - CryptoEngine._timeoutcount) > self._timeout:
  173. self._cipher = None
  174. return True
  175. return False
  176. def changepassword(self, reader=raw_input):
  177. if self._callback is None:
  178. raise CryptoException("No callback class has been specified")
  179. # if you change the password of the database you have to Change
  180. # all the cipher texts in the databse!!!
  181. self._keycrypted = self._create_password()
  182. self.set_cryptedkey(self._keycrypted)
  183. return self._keycrypted
  184. @property
  185. def callback(self):
  186. """
  187. return call back function
  188. """
  189. return self._callback
  190. @callback.setter
  191. def callback(self, callback):
  192. if isinstance(callback, Callback):
  193. self._callback = callback
  194. self._getsecret = callback.getsecret
  195. else:
  196. raise Exception("callback must be an instance of Callback!")
  197. def _create_password(self):
  198. """
  199. Create a secret password as a hash and the salt used for this hash.
  200. Change reader to manipulate how input is given.
  201. """
  202. salt = base64.b64encode(os.urandom(32))
  203. passwd = self._getsecret("Please type in the master password")
  204. if not isinstance(passwd, bytes):
  205. passwd = passwd.encode()
  206. key = get_digest(passwd, salt)
  207. hpk = salt+'$6$'.encode('utf8')+binascii.hexlify(key)
  208. self._digest = key
  209. self._salt = salt
  210. self._cipher = get_cipher(passwd, salt)
  211. return hpk.decode('utf-8')
  212. def set_cryptedkey(self, key):
  213. # TODO: rename this method!
  214. salt, digest = key.split('$6$')
  215. self._digest = digest.encode('utf-8')
  216. self._salt = salt.encode('utf-8')
  217. def get_cryptedkey(self):
  218. # TODO: rename this method!
  219. """
  220. return _keycrypted
  221. """
  222. return self._salt.decode() + u'$6$' + self._digest.decode()