crypto_engine.py 7.4 KB

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