PageRenderTime 56ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/sshproxy/cipher.py

#
Python | 317 lines | 278 code | 17 blank | 22 comment | 9 complexity | ff7902e83cd3606f2b6ed1dd2191f7f4 MD5 | raw file
Possible License(s): GPL-2.0
  1. #!/usr/bin/env python
  2. # -*- coding: ISO-8859-15 -*-
  3. #
  4. # Copyright (C) 2005-2007 David Guerizec <david@guerizec.net>
  5. #
  6. # Last modified: 2007 Dec 09, 01:33:10 by david
  7. #
  8. # This program is free software; you can redistribute it and/or
  9. # modify it under the terms of the GNU General Public License
  10. # as published by the Free Software Foundation; either version 2
  11. # of the License, or (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with this program; if not, write to the Free Software
  20. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  21. from Crypto.Cipher import Blowfish
  22. import base64
  23. from config import ConfigSection, get_config
  24. _engine_registry = {}
  25. def register_engine(cls):
  26. _engine_registry[cls.cipher_id] = cls
  27. def list_engines():
  28. return _engine_registry.keys()
  29. def get_engine(cipher_type):
  30. engine = _engine_registry.get(cipher_type, None)
  31. if engine is None:
  32. if cipher_type != 'plain':
  33. raise ValueError('Unknown cipher_type %s' % cipher_type)
  34. engine = PlainCipher
  35. return engine
  36. def cipher(text, **kw):
  37. if not text:
  38. text = ''
  39. engine = _default_engine[0]
  40. if kw.has_key('type'):
  41. engine = get_engine(kw['type'])
  42. return '%s%s' % (engine.prefix(), engine.encrypt(text, **kw))
  43. def decipher(text, **kw):
  44. if not text:
  45. return text
  46. # guess the encryption method
  47. tokens = text.split('$', 2)
  48. # default decoder is Plain
  49. engine = PlainCipher
  50. if len(tokens) == 3:
  51. try:
  52. engine = get_engine(tokens[1])
  53. text = tokens[2]
  54. except ValueError:
  55. # maybe the text contains the magic prefix...
  56. # let's try to decode it as plain text
  57. pass
  58. return engine.decrypt(text, **kw)
  59. class BaseCipher(object):
  60. # cipher_id = ''
  61. @classmethod
  62. def set_default(cls):
  63. _default_engine[0] = cls
  64. @classmethod
  65. def prefix(cls):
  66. return '$%s$' % cls.cipher_id
  67. @classmethod
  68. def encrypt(cls, text, **kw):
  69. raise NotImplementedError
  70. @classmethod
  71. def decrypt(cls, text, **kw):
  72. raise NotImplementedError
  73. class PlainCipher(BaseCipher):
  74. cipher_id = "plain"
  75. @classmethod
  76. def prefix(cls):
  77. return ''
  78. @classmethod
  79. def encrypt(cls, text, **kw):
  80. return text
  81. @classmethod
  82. def decrypt(cls, text, **kw):
  83. return text
  84. register_engine(PlainCipher)
  85. class Base64Cipher(BaseCipher):
  86. cipher_id = "base64"
  87. @classmethod
  88. def encrypt(cls, text, **kw):
  89. return base64.b64encode(text)
  90. @classmethod
  91. def decrypt(cls, text, **kw):
  92. return base64.b64decode(text)
  93. register_engine(Base64Cipher)
  94. class BlowfishCipher(BaseCipher):
  95. cipher_id = "blowfish"
  96. @classmethod
  97. def set_default(cls):
  98. cls.engine = cls.get_engine(get_config('blowfish')['secret'])
  99. _default_engine[0] = cls
  100. @classmethod
  101. def get_engine(cls, secret=None):
  102. if secret is None:
  103. secret = getattr(cls, 'secret', get_config('blowfish')['secret'])
  104. return Blowfish.new(secret, Blowfish.MODE_ECB)
  105. @classmethod
  106. def encrypt(cls, text, **kw):
  107. engine = kw.get('engine', getattr(cls, 'engine', None))
  108. if not engine:
  109. engine = cls.get_engine(kw.get('secret', None))
  110. ftext = '%s:%s%s' % (len(text), text, '*'*8)
  111. ftext = ftext[:len(ftext) - (len(ftext)%8) ]
  112. return base64.b64encode(engine.encrypt(ftext))
  113. @classmethod
  114. def decrypt(cls, text, **kw):
  115. engine = kw.get('engine', getattr(cls, 'engine', None))
  116. if not engine:
  117. engine = cls.get_engine(kw.get('secret', None))
  118. crypted = base64.b64decode(text)
  119. # an exception here means we've lost the key...
  120. # how to handle that situation ?
  121. try:
  122. size, ftext = engine.decrypt(crypted).split(':', 1)
  123. except ValueError:
  124. import log
  125. log.error('Password probably encrypted with another passphrase')
  126. return ''
  127. return ftext[:int(size)]
  128. register_engine(BlowfishCipher)
  129. _default_engine = [ PlainCipher ]
  130. class BlowfishConfigSection(ConfigSection):
  131. section_id = 'blowfish'
  132. section_defaults = {
  133. 'secret': ('Enoch Root has an old cigar box on his lap.'
  134. ' Golden light is shining out of the crack around its lid.'),
  135. }
  136. def __setitem__(self, option, value):
  137. ConfigSection.__setitem__(self, option, value)
  138. if option == 'secret':
  139. reload()
  140. BlowfishConfigSection.register()
  141. def _init_cipher():
  142. cipher_type = get_config('sshproxy')['cipher_type']
  143. get_engine(cipher_type).set_default()
  144. _init_cipher()
  145. reload = _init_cipher
  146. #######################################################################
  147. ### Recipher database
  148. #######################################################################
  149. def recipher(cipher_type, password_fd, dry_run=False):
  150. import sys, getpass
  151. from sshproxy.backend import get_backend
  152. conf = get_config()
  153. newsecret = ''
  154. if cipher_type == 'blowfish':
  155. if conf['sshproxy']['cipher_type'] == 'blowfish':
  156. print("Recipher from blowfish to blowfish does not work "
  157. "at the moment.\nPlease recipher to base64 first, "
  158. "then recipher to blowfish.")
  159. print "Sorry for the inconvenience."
  160. sys.exit(0)
  161. try:
  162. if password_fd == True:
  163. newsecret = conf['blowfish']['secret']
  164. elif not password_fd:
  165. sec1 = 1
  166. sec2 = 2
  167. while not sec1 or sec1 != sec2:
  168. sec1 = getpass.getpass("Enter secret (1/2) ")
  169. sec2 = getpass.getpass("Enter secret (2/2) ")
  170. newsecret = sec1
  171. else:
  172. try:
  173. newsecret = password_fd.readlines()[0].strip()
  174. except IndexError:
  175. newsecret = ''
  176. except (KeyboardInterrupt, EOFError):
  177. print 'Aborted...'
  178. sys.exit(0)
  179. if len(newsecret) < 10:
  180. print 'Secret must be at least 10 characters long.'
  181. sys.exit(0)
  182. elif cipher_type not in ('plain', 'base64'):
  183. print "unknown cipher_type", cipher_type
  184. sys.exit(1)
  185. if cipher_type == 'blowfish':
  186. conf['blowfish']['newsecret'] = newsecret
  187. dry_run or conf.write() # just in case it goes wrong in the middle
  188. pwdb = get_backend()
  189. nb_passwords = 0
  190. nb_pkeys = 0
  191. total = 0
  192. sites = pwdb.list_site_users()
  193. for site in sites:
  194. if not site.login:
  195. continue
  196. name = site.name
  197. uid = site.login
  198. password = site.get_token('password')
  199. if password:
  200. # decipher with old secret
  201. oldpass = decipher(password)
  202. # cipher with new secret
  203. if oldpass is not None:
  204. newpass = cipher(oldpass, type=cipher_type, secret=newsecret)
  205. # check if we can decipher the new password
  206. if (oldpass is not None and
  207. oldpass != decipher(newpass, secret=newsecret) ):
  208. raise KeyError('Problem with %s cipher on %s@%s password!' %
  209. (cipher_type, uid, name))
  210. # be verbose when in dry-run mode
  211. if dry_run and password:
  212. print '-- %s@%s [ %s / %s / %s ]' % (uid, name,
  213. password, oldpass, newpass)
  214. # if the password changed, update it if not in dry-run mode
  215. if password != newpass:
  216. if not dry_run:
  217. site.set_tokens(password=newpass)
  218. site.save()
  219. nb_passwords += 1
  220. pkey = site.get_token('pkey')
  221. if pkey:
  222. # decipher with old secret
  223. oldpkey = decipher(pkey)
  224. # cipher with new secret
  225. if oldpkey is not None:
  226. newpkey = cipher(oldpkey, type=cipher_type, secret=newsecret)
  227. # check if we can decipher the new password
  228. if (oldpkey is not None and
  229. oldpkey != decipher(newpkey, secret=newsecret) ):
  230. raise KeyError('Problem with %s cipher on %s@%s pkey!' %
  231. (cipher_type, uid, name))
  232. # be verbose when in dry-run mode
  233. if dry_run and pkey:
  234. print ' %s' % (pkey or '').replace('\n', '\n ')
  235. print ' %s' % (oldpkey or '').replace('\n', '\n ')
  236. print ' %s' % (newpkey or '').replace('\n', '\n ')
  237. print
  238. # if the pkey changed, update it if not in dry-run mode
  239. if pkey != newpkey:
  240. if not dry_run:
  241. site.set_tokens(pkey=newpkey)
  242. site.save()
  243. nb_pkeys += 1
  244. total += 1
  245. if cipher_type == 'blowfish':
  246. del conf['blowfish']['newsecret']
  247. # update secret
  248. conf['blowfish']['secret'] = newsecret
  249. conf['sshproxy']['cipher_type'] = cipher_type
  250. dry_run or conf.write()
  251. print 'Reciphered %d passwords and %d pkeys on %d entries' % (nb_passwords,
  252. nb_pkeys,
  253. total)