No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

monkeystore 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971
  1. #!/usr/bin/python
  2. # -*- coding: UTF-8 -*-
  3. # Monkeystore client
  4. #
  5. # Copyright (c) 2012 Bernd Zeimetz <b.zeimetz@conova.com>
  6. #
  7. # Parts taken from reportbug:
  8. #
  9. # Written by Chris Lawrence <lawrencc@debian.org>
  10. # (C) 2001-08 Chris Lawrence
  11. # Copyright (C) 2008-2012 Sandro Tosi <morph@debian.org>
  12. #
  13. # This program is freely distributable per the following license:
  14. #
  15. ## Permission to use, copy, modify, and distribute this software and its
  16. ## documentation for any purpose and without fee is hereby granted,
  17. ## provided that the above copyright notice appears in all copies and that
  18. ## both that copyright notice and this permission notice appear in
  19. ## supporting documentation.
  20. ##
  21. ## I DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
  22. ## IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL I
  23. ## BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  24. ## DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
  25. ## WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
  26. ## ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
  27. ## SOFTWARE.
  28. __version__ = '0.1.5.5'
  29. import sys
  30. import os
  31. import commands
  32. import re
  33. import math
  34. import string
  35. import errno
  36. import glob
  37. import getpass
  38. import textwrap
  39. import locale
  40. import time
  41. import subprocess
  42. import codecs
  43. sys.stdout = codecs.getwriter('utf8')(sys.stdout)
  44. sys.stderr = codecs.getwriter('utf8')(sys.stderr)
  45. try:
  46. import readline
  47. except ImportError:
  48. readline = None
  49. ISATTY = sys.stdout.isatty()
  50. charset = 'us-ascii'
  51. try:
  52. r, c = commands.getoutput('stty size').split()
  53. rows, columns = int(r) or 24, int(c) or 79
  54. except:
  55. rows, columns = 24, 79
  56. def owrite(message, *args):
  57. if args:
  58. message = message % args
  59. if isinstance(message, unicode):
  60. message = message.encode(charset, 'replace')
  61. sys.stdout.write(message)
  62. sys.stdout.flush()
  63. def ewrite(message, *args):
  64. if not ISATTY:
  65. return
  66. if args:
  67. message = message % args
  68. if isinstance(message, unicode):
  69. message = message.encode(charset, 'replace')
  70. sys.stderr.write(message)
  71. sys.stderr.flush()
  72. log_message = ewrite
  73. display_failure = ewrite
  74. def system(cmdline):
  75. try:
  76. x = os.getcwd()
  77. except OSError:
  78. os.chdir('/')
  79. return os.system(cmdline)
  80. def indent_wrap_text(text, starttext='', indent=0, linelen=None):
  81. """Wrapper for textwrap.fill to the existing API."""
  82. if not linelen:
  83. linelen = columns-1
  84. if indent:
  85. si = ' '*indent
  86. else:
  87. si = ''
  88. text = ' '.join(text.split())
  89. if not text:
  90. return starttext+'\n'
  91. output = textwrap.fill(text, width=linelen, initial_indent=starttext,
  92. subsequent_indent=si)
  93. if output.endswith('\n'):
  94. return output
  95. return output + '\n'
  96. # Readline support, if available
  97. if readline is not None:
  98. readline.parse_and_bind("tab: complete")
  99. try:
  100. # minimize the word delimeter list if possible
  101. readline.set_completer_delims(' ')
  102. except:
  103. pass
  104. class our_completer(object):
  105. def __init__(self, completions=None):
  106. self.completions = None
  107. if completions:
  108. self.completions = tuple(map(str, completions))
  109. def complete(self, text, i):
  110. if not self.completions: return None
  111. matching = [x for x in self.completions if x.startswith(text)]
  112. if i < len(matching):
  113. return matching[i]
  114. else:
  115. return None
  116. def our_raw_input(prompt = None, completions=None, completer=None):
  117. istty = sys.stdout.isatty()
  118. if not istty:
  119. sys.stderr.write(prompt)
  120. sys.stderr.flush()
  121. if readline:
  122. if completions and not completer:
  123. completer = our_completer(completions).complete
  124. if completer:
  125. readline.set_completer(completer)
  126. try:
  127. if istty:
  128. ret = raw_input(prompt)
  129. else:
  130. ret = raw_input()
  131. except EOFError:
  132. ewrite('\nUser interrupt (^D).\n')
  133. raise SystemExit
  134. if readline:
  135. readline.set_completer(None)
  136. return ret.strip()
  137. def select_options(msg, ok, help, allow_numbers=None, nowrap=False):
  138. err_message = ''
  139. for option in ok:
  140. if option in string.ascii_uppercase:
  141. default=option
  142. break
  143. if not help: help = {}
  144. if '?' not in ok: ok = ok+'?'
  145. if nowrap:
  146. longmsg = msg+' ['+'|'.join(ok)+']?'+' '
  147. else:
  148. longmsg = indent_wrap_text(msg+' ['+'|'.join(ok)+']?').strip()+' '
  149. ch = our_raw_input(longmsg, allow_numbers)
  150. # Allow entry of a bug number here
  151. if allow_numbers:
  152. while ch and ch[0] == '#': ch = ch[1:]
  153. if type(allow_numbers) == type(1):
  154. try:
  155. return str(int(ch))
  156. except ValueError:
  157. pass
  158. else:
  159. try:
  160. number = int(ch)
  161. if number in allow_numbers:
  162. return str(number)
  163. else:
  164. nums = list(allow_numbers)
  165. nums.sort()
  166. err_message = 'Only the following entries are allowed: '+\
  167. ', '.join(map(str, nums))
  168. except (ValueError, TypeError):
  169. pass
  170. if not ch: ch = default
  171. ch = ch[0]
  172. if ch=='?':
  173. help['?'] = 'Display this help.'
  174. for ch in ok:
  175. if ch in string.ascii_uppercase:
  176. desc = '(default) '
  177. else:
  178. desc = ''
  179. desc += help.get(ch, help.get(ch.lower(),
  180. 'No help for this option.'))
  181. ewrite(indent_wrap_text(desc+'\n', '%s - '% ch, 4))
  182. return select_options(msg, ok, help, allow_numbers, nowrap)
  183. elif (ch.lower() in ok) or (ch.upper() in ok):
  184. return ch.lower()
  185. elif err_message:
  186. ewrite(indent_wrap_text(err_message))
  187. else:
  188. ewrite('Invalid selection.\n')
  189. return select_options(msg, ok, help, allow_numbers, nowrap)
  190. def yes_no(msg, yeshelp, nohelp, default=True, nowrap=False):
  191. "Return True for yes, False for no."
  192. if default:
  193. ok = 'Ynq'
  194. else:
  195. ok = 'yNq'
  196. res = select_options(msg, ok, {'y': yeshelp, 'n': nohelp, 'q' : 'Quit.'},
  197. nowrap=nowrap)
  198. if res == 'q':
  199. raise SystemExit
  200. return (res == 'y')
  201. def long_message(text, *args):
  202. if args:
  203. ewrite(indent_wrap_text(text % args))
  204. else:
  205. ewrite(indent_wrap_text(text))
  206. final_message = long_message
  207. def get_string(prompt, options=None, title=None, empty_ok=False, force_prompt=False,
  208. default='', completer=None):
  209. if prompt and (len(prompt) < 2*columns/3) and not force_prompt:
  210. if default:
  211. prompt = '%s [%s]: ' % (prompt, default)
  212. response = our_raw_input(prompt, options, completer) or default
  213. else:
  214. response = our_raw_input(prompt, options, completer)
  215. else:
  216. if prompt:
  217. ewrite(indent_wrap_text(prompt))
  218. if default:
  219. response = our_raw_input('[%s]> ' % default, options, completer) or default
  220. else:
  221. response = our_raw_input('> ', options, completer)
  222. # Translate the response into a Unicode string
  223. if response is not None and not isinstance(response, unicode):
  224. response = unicode(response, charset, 'replace')
  225. return response
  226. def get_multiline(prompt):
  227. ewrite('\n')
  228. ewrite(indent_wrap_text(prompt + " Press ENTER on a blank line to continue.\n"))
  229. l = []
  230. while 1:
  231. entry = get_string('', force_prompt=True).strip()
  232. if not entry:
  233. break
  234. l.append(entry)
  235. ewrite('\n')
  236. return l
  237. def get_password_input(prompt=None):
  238. return getpass.getpass(prompt)
  239. def FilenameCompleter(text, i):
  240. text = os.path.expanduser(text)
  241. text = os.path.expandvars(text)
  242. paths = glob.glob(text+'*')
  243. if not paths: return None
  244. if i < len(paths):
  245. entry = paths[i]
  246. if os.path.isdir(entry):
  247. return entry+'/'
  248. return entry
  249. else:
  250. return None
  251. def get_filename(prompt, title=None, force_prompt=False, default=''):
  252. return get_string(prompt, title=title, force_prompt=force_prompt,
  253. default=default, completer=FilenameCompleter)
  254. def select_multiple(par, options, prompt, title=None, order=None, extras=None):
  255. return menu(par, options, prompt, title=title, order=order, extras=extras,
  256. multiple=True, empty_ok=False)
  257. def menu(par, options, prompt, default=None, title=None, any_ok=False,
  258. order=None, extras=None, multiple=False, empty_ok=False, column_parts=3):
  259. selected = {}
  260. if not extras:
  261. extras = []
  262. else:
  263. extras = list(extras)
  264. if title:
  265. ewrite(title+'\n\n')
  266. ewrite(indent_wrap_text(par, linelen=columns)+'\n')
  267. if isinstance(options, dict):
  268. options = options.copy()
  269. # Convert to a list
  270. if order:
  271. olist = []
  272. for key in order:
  273. if options.has_key(key):
  274. olist.append( (key, options[key]) )
  275. del options[key]
  276. # Append anything out of order
  277. options = options.items()
  278. options.sort()
  279. for option in options:
  280. olist.append( option )
  281. options = olist
  282. else:
  283. options = options.items()
  284. options.sort()
  285. if multiple:
  286. options.append( ('none', '') )
  287. default = 'none'
  288. extras += ['done']
  289. allowed = map(lambda x: x[0], options)
  290. allowed = allowed + extras
  291. maxlen_name = int(min(max(map(len, allowed)), columns/column_parts))
  292. digits = int(math.ceil(math.log10(len(options)+1)))
  293. i = 1
  294. for name, desc in options:
  295. text = indent_wrap_text(desc, indent=(maxlen_name+digits+3),
  296. starttext=('%*d %-*.*s ' % (
  297. digits, i, maxlen_name, maxlen_name, name)))
  298. ewrite(text)
  299. if len(options) < 5:
  300. ewrite('\n')
  301. i = i+1
  302. if len(options) >= 5:
  303. ewrite('\n')
  304. if multiple:
  305. prompt += '(one at a time) '
  306. while 1:
  307. if default:
  308. aprompt = prompt + '[%s] ' % default
  309. else:
  310. aprompt = prompt
  311. response = our_raw_input(aprompt, allowed)
  312. if not response: response = default
  313. try:
  314. num = int(response)
  315. if 1 <= num <= len(options):
  316. response = options[num-1][0]
  317. except (ValueError, TypeError):
  318. pass
  319. if response in allowed or (response == default and response):
  320. if multiple:
  321. if response == 'done':
  322. return selected.keys()
  323. elif response == 'none':
  324. return []
  325. elif selected.get(response):
  326. del selected[response]
  327. else:
  328. selected[response]=1
  329. ewrite('- selected: %s\n' % ', '.join(selected.keys()))
  330. if len(selected):
  331. default = 'done'
  332. else:
  333. default = 'none'
  334. continue
  335. else:
  336. return response
  337. if any_ok and response:
  338. return response
  339. elif empty_ok and not response:
  340. return
  341. ewrite('Invalid entry.\n')
  342. return
  343. def initialize ():
  344. return True
  345. def can_input():
  346. return sys.stdin.isatty()
  347. # ----------------------------------------------------------
  348. # Pyperclip v1.3
  349. # A cross-platform clipboard module for Python. (only handles plain text for now)
  350. # By Al Sweigart al@coffeeghost.net
  351. # Usage:
  352. # import pyperclip
  353. # pyperclip.copy('The text to be copied to the clipboard.')
  354. # spam = pyperclip.paste()
  355. # On Mac, this module makes use of the pbcopy and pbpaste commands, which should come with the os.
  356. # On Linux, this module makes use of the xclip command, which should come with the os. Otherwise run "sudo apt-get install xclip"
  357. # Copyright (c) 2010, Albert Sweigart
  358. # All rights reserved.
  359. #
  360. # BSD-style license:
  361. #
  362. # Redistribution and use in source and binary forms, with or without
  363. # modification, are permitted provided that the following conditions are met:
  364. # * Redistributions of source code must retain the above copyright
  365. # notice, this list of conditions and the following disclaimer.
  366. # * Redistributions in binary form must reproduce the above copyright
  367. # notice, this list of conditions and the following disclaimer in the
  368. # documentation and/or other materials provided with the distribution.
  369. # * Neither the name of the pyperclip nor the
  370. # names of its contributors may be used to endorse or promote products
  371. # derived from this software without specific prior written permission.
  372. #
  373. # THIS SOFTWARE IS PROVIDED BY Albert Sweigart "AS IS" AND ANY
  374. # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  375. # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  376. # DISCLAIMED. IN NO EVENT SHALL Albert Sweigart BE LIABLE FOR ANY
  377. # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  378. # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  379. # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  380. # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  381. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  382. # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  383. # Change Log:
  384. # 1.2 Use the platform module to help determine OS.
  385. # 1.3 Changed ctypes.windll.user32.OpenClipboard(None) to ctypes.windll.user32.OpenClipboard(0), after some people ran into some TypeError
  386. import platform, os
  387. def winGetClipboard():
  388. ctypes.windll.user32.OpenClipboard(0)
  389. pcontents = ctypes.windll.user32.GetClipboardData(1) # 1 is CF_TEXT
  390. data = ctypes.c_char_p(pcontents).value
  391. #ctypes.windll.kernel32.GlobalUnlock(pcontents)
  392. ctypes.windll.user32.CloseClipboard()
  393. return data
  394. def winSetClipboard(text):
  395. GMEM_DDESHARE = 0x2000
  396. ctypes.windll.user32.OpenClipboard(0)
  397. ctypes.windll.user32.EmptyClipboard()
  398. try:
  399. # works on Python 2 (bytes() only takes one argument)
  400. hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(text))+1)
  401. except TypeError:
  402. # works on Python 3 (bytes() requires an encoding)
  403. hCd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(text, 'ascii'))+1)
  404. pchData = ctypes.windll.kernel32.GlobalLock(hCd)
  405. try:
  406. # works on Python 2 (bytes() only takes one argument)
  407. ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes(text))
  408. except TypeError:
  409. # works on Python 3 (bytes() requires an encoding)
  410. ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pchData), bytes(text, 'ascii'))
  411. ctypes.windll.kernel32.GlobalUnlock(hCd)
  412. ctypes.windll.user32.SetClipboardData(1,hCd)
  413. ctypes.windll.user32.CloseClipboard()
  414. def macSetClipboard(text):
  415. outf = os.popen('pbcopy', 'w')
  416. outf.write(text)
  417. outf.close()
  418. def macGetClipboard():
  419. outf = os.popen('pbpaste', 'r')
  420. content = outf.read()
  421. outf.close()
  422. return content
  423. def gtkGetClipboard():
  424. return gtk.Clipboard().wait_for_text()
  425. def gtkSetClipboard(text):
  426. cb = gtk.Clipboard()
  427. cb.set_text(text)
  428. cb.store()
  429. def qtGetClipboard():
  430. return str(cb.text())
  431. def qtSetClipboard(text):
  432. cb.setText(text)
  433. def xclipSetClipboard(text):
  434. outf = os.popen('xclip -selection c', 'w')
  435. outf.write(text)
  436. outf.close()
  437. def xclipGetClipboard():
  438. outf = os.popen('xclip -selection c -o', 'r')
  439. content = outf.read()
  440. outf.close()
  441. return content
  442. if os.name == 'nt' or platform.system() == 'Windows':
  443. import ctypes
  444. getcb = winGetClipboard
  445. setcb = winSetClipboard
  446. elif os.name == 'mac' or platform.system() == 'Darwin':
  447. getcb = macGetClipboard
  448. setcb = macSetClipboard
  449. elif os.name == 'posix' or platform.system() == 'Linux':
  450. xclipExists = os.system('which xclip > /dev/null') == 0
  451. if xclipExists:
  452. getcb = xclipGetClipboard
  453. setcb = xclipSetClipboard
  454. else:
  455. try:
  456. import gtk
  457. getcb = gtkGetClipboard
  458. setcb = gtkSetClipboard
  459. except:
  460. try:
  461. import PyQt4.QtCore
  462. import PyQt4.QtGui
  463. app = QApplication([])
  464. cb = PyQt4.QtGui.QApplication.clipboard()
  465. getcb = qtGetClipboard
  466. setcb = qtSetClipboard
  467. except:
  468. raise Exception('Copying to clipboard requires the gtk or PyQt4 module installed, or the xclip command.')
  469. copy = setcb
  470. paste = getcb
  471. # ----------------------------------------------------------
  472. import gnupg
  473. import xmlrpclib
  474. import difflib
  475. try:
  476. import simplejson as json
  477. except ImportError:
  478. import json
  479. class GPGCardUser(object):
  480. "Parse gpg card status output for a user name"
  481. def __init__(self, gpg):
  482. self.gpg = gpg
  483. self.carduser = None
  484. self.card_ok = False
  485. self.carduser_re = re.compile(r'^Login data *\.*: ([a-z]+)',re.M)
  486. def __nonzero__(self):
  487. return not self.card_ok
  488. __bool__=__nonzero__
  489. def __str__(self):
  490. return self.carduser or ''
  491. def handle_status(self, key, value):
  492. if key=='CARDCTRL' and int(value.split(' ')[0])==3:
  493. self.card_ok=True
  494. def _process_data(self):
  495. if not (self.data and self.card_ok):
  496. return
  497. if gnupg.__version__ < '0.4.1':
  498. self.carduser = self.carduser_re.findall(self.data)[0]
  499. else:
  500. self.carduser = [line.split(':')[1] for line in self.data.split('\n') if line.split(':')[0] == 'login'][0]
  501. def __encrypt_data__(data, keys, passphrase=None, symmetric=False):
  502. gpg_data = gpg.encrypt(data, keys, passphrase=passphrase, symmetric=symmetric, always_trust=True)
  503. if not gpg_data.ok:
  504. errstr = ''
  505. if hasattr(gpg_data,'stderr'):
  506. errstr = gpg_data.stderr
  507. raise Exception("Failed to encrypt \n%s" %(errstr,))
  508. return gpg_data.data
  509. def __decrypt_data__(gpg_data, passphrase=None):
  510. data = gpg.decrypt(gpg_data, passphrase=passphrase, always_trust=True)
  511. if not data.ok and not data.data:
  512. errstr = ''
  513. if hasattr(data,'stderr'):
  514. errstr = data.stderr
  515. raise Exception("Failed to decrypt \n%s" %(errstr,))
  516. return data.data
  517. def get_token(server, user):
  518. retry_count = 0
  519. while True:
  520. try:
  521. token_crypt = server.retrieve_token(user)
  522. break
  523. except TypeError:
  524. # restart gpg-agent on windows.
  525. if os.name == 'nt' or platform.system() == 'Windows':
  526. if retry_count >= 3:
  527. raise
  528. subprocess.call(['C:\Program Files (x86)\GNU\GnuPG\gpg-connect-agent.exe', 'KILLAGENT', '/bye'], shell=True)
  529. GPGCardUser(gpg)
  530. else:
  531. raise
  532. retry_count = retry_count + 1
  533. return __decrypt_data__(token_crypt)
  534. def __passwords_are_similar__(old_password, new_password):
  535. similarity_checker = difflib.SequenceMatcher(None, old_password.lower(), new_password.lower())
  536. return (similarity_checker.quick_ratio() > 0.6)
  537. def __menu_password_select__(server, user, todo, string=''):
  538. while True:
  539. token=get_token(server, user)
  540. if string != '':
  541. search_string = string
  542. else:
  543. search_string = get_string("Enter hostname (needs exact match if less than 6 characters): ", empty_ok=False)
  544. search_result = server.search(user,
  545. __encrypt_data__(search_string,
  546. None,
  547. passphrase=token,
  548. symmetric=True))
  549. if search_result:
  550. break
  551. if not yes_no('Nothing found - try again?', 'try again', 'no, thanks'):
  552. sys.exit(0)
  553. resultset = {}
  554. result_menu_content = {}
  555. for category, _hostnames in search_result.items():
  556. for hostname, host_data in _hostnames.items():
  557. for service, service_data in host_data.items():
  558. for username, description in service_data.items():
  559. menu_string = "%s\t%s\t%s\t%s" %(category, hostname, service, username)
  560. resultset[menu_string] = (category, hostname, service, username)
  561. result_menu_content[menu_string] = description
  562. menutext= 'Choose the password you want to ' + {
  563. 'g' : 'retrieve',
  564. 'u' : 'update',
  565. 'd' : 'delete' }[todo]
  566. chosen_password = menu(menutext, result_menu_content, '% ', column_parts=1.5)
  567. category, hostname, service, username = resultset[chosen_password]
  568. description = search_result[category][hostname][service][username]
  569. password = get_password(server, user, category, hostname, service, username)
  570. print_password = password
  571. url = get_url(server, user, category, hostname, service, username)
  572. if todo=='u' or todo=='g':
  573. if todo=='u':
  574. new_description = get_string('Enter new description (hit enter to use the old one!): ',
  575. default=description)
  576. while True:
  577. new_password = get_string('Enter new password (hit enter to use the old one!): ',
  578. default=password).strip()
  579. if len(new_password) == 0:
  580. ewrite('New password empty, try again!\n\n')
  581. continue
  582. if __passwords_are_similar__(password, new_password) and not password == new_password:
  583. ewrite('New password too similar to the old password, try again!\n\n')
  584. continue
  585. if new_password == password:
  586. new_password=''
  587. print_password=password
  588. else:
  589. print_password=new_password
  590. break
  591. if not (new_password == '' and new_description == description):
  592. update_password(server, user, category, hostname, service, username, new_password, new_description)
  593. else:
  594. ewrite("Nothing updated.\n")
  595. del new_password
  596. del new_description
  597. copy(print_password)
  598. owrite('Password copied to clipboard: %s\n' %(print_password,))
  599. owrite('URL: %s\n' %(url,))
  600. del password
  601. del print_password
  602. else:
  603. if todo=='d':
  604. message="Delete password for %s/%s %s/%s?" %(category, hostname, service, username)
  605. if yes_no(message, 'Delete password', "Don't delete password", default=False):
  606. delete_password(server, user, category, hostname, service, username)
  607. owrite('Password deleted.\n')
  608. def __menu_password_add__(server, user, category):
  609. hostname = get_string('Enter hostname: ')
  610. service = get_string('Enter service (ssh, mysql, ...): ')
  611. username = get_string('Enter username: ')
  612. description = get_string('Enter description (optional): ', empty_ok=True)
  613. if description == None:
  614. description = ''
  615. password = get_string('Enter password: ')
  616. url = add_password(server, user, category, hostname, service, username, password, description)
  617. print_password_info(category, hostname, service, username, url, interactive=True)
  618. return
  619. def __menu_category__(server, user):
  620. token=get_token(server, user)
  621. _remote_categories = server.list_categories(user, token)
  622. categories = {}
  623. for i in _remote_categories:
  624. categories[i] = ''
  625. return menu('Available categories:', categories, 'Please choose a category: ')
  626. def __menu_add_get__():
  627. return select_options('What do you want to do?',
  628. 'agud',
  629. { 'a' : 'add a new password',
  630. 'g' : 'get an existing password',
  631. 'u' : 'update an existing password',
  632. 'd' : 'delete an existing password',
  633. })
  634. def update_password(server, user, category, hostname, service, username, password, description=None, reencrypt=False):
  635. token=get_token(server, user)
  636. if os.name == 'nt' or platform.system() == 'Windows':
  637. # remove Unicode Character 'REPLACEMENT CHARACTER' (U+FFFD)
  638. # http://www.fileformat.info/info/unicode/char/fffd/index.htm
  639. # seems windows is producing some fuckup when entering passwords...
  640. password=password.replace(u'\ufffd','')
  641. description=description.replace(u'\ufffd','')
  642. if password and not password == '':
  643. password_crypt = __encrypt_data__(password, None, passphrase=token, symmetric=True)
  644. else:
  645. password_crypt = ''
  646. if description and not description == '':
  647. description_crypt = __encrypt_data__(description, None, passphrase=token, symmetric=True)
  648. else:
  649. description_crypt = ''
  650. if not reencrypt:
  651. return server.update_password(user, category, hostname, service, username, password_crypt, description_crypt)
  652. else:
  653. return server.reencrypt_password(user, category, hostname, service, username, password_crypt)
  654. def add_password(server, user, category, hostname, service, username, password, description=''):
  655. token=get_token(server, user)
  656. if os.name == 'nt' or platform.system() == 'Windows':
  657. # remove Unicode Character 'REPLACEMENT CHARACTER' (U+FFFD)
  658. # http://www.fileformat.info/info/unicode/char/fffd/index.htm
  659. # seems windows is producing some fuckup when entering passwords...
  660. password=password.replace(u'\ufffd','')
  661. description=description.replace(u'\ufffd','')
  662. password_crypt = __encrypt_data__(password, None, passphrase=token, symmetric=True)
  663. description_crypt = __encrypt_data__(description, None, passphrase=token, symmetric=True)
  664. return server.add_password(user, category, hostname, service, username, password_crypt, description_crypt)
  665. def delete_password(server, user, category, hostname, service, username):
  666. token=get_token(server, user)
  667. return server.delete_password(user, token, category, hostname, service, username)
  668. def get_url(server, user, category, hostname, service, username):
  669. return __decrypt_data__(server.get_url(user, category, hostname, service, username))
  670. def get_password(server, user, category, hostname, service, username):
  671. return __decrypt_data__(server.get_password(user, category, hostname, service, username))
  672. def get_password_by_url(server, user, url):
  673. return __decrypt_data__(server.get_password_by_url(user, url))
  674. def print_password_info(category, hostname, service, username, url, interactive=False):
  675. password_text='%s/%s %s/%s %s' %(category, hostname, service, username, url)
  676. if interactive:
  677. owrite('\nPassword stored: ------- %s ------\n\n' %(password_text,))
  678. else:
  679. owrite(password_text + '\n')
  680. return
  681. def reencrypt_passwords(server, user):
  682. metadata = json.loads(__decrypt_data__(server.get_metadata(user)))
  683. for category, _hostnames in metadata.iteritems():
  684. for hostname, _services in _hostnames.iteritems():
  685. for service, _usernames in _services.iteritems():
  686. for username, description in _usernames.iteritems():
  687. while True:
  688. try:
  689. update_password(server, user, category, hostname, service, username,
  690. get_password(server, user, category, hostname, service, username),
  691. description=None, reencrypt=True)
  692. except Exception, e:
  693. print e
  694. time.sleep(4)
  695. else:
  696. break
  697. owrite("reencrypted: %s/%s/%s/%s\n" %(category, hostname, service, username))
  698. def list_passwords(server, user):
  699. metadata = json.loads(__decrypt_data__(server.get_metadata(user)))
  700. for category, _hostnames in metadata.iteritems():
  701. for hostname, _services in _hostnames.iteritems():
  702. for service, _usernames in _services.iteritems():
  703. for username, description in _usernames.iteritems():
  704. owrite("%s/%s/%s/%s\n" %(category, hostname, service, username))
  705. if __name__ == '__main__':
  706. from optparse import OptionParser
  707. from os.path import basename
  708. prog = basename(sys.argv[0])
  709. epilog = '%s - conova communications GmbH' %(prog,)
  710. parser = OptionParser(epilog=epilog)
  711. parser.add_option('-P', '--password', action='store', default='', dest='password')
  712. parser.add_option('-H', '--hostname', action='store', default='', dest='hostname')
  713. parser.add_option('-D', '--description', action='store', default='', dest='description')
  714. parser.add_option('-C', '--category', action='store', default='', dest='category')
  715. parser.add_option('-S', '--service', action='store', default='', dest='service')
  716. parser.add_option('-U', '--username', action='store', default='', dest='username')
  717. parser.add_option('-a', '--add', action='store_true', default=False, dest='add', help='add a new password')
  718. parser.add_option('-d', '--delete', action='store_true', default=False, dest='delete', help='delete a password')
  719. parser.add_option('-u', '--update', action='store_true', default=False, dest='update', help='update an entry')
  720. parser.add_option('-g', '--get', action='store_true', default=False, dest='get', help='retrieve password')
  721. parser.add_option('-c', '--clipboard', action='store_true', default=False, dest='clipboard', help='copy password to clipboard')
  722. parser.add_option('--reencrypt', action='store_true', default=False, dest='reencrypt', help='re-encrypt all passwords, requires access to metadata')
  723. parser.add_option('--list', action='store_true', default=False, dest='listp', help='list all passwords, requires access to metadata')
  724. parser.add_option('-V', '--version', action='store_true', default=False, dest='version', help='show version info and exit')
  725. ewrite('Monkeystore version %s\n' %(__version__,))
  726. try:
  727. gpg=gnupg.GPG()
  728. result = GPGCardUser(gpg)
  729. # retrieve login user from smartcard
  730. args = ['--card-status']
  731. process = gpg._open_subprocess(args)
  732. gpg._collect_output(process, result)
  733. if not result.card_ok:
  734. raise Exception()
  735. result._process_data()
  736. user = result.carduser
  737. if not user or user == '':
  738. raise Exception()
  739. except Exception,e:
  740. ewrite("Failed to access OpenGPG card! Make sure card is inserted!\n")
  741. if os.name == 'nt' or platform.system() == 'Windows':
  742. ewrite("You might need to kill the process named 'gpg-agent'.\n")
  743. if result.data:
  744. ewrite(result.data+"\n")
  745. sys.exit(1)
  746. try:
  747. server = xmlrpclib.ServerProxy('http://172.24.1.1:6000/')
  748. #server = xmlrpclib.ServerProxy('http://127.0.0.1:5000/')
  749. if len(sys.argv) == 1:
  750. todo = __menu_add_get__()
  751. if todo == 'a':
  752. category = __menu_category__(server, user)
  753. __menu_password_add__(server, user, category)
  754. elif todo == 'g' or todo == 'u' or todo == 'd':
  755. __menu_password_select__(server, user, todo)
  756. else:
  757. sys.exit(0)
  758. else:
  759. (options, args) = parser.parse_args()
  760. if options.version:
  761. owrite('%s\nVersion: %s\n' %(epilog, __version__))
  762. sys.exit(0)
  763. if args and args[0].startswith('monkeystore://'):
  764. url = args[0]
  765. password = get_password_by_url(server, user, url)
  766. if options.clipboard:
  767. copy(password)
  768. else:
  769. owrite(password+'\n')
  770. del password
  771. elif len(args) == 1: # probably a search string
  772. password=__menu_password_select__(server, user, 'g', args[0] )
  773. elif options.reencrypt:
  774. reencrypt_passwords(server, user)
  775. elif options.listp:
  776. list_passwords(server, user)
  777. else:
  778. if (options.hostname=='' or
  779. options.category=='' or
  780. options.service=='' or
  781. options.username==''):
  782. ewrite('The following options are required: --category, --hostname, --service, --user\n')
  783. sys.exit(1)
  784. if options.add:
  785. if options.password=='':
  786. ewrite("Can't add password without --password\n")
  787. sys.exit(1)
  788. url = add_password(server, user, options.category, options.hostname, options.service, options.username, options.password, options.description)
  789. if options.clipboard:
  790. copy(url)
  791. print_password_info(options.category, options.hostname, options.service, options.username, url, interactive=False)
  792. del url
  793. if options.get:
  794. password=get_password(server, user, options.category, options.hostname, options.service, options.username)
  795. if options.clipboard:
  796. copy(password)
  797. else:
  798. ewrite(password+'\n')
  799. del password
  800. if options.delete:
  801. delete_password(server, user, options.category, options.hostname, options.service, options.username)
  802. if options.update:
  803. update_password(server, user, options.category, options.hostname, options.service, options.username, options.password, options.description, reencrypt=False)
  804. except xmlrpclib.Fault, err:
  805. ewrite("A fault occurred: %s - %s\n" %(err.faultCode,err.faultString))
  806. # x = select_options('foo', 'abcd',
  807. # {'y': 'YESPLEASE',
  808. # 'n': "Don't submit the bug report; instead, "
  809. # "save it in a temporary file (exits reportbug).",
  810. # 'q': "Save it in a temporary file and quit.",
  811. # 'a': "Attach a file.",
  812. # 'd': "Detach an attachment file.",
  813. # 'i': "Include a text file.",
  814. # 'c': "Change editor and re-edit.",
  815. # 'e': 'Re-edit the bug report.',
  816. # 'l': 'Pipe the message through the pager.',
  817. # 'p': 'print message to stdout.',
  818. # 't': 'Add tags.',
  819. # 's': 'Add a X-Debbugs-CC recipient (a CC but after BTS processing).',
  820. # 'm': "Choose a mailer to edit the report."})