crypto_engine.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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) 2014 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. try:
  29. from Crypto.Cipher import AES
  30. from Crypto.Protocol.KDF import PBKDF2
  31. except ImportError:
  32. # PyCryptop not found, we use a compatible implementation
  33. # in pure Python.
  34. # This is good for Windows where software installation suck
  35. # or embeded devices where compilation is a bit harder
  36. from pwman.util.crypto import AES
  37. from pwman.util.crypto.pypbkdf2 import PBKDF2
  38. print("WARNING: You are not using PyCrypto!!!")
  39. print("WARNING: You should install PyCrypto for better security and perfomance")
  40. print("WARNING: You can supress this warning by editing pwman config file.")
  41. from pwman.util.callback import Callback
  42. if sys.version_info.major > 2: # pragma: no cover
  43. raw_input = input
  44. EncodeAES = lambda c, s: base64.b64encode(c.encrypt(s))
  45. DecodeAES = lambda c, e: c.decrypt(base64.b64decode(e)).rstrip()
  46. def generate_password(pass_len=8, uppercase=True, lowercase=True, digits=True,
  47. special_chars=True):
  48. allowed = ''
  49. if lowercase:
  50. allowed = allowed + string.ascii_lowercase
  51. if uppercase:
  52. allowed = allowed + string.ascii_uppercase
  53. if digits:
  54. allowed = allowed + string.digits
  55. if special_chars:
  56. allowed = allowed + string.punctuation
  57. password = ''.join(random.SystemRandom().choice(allowed)
  58. for _ in range(pass_len))
  59. return password
  60. def zerome(string):
  61. """
  62. securely erase strings ...
  63. for windows: ctypes.cdll.msvcrt.memset
  64. """
  65. bufsize = len(string) + 1
  66. offset = sys.getsizeof(string) - bufsize
  67. ctypes.memset(id(string) + offset, 0, bufsize)
  68. class CryptoException(Exception):
  69. pass
  70. def get_digest(password, salt):
  71. """
  72. Get a digest based on clear text password
  73. """
  74. iterations = 5000
  75. if isinstance(password, bytes):
  76. password = password.decode()
  77. try:
  78. return PBKDF2(password, salt, dkLen=32, count=iterations)
  79. except TypeError:
  80. return PBKDF2(password, salt, iterations=iterations).read(32)
  81. def get_cipher(password, salt):
  82. """
  83. Create a chiper object from a hashed password
  84. """
  85. iv = os.urandom(AES.block_size)
  86. dig = get_digest(password, salt)
  87. chiper = AES.new(dig, AES.MODE_ECB, iv)
  88. return chiper
  89. def prepare_data(text, block_size):
  90. """
  91. prepare data before encryption so the lenght matches the expected
  92. lenght by the algorithm.
  93. """
  94. num_blocks = len(text)//block_size + 1
  95. newdatasize = block_size*num_blocks
  96. return text.ljust(newdatasize)
  97. class CryptoEngine(object): # pagma: no cover
  98. _timeoutcount = 0
  99. _instance = None
  100. _callback = None
  101. @classmethod
  102. def get(cls, timeout=-1):
  103. if CryptoEngine._instance:
  104. return CryptoEngine._instance
  105. CryptoEngine._instance = CryptoEngine(timeout)
  106. return CryptoEngine._instance
  107. def __init__(self, salt=None, digest=None, algorithm='AES',
  108. timeout=-1, reader=None):
  109. """
  110. Initialise the Cryptographic Engine
  111. """
  112. self._algo = algorithm
  113. self._digest = digest if digest else None
  114. self._salt = salt if salt else None
  115. self._timeout = timeout
  116. self._cipher = None
  117. self._reader = reader
  118. self._callback = None
  119. self._getsecret = None # This is set in callback.setter
  120. def authenticate(self, password):
  121. """
  122. salt and digest are stored in a file or a database
  123. """
  124. dig = get_digest(password, self._salt)
  125. if binascii.hexlify(dig) == self._digest or dig == self._digest:
  126. CryptoEngine._timeoutcount = time.time()
  127. self._cipher = get_cipher(password, self._salt)
  128. return True
  129. return False
  130. def _auth(self):
  131. """
  132. Read password from the user, if the password is correct,
  133. finish the execution an return the password and salt which
  134. are read from the file.
  135. """
  136. salt = self._salt
  137. tries = 0
  138. while tries < 5:
  139. password = self._getsecret("Please type in your master password"
  140. ).encode('utf-8')
  141. if self.authenticate(password):
  142. return password, salt
  143. print("You entered a wrong password...")
  144. tries += 1
  145. raise CryptoException("You entered wrong password 5 times..")
  146. def encrypt(self, text):
  147. if not self._is_authenticated():
  148. p, s = self._auth()
  149. cipher = get_cipher(p, s)
  150. self._cipher = cipher
  151. del(p)
  152. return EncodeAES(self._cipher, prepare_data(text, AES.block_size))
  153. def decrypt(self, cipher_text):
  154. if not self._is_authenticated():
  155. p, s = self._auth()
  156. cipher = get_cipher(p, s)
  157. self._cipher = cipher
  158. del(p)
  159. return DecodeAES(self._cipher, prepare_data(cipher_text,
  160. AES.block_size))
  161. def forget(self):
  162. """
  163. discard cipher
  164. """
  165. self._cipher = None
  166. def _is_authenticated(self):
  167. if not self._digest and not self._salt:
  168. self._create_password()
  169. if not self._is_timedout() and self._cipher is not None:
  170. return True
  171. return False
  172. def _is_timedout(self):
  173. if self._timeout > 0:
  174. if (time.time() - CryptoEngine._timeoutcount) > self._timeout:
  175. self._cipher = None
  176. return True
  177. return False
  178. def changepassword(self, reader=raw_input):
  179. if self._callback is None:
  180. raise CryptoException("No callback class has been specified")
  181. # if you change the password of the database you have to Change
  182. # all the cipher texts in the databse!!!
  183. self._keycrypted = self._create_password()
  184. self.set_cryptedkey(self._keycrypted)
  185. return self._keycrypted
  186. @property
  187. def callback(self):
  188. """
  189. return call back function
  190. """
  191. return self._callback
  192. @callback.setter
  193. def callback(self, callback):
  194. if isinstance(callback, Callback):
  195. self._callback = callback
  196. self._getsecret = callback.getsecret
  197. else:
  198. raise Exception("callback must be an instance of Callback!")
  199. def _create_password(self):
  200. """
  201. Create a secret password as a hash and the salt used for this hash.
  202. Change reader to manipulate how input is given.
  203. """
  204. salt = base64.b64encode(os.urandom(32))
  205. passwd = self._getsecret("Please type in the master password")
  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()