crypto_engine.py 7.6 KB

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