ui.py 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651
  1. # -*- coding: utf-8 -*-
  2. """
  3. # Simple password manager
  4. # Copyright (c) 2011-2023 Michael Büsch <m@bues.ch>
  5. # Licensed under the GNU/GPL version 2 or later.
  6. """
  7. from libpwman.database import *
  8. from libpwman.dbdiff import *
  9. from libpwman.exception import *
  10. from libpwman.otp import *
  11. from libpwman.ui_escape import *
  12. from libpwman.util import *
  13. import functools
  14. import os
  15. import pathlib
  16. import re
  17. import readline
  18. import sys
  19. import time
  20. import traceback
  21. from cmd import Cmd
  22. from copy import copy
  23. from dataclasses import dataclass, field
  24. from typing import Optional, Tuple
  25. if osIsPosix:
  26. import signal
  27. __all__ = [
  28. "PWMan",
  29. "PWManTimeout",
  30. ]
  31. class PWManTimeout(Exception):
  32. def __init__(self, seconds):
  33. if seconds is not None and seconds >= 0:
  34. self.seconds = seconds
  35. if osIsPosix:
  36. signal.signal(signal.SIGALRM, self.__timeout)
  37. self.poke()
  38. else:
  39. raise PWManError("Timeout is not supported on this OS.")
  40. else:
  41. self.seconds = None
  42. def poke(self):
  43. if self.seconds is not None:
  44. signal.alarm(self.seconds)
  45. def __timeout(self, signum, frame):
  46. raise self
  47. @dataclass
  48. class PWManOpts:
  49. """UI command option parser.
  50. """
  51. __opts : list = field(default_factory=list)
  52. __params : list = field(default_factory=list)
  53. __atCmdIndex : dict = field(default_factory=dict)
  54. __error : Optional[Tuple[str, str]] = None
  55. @classmethod
  56. def parse(cls,
  57. line,
  58. optTemplates,
  59. ignoreFirst=False,
  60. unescape=True,
  61. softFail=False):
  62. """Parses the command options in 'line' and returns an Opts instance.
  63. optTemplates is a tuple of the possible options.
  64. """
  65. optTemplatesRaw = cls.rawOptTemplates(optTemplates)
  66. opts = cls()
  67. i = 0
  68. while True:
  69. p = cls.parseParam(line, i,
  70. ignoreFirst=ignoreFirst,
  71. unescape=unescape)
  72. if not p:
  73. break
  74. if opts.nrParams:
  75. opts._appendParam(i, p)
  76. else:
  77. try:
  78. optIdx = optTemplatesRaw.index(p)
  79. except ValueError:
  80. opts._appendParam(i, p)
  81. i += 1
  82. continue
  83. if optTemplates[optIdx].endswith(":"):
  84. i += 1
  85. arg = cls.parseParam(line, i,
  86. ignoreFirst=ignoreFirst,
  87. unescape=unescape)
  88. if not arg and softFail:
  89. opts._setError(p, "no_arg")
  90. break
  91. if not arg:
  92. PWMan._err(None, "Option '%s' "
  93. "requires an argument." % p)
  94. opts._appendOpt(i, p, arg)
  95. else:
  96. opts._appendOpt(i, p)
  97. i += 1
  98. return opts
  99. def _appendOpt(self, cmdIndex, optName, optValue=None):
  100. self.__opts.append( (optName, optValue) )
  101. self.__atCmdIndex[cmdIndex] = (optName, optValue)
  102. def _appendParam(self, cmdIndex, param):
  103. self.__params.append(param)
  104. self.__atCmdIndex[cmdIndex] = (None, param)
  105. def _setError(self, optName, error):
  106. self.__error = (optName, error)
  107. def __contains__(self, optName):
  108. """Check if we have a specific "-X" style option.
  109. """
  110. return optName in (o[0] for o in self.__opts)
  111. @property
  112. def error(self):
  113. return self.__error
  114. @property
  115. def hasOpts(self):
  116. """Do we have -X style options?
  117. """
  118. return bool(self.__opts)
  119. def getOpt(self, optName, default=None):
  120. """Get an option value by "-X" style name.
  121. """
  122. if optName in self:
  123. return [ o[1] for o in self.__opts if o[0] == optName ][-1]
  124. return default
  125. @property
  126. def nrParams(self):
  127. """The number of trailing parameters.
  128. """
  129. return len(self.__params)
  130. def getParam(self, index, default=None):
  131. """Get a trailing parameter at index.
  132. """
  133. if index < 0 or index >= self.nrParams:
  134. return default
  135. return self.__params[index]
  136. def getComplParamIdx(self, complText):
  137. """Get the parameter index in an active completion.
  138. complText: The partial parameter text in the completion.
  139. """
  140. if complText:
  141. paramIdx = self.nrParams - 1
  142. else:
  143. paramIdx = self.nrParams
  144. if paramIdx < 0:
  145. return None
  146. return paramIdx
  147. def atCmdIndex(self, cmdIndex):
  148. """Get an item (option or parameter) at command line index cmdIndex.
  149. Returns (optName, optValue) if it is an option.
  150. Returns (None, parameter) if it is a parameter.
  151. Returns (None, None) if it does not exist.
  152. """
  153. return self.__atCmdIndex.get(cmdIndex, (None, None))
  154. @classmethod
  155. def skipParams(cls, line, count,
  156. lineIncludesCommand=False, unescape=True):
  157. """Return a parameter string with the first 'count'
  158. parameters skipped.
  159. """
  160. sline = cls.patchSpaceEscapes(line)
  161. if lineIncludesCommand:
  162. count += 1
  163. i = 0
  164. while i < len(sline) and count > 0:
  165. while i < len(sline) and not sline[i].isspace():
  166. i += 1
  167. while i < len(sline) and sline[i].isspace():
  168. i += 1
  169. count -= 1
  170. if i >= len(sline):
  171. return ""
  172. s = line[i:]
  173. if unescape:
  174. s = unescapeCmd(s)
  175. return s
  176. @classmethod
  177. def calcParamIndex(cls, line, endidx):
  178. """Returns the parameter index into the commandline
  179. given the character end-index. This honors space-escape.
  180. """
  181. line = cls.patchSpaceEscapes(line)
  182. startidx = endidx - 1
  183. while startidx > 0 and not line[startidx].isspace():
  184. startidx -= 1
  185. return len([l for l in line[:startidx].split() if l]) - 1
  186. @classmethod
  187. def patchSpaceEscapes(cls, line):
  188. # Patch a commandline for simple whitespace based splitting.
  189. # We just replace the space escape sequence by a random
  190. # non-whitespace string. The line remains the same size.
  191. return line.replace('\\ ', '_S')
  192. @classmethod
  193. def parseParam(cls, line, paramIndex,
  194. ignoreFirst=False, unescape=True):
  195. """Returns the full parameter from the commandline.
  196. """
  197. sline = cls.patchSpaceEscapes(line)
  198. if ignoreFirst:
  199. paramIndex += 1
  200. inParam = False
  201. idx = 0
  202. for startIndex, c in enumerate(sline):
  203. if c.isspace():
  204. if inParam:
  205. idx += 1
  206. inParam = False
  207. else:
  208. inParam = True
  209. if idx == paramIndex:
  210. break
  211. else:
  212. return ""
  213. endIndex = startIndex
  214. while endIndex < len(sline) and not sline[endIndex].isspace():
  215. endIndex += 1
  216. p = line[startIndex : endIndex]
  217. if unescape:
  218. p = unescapeCmd(p)
  219. return p
  220. @classmethod
  221. def parseComplParam(cls, line, paramIndex, unescape=True):
  222. return cls.parseParam(line, paramIndex,
  223. ignoreFirst=True, unescape=unescape)
  224. @classmethod
  225. def parseParams(cls, line, paramIndex, count,
  226. ignoreFirst=False, unescape=True):
  227. """Returns a generator of the specified parameters from the commandline.
  228. paramIndex: start index.
  229. count: Number of paramerts to fetch.
  230. """
  231. return ( cls.parseParam(line, i, ignoreFirst, unescape)
  232. for i in range(paramIndex, paramIndex + count) )
  233. @classmethod
  234. def parseComplParams(cls, line, paramIndex, count, unescape=True):
  235. return cls.parseParams(line, paramIndex, count,
  236. ignoreFirst=True, unescape=unescape)
  237. @classmethod
  238. def rawOptTemplates(cls, optTemplates):
  239. """Remove the modifiers from opt templates.
  240. """
  241. return [ ot.replace(":", "") for ot in optTemplates ]
  242. # PWMan completion decorator that does common things and workarounds.
  243. def completion(func):
  244. @functools.wraps(func)
  245. def wrapper(self, text, line, begidx, endidx):
  246. try:
  247. self._timeout.poke()
  248. # Find the real begidx that takes space escapes into account.
  249. sline = PWManOpts.patchSpaceEscapes(line)
  250. realBegidx = endidx
  251. while realBegidx > 0:
  252. if sline[realBegidx - 1] == " ":
  253. break
  254. realBegidx -= 1
  255. if begidx == realBegidx:
  256. textPrefix = ""
  257. else:
  258. # Workaround: Patch the begidx to fully
  259. # honor all escapes. Remember the text
  260. # between the real begidx and the orig begidx.
  261. # It must be removed from the results.
  262. textPrefix = line[realBegidx : begidx]
  263. begidx = realBegidx
  264. # Fixup text.
  265. # By fetching the parameter again it is ensured that
  266. # it is properly unescaped.
  267. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  268. text = PWManOpts.parseComplParam(line, paramIdx)
  269. # Call the PWMan completion handler.
  270. completions = func(self, text, line, begidx, endidx)
  271. # If we fixed begidx in the workaround above,
  272. # we need to remove the additional prefix from the results,
  273. # because Cmd/readline won't expect it.
  274. if textPrefix:
  275. for i, comp in enumerate(copy(completions)):
  276. if comp.startswith(textPrefix):
  277. completions[i] = comp[len(textPrefix) : ]
  278. return completions
  279. except (EscapeError, CSQLError, PWManError, PWManTimeout) as e:
  280. return []
  281. except Exception as e:
  282. print("\nException in completion handler:\n\n%s" % (
  283. traceback.format_exc()),
  284. file=sys.stderr)
  285. return []
  286. return wrapper
  287. class PWManMeta(type):
  288. def __new__(cls, name, bases, dct):
  289. for name, attr in dct.items():
  290. # Fixup command docstrings.
  291. if (name.startswith("do_") and
  292. not getattr(attr, "_pwman_fixed", False) and
  293. attr.__doc__):
  294. # Remove leading double-tabs.
  295. attr.__doc__, n = re.subn("^\t\t", "\t", attr.__doc__,
  296. 0, re.MULTILINE)
  297. # Remove trailing white space.
  298. attr.__doc__ = attr.__doc__.rstrip()
  299. # Tabs to spaces.
  300. attr.__doc__, n = re.subn("\t", " " * 8, attr.__doc__,
  301. 0, re.MULTILINE)
  302. attr._pwman_fixed = True
  303. return super().__new__(cls, name, bases, dct)
  304. class PWMan(Cmd, metaclass=PWManMeta):
  305. class CommandError(Exception): pass
  306. class Quit(Exception): pass
  307. def __init__(self, filename, passphrase, timeout=None):
  308. super().__init__()
  309. self.__isInteractive = False
  310. if sys.flags.optimize >= 2:
  311. # We need docstrings.
  312. raise PWManError("pwman does not support "
  313. "Python optimization level 2 (-OO). "
  314. "Please call with python3 -O or less.")
  315. # argument delimiter shall be space.
  316. readline.set_completer_delims(" ")
  317. self.__dbs = {
  318. "main" : PWManDatabase(filename, passphrase, readOnly=False),
  319. }
  320. self.__selDbName = "main"
  321. self.__updatePrompt()
  322. self._timeout = PWManTimeout(timeout)
  323. @property
  324. def __db(self):
  325. return self._getDb(self.__selDbName)
  326. def _getDb(self, name):
  327. return self.__dbs.get(name, None)
  328. def __updatePrompt(self):
  329. if len(self.__dbs) > 1:
  330. dbName = self.__selDbName
  331. lim = 20
  332. if len(dbName) > lim - 3:
  333. dbName = dbName[:lim-3] + "..."
  334. else:
  335. dbName = ""
  336. dirty = any(db.isDirty() for db in self.__dbs.values())
  337. self.prompt = "%spwman%s%s$ " % (
  338. "*" if dirty else "",
  339. "/" if dbName else "",
  340. dbName
  341. )
  342. @classmethod
  343. def _err(cls, source, message):
  344. source = (" " + source + ":") if source else ""
  345. raise cls.CommandError("***%s %s" % (source, message))
  346. @classmethod
  347. def _warn(cls, source, message):
  348. source = (" " + source + ":") if source else ""
  349. print("***%s %s" % (source, message))
  350. @classmethod
  351. def _info(cls, source, message):
  352. source = ("+++ " + source + ": ") if source else ""
  353. print("%s%s" % (source, message))
  354. def precmd(self, line):
  355. self._timeout.poke()
  356. first = PWManOpts.parseParam(line, 0, unescape=False)
  357. if first.endswith('?'):
  358. return "help %s" % first[:-1]
  359. return line
  360. def postcmd(self, stop, line):
  361. self.__updatePrompt()
  362. self._timeout.poke()
  363. def default(self, line):
  364. extra = "\nType 'help' for more help." if self.__isInteractive else ""
  365. self._err(None, "Unknown command: %s%s" % (line, extra))
  366. def emptyline(self):
  367. self._timeout.poke()
  368. # Don't repeat the last command
  369. @completion
  370. def __complete_category_title(self, text, line, begidx, endidx):
  371. # Generic [category] [title] completion
  372. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  373. if paramIdx == 0:
  374. # Category completion
  375. return self.__getCategoryCompletions(text)
  376. elif paramIdx == 1:
  377. # Entry title completion
  378. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  379. text)
  380. return []
  381. @completion
  382. def __complete_category_title_item(self, text, line, begidx, endidx):
  383. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  384. if paramIdx in (0, 1):
  385. return self.__complete_category_title(text, line, begidx, endidx)
  386. category, title, item = PWManOpts.parseComplParams(line, 0, 3)
  387. cmpl = []
  388. if paramIdx == 2:
  389. cmpl.extend(escapeCmd(n) + " "
  390. for n in ("user", "password", "bulk", "totpkey")
  391. if n.startswith(item))
  392. cmpl.extend(self.__getEntryAttrCompletions(category, title, item,
  393. doName=(paramIdx == 2),
  394. doData=False,
  395. text=text))
  396. return cmpl
  397. def __getCategoryCompletions(self, text, db=None):
  398. db = db or self.__db
  399. return [ escapeCmd(n) + " "
  400. for n in db.getCategoryNames()
  401. if n.startswith(text) ]
  402. def __getEntryTitleCompletions(self, category, text, db=None):
  403. db = db or self.__db
  404. return [ escapeCmd(t) + " "
  405. for t in db.getEntryTitles(category)
  406. if t.startswith(text) ]
  407. def __getEntryAttrCompletions(self, category, title, name, doName, doData, text, db=None):
  408. db = db or self.__db
  409. if category and title:
  410. entry = db.getEntry(category, title)
  411. if entry:
  412. if doName: # complete name
  413. entryAttrs = db.getEntryAttrs(entry)
  414. if entryAttrs:
  415. return [ escapeCmd(entryAttr.name) + " "
  416. for entryAttr in entryAttrs
  417. if entryAttr.name.startswith(name) ]
  418. elif doData: # complete data
  419. entryAttr = db.getEntryAttr(entry, name)
  420. if entryAttr:
  421. return [ escapeCmd(entryAttr.data) + " " ]
  422. return []
  423. def __getDatabaseCompletions(self, text):
  424. return [ escapeCmd(n) + " "
  425. for n in self.__dbs.keys()
  426. if n.startswith(text) ]
  427. def __getPathCompletions(self, text):
  428. """Return an escaped file system path completion.
  429. 'text' is the unescaped partial path string.
  430. """
  431. try:
  432. path = pathlib.Path(text)
  433. trailingChar = text[-1] if text else ""
  434. sep = os.path.sep
  435. base = path.parts[-1] if path.parts else ""
  436. dirPath = pathlib.Path(*path.parts[:-1])
  437. dirPathListing = [ f for f in dirPath.iterdir()
  438. if f.parts[-1].startswith(base) ]
  439. if (path.is_dir() and
  440. (trailingChar in (sep, "/", "\\") or
  441. len(dirPathListing) <= 1)):
  442. # path is an unambiguous directory.
  443. # Show its contents.
  444. useListing = path.iterdir()
  445. else:
  446. # path is a file or an ambiguous directory.
  447. # Show the alternatives.
  448. useListing = dirPathListing
  449. return [ escapeCmd(str(f)) + (escapeCmd(sep) if f.is_dir() else " ")
  450. for f in useListing ]
  451. except OSError:
  452. pass
  453. return []
  454. cmdHelpShow = (
  455. ("list", ("ls", "cat"), "List/print entry contents"),
  456. ("find", ("f",), "Search the database for patterns"),
  457. ("totp", ("t",), "Generate TOTP token"),
  458. ("diff", (), "Show the database differences"),
  459. )
  460. cmdHelpEdit = (
  461. ("new", ("n", "add"), "Create new entry"),
  462. ("edit_user", ("eu",), "Edit the 'user' field of an entry"),
  463. ("edit_pw", ("ep",), "Edit the 'password' field of an entry"),
  464. ("edit_bulk", ("eb",), "Edit the 'bulk' field of an entry"),
  465. ("edit_totp", ("et",), "Edit the TOTP key and parameters"),
  466. ("edit_attr", ("ea",), "Edit an entry attribute"),
  467. ("move", ("mv", "rename"), "Move/rename an existing entry"),
  468. ("copy", ("cp",), "Copy an existing entry or category"),
  469. ("remove", ("rm", "del"), "Remove an existing entry"),
  470. )
  471. cmdHelpDatabase = (
  472. ("database", ("db",), "Open or select another database"),
  473. ("commit", ("c", "w"), "Commit/write selected db to disk"),
  474. ("drop", (), "Drop uncommitted changes in selected db"),
  475. ("close", (), "Close a database"),
  476. ("dbdump", (), "Dump the selected database"),
  477. ("dbimport", (), "Import a database dump file"),
  478. ("masterp", (), "Change the master passphrase"),
  479. )
  480. cmdHelpMisc = (
  481. ("help", ("h",), "Show help about commands"),
  482. ("quit", ("q", "exit", "^D"), "Quit pwman"),
  483. ("cls", (), "Clear screen"),
  484. )
  485. def do_help(self, params):
  486. """--- Shows help text about a command ---
  487. Command: help [COMMAND]
  488. If COMMAND is not given: Show a command summary.
  489. If COMMAND is given: Show detailed help about that command.
  490. Aliases: h
  491. """
  492. if params:
  493. Cmd.do_help(self, params)
  494. return
  495. def printCmdHelp(cmdHelp):
  496. for cmd, aliases, desc in cmdHelp:
  497. spc = " " * (10 - len(cmd))
  498. msg = " %s%s%s" % (cmd, spc, desc)
  499. if aliases:
  500. msg += " " * (52 - len(msg))
  501. msg += " Alias%s: %s" %\
  502. ("es" if len(aliases) > 1 else "",
  503. ", ".join(aliases))
  504. self._info(None, msg)
  505. self._info(None, "\nSearching/listing commands:")
  506. printCmdHelp(self.cmdHelpShow)
  507. self._info(None, "\nEditing commands:")
  508. printCmdHelp(self.cmdHelpEdit)
  509. self._info(None, "\nDatabase commands:")
  510. printCmdHelp(self.cmdHelpDatabase)
  511. self._info(None, "\nMisc commands:")
  512. printCmdHelp(self.cmdHelpMisc)
  513. self._info(None, "\nType 'command?' or 'help command' for more help on a command.")
  514. do_h = do_help
  515. def do_quit(self, params):
  516. """--- Exit pwman ---
  517. Command: quit [!]
  518. Use the exclamation mark to force quit and discard changes.
  519. Aliases: q exit ^D
  520. """
  521. if params == "!":
  522. for db in self.__dbs.values():
  523. db.flunkDirty()
  524. raise self.Quit()
  525. do_q = do_quit
  526. do_exit = do_quit
  527. do_EOF = do_quit
  528. def do_cls(self, params):
  529. """--- Clear console screen ---
  530. Command: cls
  531. Clear the console screen.
  532. Note that this does not clear a possibly existing
  533. 'screen' session buffer or other advanced console buffers.
  534. Aliases: None
  535. """
  536. clearScreen()
  537. __commit_opts = ("-a",)
  538. def do_commit(self, params):
  539. """--- Write changes to the database file(s) ---
  540. Command: commit
  541. Options:
  542. -a Commit all open databases.
  543. Aliases: c w
  544. """
  545. opts = PWManOpts.parse(params, self.__commit_opts)
  546. dbs = self.__dbs.values() if "-a" in opts else [ self.__db ]
  547. try:
  548. for db in dbs:
  549. db.commit()
  550. except PWManError as e:
  551. self._err("commit", str(e))
  552. do_c = do_commit
  553. do_w = do_commit
  554. @completion
  555. def complete_commit(self, text, line, begidx, endidx):
  556. if text == "-":
  557. return PWManOpts.rawOptTemplates(self.__commit_opts)
  558. return []
  559. complete_c = complete_commit
  560. complete_w = complete_commit
  561. def do_masterp(self, params):
  562. """--- Change the master passphrase ---
  563. Command: masterp
  564. Aliases: None
  565. """
  566. p = readPassphrase("Current master passphrase")
  567. if p != self.__db.getPassphrase():
  568. time.sleep(1)
  569. self._warn(None, "Passphrase mismatch! ")
  570. return
  571. p = readPassphrase("Master passphrase", verify=True)
  572. if p is None:
  573. self._info(None, "Passphrase not changed.")
  574. return
  575. if p != self.__db.getPassphrase():
  576. self.__db.setPassphrase(p)
  577. def do_list(self, params):
  578. """--- Print a listing ---
  579. Command: list [category] [title] [item]
  580. If a category is given as parameter, list the
  581. contents of the category. If category and entry
  582. are given, list the contents of the entry.
  583. If item is given, then only list one specific content item.
  584. Item may be one of: user, password, bulk, totpkey or any attribute name.
  585. Aliases: ls cat
  586. """
  587. category, title, item = PWManOpts.parseParams(params, 0, 3)
  588. if not category and not title and not item:
  589. self._info(None, "Categories:")
  590. self._info(None, "\t" + "\n\t".join(self.__db.getCategoryNames()))
  591. elif category and not title and not item:
  592. self._info(None, "Entries in category '%s':" % category)
  593. self._info(None, "\t" + "\n\t".join(self.__db.getEntryTitles(category)))
  594. elif category and title and not item:
  595. entry = self.__db.getEntry(category, title)
  596. if entry:
  597. self._info(None, self.__db.dumpEntry(entry))
  598. else:
  599. self._err("list", "'%s/%s' not found" % (category, title))
  600. elif category and title and item:
  601. entry = self.__db.getEntry(category, title)
  602. if entry:
  603. if item == "user":
  604. if not entry.user:
  605. self._err("list", "'%s/%s' has no 'user' field." % (
  606. category, title))
  607. self._info(None, entry.user)
  608. elif item == "password":
  609. if not entry.pw:
  610. self._err("list", "'%s/%s' has no 'password' field." % (
  611. category, title))
  612. self._info(None, entry.pw)
  613. elif item == "bulk":
  614. bulk = self.__db.getEntryBulk(entry)
  615. if not bulk:
  616. self._err("list", "'%s/%s' has no 'bulk' field." % (
  617. category, title))
  618. self._info(None, bulk.data)
  619. elif item == "totpkey":
  620. entryTotp = self.__db.getEntryTotp(entry)
  621. if not entryTotp:
  622. self._err("list", "'%s/%s' has no 'TOTP key'." % (
  623. category, title))
  624. self._info(None, "TOTP key: %s (base32 encoding)" % entryTotp.key)
  625. self._info(None, "TOTP digits: %d" % entryTotp.digits)
  626. self._info(None, "TOTP hash: %s" % entryTotp.hmacHash)
  627. else: # attribute
  628. attr = self.__db.getEntryAttr(entry, item)
  629. if not attr:
  630. self._err("list", "'%s/%s' has no attribute '%s'." % (
  631. category, title, item))
  632. self._info(None, attr.data)
  633. else:
  634. self._err("list", "'%s/%s' not found" % (category, title))
  635. else:
  636. self._err("list", "Invalid parameter")
  637. do_ls = do_list
  638. do_cat = do_list
  639. complete_list = __complete_category_title_item
  640. complete_ls = complete_list
  641. complete_cat = complete_list
  642. def do_new(self, params):
  643. """--- Create a new entry ---
  644. Command: new [category] [title] [user] [password]
  645. Create a new database entry. If no parameters are given,
  646. they are asked for interactively.
  647. Aliases: n add
  648. """
  649. if params:
  650. category, title, user, pw = PWManOpts.parseParams(params, 0, 4)
  651. else:
  652. self._info("new", "Create new entry:")
  653. category = input("\tCategory: ")
  654. title = input("\tEntry title: ")
  655. user = input("\tUsername: ")
  656. pw = input("\tPassword: ")
  657. if not category or not title:
  658. self._err("new", "Invalid parameters. "
  659. "Need to supply category and title.")
  660. entry = PWManEntry(category=category, title=title, user=user, pw=pw)
  661. try:
  662. self.__db.addEntry(entry)
  663. except (PWManError) as e:
  664. self._err("new", str(e))
  665. do_n = do_new
  666. do_add = do_new
  667. complete_new = __complete_category_title
  668. complete_n = complete_new
  669. complete_add = complete_new
  670. def __do_edit_entry(self, params, commandName,
  671. entry2data, data2entry):
  672. category, title = PWManOpts.parseParams(params, 0, 2)
  673. if not category or not title:
  674. self._err(commandName, "Invalid parameters. "
  675. "Need to supply category and title.")
  676. newData = PWManOpts.skipParams(params, 2).strip()
  677. try:
  678. self.__db.editEntry(data2entry(category, title, newData))
  679. except (PWManError) as e:
  680. self._err(commandName, str(e))
  681. def do_edit_user(self, params):
  682. """--- Edit the 'user' field of an existing entry ---
  683. Command: edit_user category title NEWDATA...
  684. Change the 'user' field of an existing database entry.
  685. NEWDATA is the new data to write into the 'user' field.
  686. The NEWDATA must _not_ be escaped (however, category and
  687. title must be escaped).
  688. Aliases: eu
  689. """
  690. self.__do_edit_entry(params, "edit_user",
  691. lambda entry: entry.user,
  692. lambda cat, tit, data: PWManEntry(cat, tit, user=data))
  693. do_eu = do_edit_user
  694. @completion
  695. def complete_edit_user(self, text, line, begidx, endidx):
  696. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  697. if paramIdx == 0:
  698. # Category completion
  699. return self.__getCategoryCompletions(text)
  700. elif paramIdx == 1:
  701. # Entry title completion
  702. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  703. text)
  704. elif paramIdx == 2:
  705. # User data
  706. entry = self.__db.getEntry(PWManOpts.parseComplParam(line, 0),
  707. PWManOpts.parseComplParam(line, 1))
  708. return [ escapeCmd(entry.user) ]
  709. return []
  710. complete_eu = complete_edit_user
  711. def do_edit_pw(self, params):
  712. """--- Edit the 'password' field of an existing entry ---
  713. Command: edit_pw category title NEWDATA...
  714. Change the 'password' field of an existing database entry.
  715. NEWDATA is the new data to write into the 'password' field.
  716. The NEWDATA must _not_ be escaped (however, category and
  717. title must be escaped).
  718. Aliases: ep
  719. """
  720. self.__do_edit_entry(params, "edit_pw",
  721. lambda entry: entry.pw,
  722. lambda cat, tit, data: PWManEntry(cat, tit, pw=data))
  723. do_ep = do_edit_pw
  724. @completion
  725. def complete_edit_pw(self, text, line, begidx, endidx):
  726. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  727. if paramIdx == 0:
  728. # Category completion
  729. return self.__getCategoryCompletions(text)
  730. elif paramIdx == 1:
  731. # Entry title completion
  732. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  733. text)
  734. elif paramIdx == 2:
  735. # Password data
  736. entry = self.__db.getEntry(PWManOpts.parseComplParam(line, 0),
  737. PWManOpts.parseComplParam(line, 1))
  738. return [ escapeCmd(entry.pw) ]
  739. return []
  740. complete_ep = complete_edit_pw
  741. def do_edit_bulk(self, params):
  742. """--- Edit the 'bulk' field of an existing entry ---
  743. Command: edit_bulk category title NEWDATA...
  744. Change the 'bulk' field of an existing database entry.
  745. NEWDATA is the new data to write into the 'bulk' field.
  746. The NEWDATA must _not_ be escaped (however, category and
  747. title must be escaped).
  748. Aliases: eb
  749. """
  750. category, title = PWManOpts.parseParams(params, 0, 2)
  751. data = PWManOpts.skipParams(params, 2).strip()
  752. if not category:
  753. self._err("edit_bulk", "Category parameter is required.")
  754. if not title:
  755. self._err("edit_bulk", "Title parameter is required.")
  756. entry = self.__db.getEntry(category, title)
  757. if not entry:
  758. self._err("edit_bulk", "'%s/%s' not found" % (category, title))
  759. entryBulk = self.__db.getEntryBulk(entry)
  760. if not entryBulk:
  761. entryBulk = PWManEntryBulk(entry=entry)
  762. entryBulk.data = data
  763. self.__db.setEntryBulk(entryBulk)
  764. do_eb = do_edit_bulk
  765. @completion
  766. def complete_edit_bulk(self, text, line, begidx, endidx):
  767. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  768. if paramIdx == 0:
  769. # Category completion
  770. return self.__getCategoryCompletions(text)
  771. elif paramIdx == 1:
  772. # Entry title completion
  773. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  774. text)
  775. elif paramIdx == 2:
  776. # Bulk data
  777. entry = self.__db.getEntry(PWManOpts.parseComplParam(line, 0),
  778. PWManOpts.parseComplParam(line, 1))
  779. if entry:
  780. entryBulk = self.__db.getEntryBulk(entry)
  781. if entryBulk:
  782. return [ escapeCmd(entryBulk.data) ]
  783. return []
  784. complete_eb = complete_edit_bulk
  785. def do_remove(self, params):
  786. """--- Remove an existing entry ---
  787. Command: remove category [title]
  788. Remove an existing database entry.
  789. Aliases: rm del
  790. """
  791. category, title = PWManOpts.parseParams(params, 0, 2)
  792. if not category:
  793. self._err("remove", "Category parameter is required.")
  794. if not title:
  795. # Remove whole category
  796. for title in self.__db.getEntryTitles(category):
  797. p = "%s %s" % (escapeCmd(category),
  798. escapeCmd(title))
  799. self._info("remove", "running command: remove %s" % p)
  800. self.do_remove(p)
  801. return
  802. try:
  803. self.__db.delEntry(PWManEntry(category, title))
  804. except (PWManError) as e:
  805. self._err("remove", str(e))
  806. do_rm = do_remove
  807. do_del = do_remove
  808. complete_remove = __complete_category_title
  809. complete_rm = complete_remove
  810. complete_del = complete_remove
  811. __move_copy_opts = ("-s:", "-d:")
  812. def __do_move_copy(self, command, params):
  813. opts = PWManOpts.parse(params, self.__move_copy_opts)
  814. sourceDbName = opts.getOpt("-s", default=self.__selDbName)
  815. sourceDb = self._getDb(sourceDbName)
  816. if sourceDb is None:
  817. self._err(command, "Source database '%s' does not exist" % sourceDbName)
  818. destDbName = opts.getOpt("-d", default=self.__selDbName)
  819. destDb = self._getDb(destDbName)
  820. if destDb is None:
  821. self._err(command, "Destination database '%s' does not exist" % destDbName)
  822. if opts.nrParams in (3, 4):
  823. # Entry rename/move or copy
  824. fromCategory, fromTitle, toCategory, toTitle =\
  825. (opts.getParam(0), opts.getParam(1),
  826. opts.getParam(2), opts.getParam(3))
  827. toTitle = toTitle or fromTitle
  828. entry = sourceDb.getEntry(fromCategory, fromTitle)
  829. if not entry:
  830. self._err(command, "Source entry does not exist.")
  831. if sourceDb is destDb and fromCategory == toCategory and fromTitle == toTitle:
  832. return
  833. try:
  834. sourceDb.moveEntry(entry, toCategory, toTitle,
  835. toDb=destDb,
  836. copy=(command == "copy"))
  837. except (PWManError) as e:
  838. self._err(command, str(e))
  839. elif (sourceDb is destDb and opts.nrParams == 2) or\
  840. (sourceDb is not destDb and opts.nrParams in (1, 2)):
  841. # Whole category move or copy.
  842. fromCategory, toCategory = opts.getParam(0), opts.getParam(1)
  843. toCategory = toCategory or fromCategory
  844. try:
  845. sourceDb.moveEntries(fromCategory, toCategory,
  846. toDb=destDb,
  847. copy=(command == "copy"))
  848. except (PWManError) as e:
  849. self._err(command, str(e))
  850. else:
  851. self._err(command, "Invalid parameters.")
  852. @completion
  853. def __complete_move_copy(self, text, line, begidx, endidx):
  854. if text == "-":
  855. return PWManOpts.rawOptTemplates(self.__move_copy_opts)
  856. if len(text) == 2 and text.startswith("-"):
  857. return [ text + " " ]
  858. dbOpts = ("-s", "-d")
  859. opts = PWManOpts.parse(line, self.__move_copy_opts, ignoreFirst=True, softFail=True)
  860. if opts.error:
  861. opt, error = opts.error
  862. if error == "no_arg" and opt in dbOpts:
  863. return self.__getDatabaseCompletions(text)
  864. return []
  865. optName, value = opts.atCmdIndex(PWManOpts.calcParamIndex(line, endidx))
  866. if optName in dbOpts:
  867. return self.__getDatabaseCompletions(text)
  868. sourceDbName = opts.getOpt("-s", default=self.__selDbName)
  869. sourceDb = self._getDb(sourceDbName)
  870. if sourceDb is None:
  871. return []
  872. destDbName = opts.getOpt("-d", default=self.__selDbName)
  873. destDb = self._getDb(destDbName)
  874. if destDb is None:
  875. return []
  876. paramIdx = opts.getComplParamIdx(text)
  877. if paramIdx == 0:
  878. # Category completion
  879. return self.__getCategoryCompletions(text, db=sourceDb)
  880. elif paramIdx == 1:
  881. # Entry title completion
  882. category = opts.getParam(0)
  883. if category:
  884. compl = self.__getEntryTitleCompletions(category, text, db=sourceDb)
  885. if compl:
  886. return compl
  887. # Category completion
  888. return self.__getCategoryCompletions(text, db=destDb)
  889. elif paramIdx == 2:
  890. # Category completion
  891. return self.__getCategoryCompletions(text, db=destDb)
  892. elif paramIdx == 3:
  893. # Entry title completion
  894. category = opts.getParam(2)
  895. if category:
  896. return self.__getEntryTitleCompletions(category, text, db=destDb)
  897. return []
  898. def do_move(self, params):
  899. """--- Move/rename an existing entry or a category ---
  900. Move/rename an existing entry:
  901. Command: move CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  902. (NEW_TITLE defaults to TITLE)
  903. Move all entries from one category into another category.
  904. Command: move FROM_CATEGORY TO_CATEGORY
  905. Move an entry from one database to another:
  906. Command: move -s main -d other CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  907. (NEW_TITLE defaults to TITLE)
  908. Move all entries from a category from one database into another database:
  909. Command: move -s main -d other FROM_CATEGORY [TO_CATEGORY]
  910. (TO_CATEGORY defaults to FROM_CATEGORY)
  911. Options:
  912. -s SOURCE_DATABASE_NAME
  913. -d DESTINATION_DATABASE_NAME
  914. Databases default to the currently selected database.
  915. The named databases must be open. See 'database' command.
  916. Aliases: mv rename
  917. """
  918. self.__do_move_copy("move", params)
  919. do_mv = do_move
  920. do_rename = do_move
  921. complete_move = __complete_move_copy
  922. complete_mv = complete_move
  923. complete_rename = complete_move
  924. __copy_opts = ("-s:", "-d:")
  925. def do_copy(self, params):
  926. """--- Copy an entry or a category ---
  927. Copy an existing entry:
  928. Command: copy CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  929. (NEW_TITLE defaults to TITLE)
  930. Copy all entries from a category into another category:
  931. Command: copy FROM_CATEGORY TO_CATEGORY
  932. Copy an entry from one database to another:
  933. Command: copy -s main -d other CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  934. (NEW_TITLE defaults to TITLE)
  935. Copy all entries from a category from one database into another database:
  936. Command: copy -s main -d other FROM_CATEGORY [TO_CATEGORY]
  937. (TO_CATEGORY defaults to FROM_CATEGORY)
  938. Options:
  939. -s SOURCE_DATABASE_NAME
  940. -d DESTINATION_DATABASE_NAME
  941. Databases default to the currently selected database.
  942. The named databases must be open. See 'database' command.
  943. Aliases: cp
  944. """
  945. self.__do_move_copy("copy", params)
  946. do_cp = do_copy
  947. complete_copy = __complete_move_copy
  948. complete_cp = complete_copy
  949. __database_opts = ("-f:",)
  950. def do_database(self, params):
  951. """--- Open a database or switch to an already opened database ---
  952. Command: database [-f FILEPATH] [NAME]
  953. If neither FILEPATH nor NAME are given, then
  954. a list of all currently opened databases will be printed.
  955. The currently selected database will be marked with [@].
  956. All databases with uncommitted changes will be marked with [*].
  957. If only NAME is given, then the selected database will
  958. be switched to the named one. NAME must already be open.
  959. A new database can be opened with -f FILEPATH.
  960. NAME is optional in this case.
  961. The selected database will be switched to the newly opened one.
  962. Aliases: db
  963. """
  964. opts = PWManOpts.parse(params, self.__database_opts)
  965. path = opts.getOpt("-f")
  966. name = opts.getParam(0)
  967. if path:
  968. if opts.nrParams not in (0, 1):
  969. self._err("database", "Invalid parameters.")
  970. # Open a new db.
  971. path = pathlib.Path(path)
  972. name = name or path.name
  973. if name == "main":
  974. self._err("database",
  975. "The database name 'main' is reserved. "
  976. "Please select another name.")
  977. if name in self.__dbs:
  978. self._err("database",
  979. ("The database name '%' is already used. "
  980. "Please select another name.") % name)
  981. try:
  982. passphrase = readPassphrase(
  983. "Master passphrase of '%s'" % path,
  984. verify=not path.exists())
  985. if passphrase is None:
  986. self._err("database", "Could not get passphrase.")
  987. db = PWManDatabase(filename=path,
  988. passphrase=passphrase,
  989. readOnly=False)
  990. except PWManError as e:
  991. self._err("database", str(e))
  992. self.__dbs[name] = db
  993. self.__selDbName = name
  994. elif opts.nrParams == 1:
  995. # Switch selected db to NAME.
  996. if name not in self.__dbs:
  997. self._err("database", "The database '%s' does not exist." % name)
  998. if name != self.__selDbName:
  999. self.__selDbName = name
  1000. elif opts.nrParams == 0:
  1001. # Print db list.
  1002. for name, db in self.__dbs.items():
  1003. flags = "@" if db is self.__db else " "
  1004. flags += "*" if db.isDirty() else " "
  1005. path = db.getFilename()
  1006. self._info(None, "[%s] %s: %s" % (
  1007. flags, name, path))
  1008. else:
  1009. self._err("database", "Invalid parameters.")
  1010. do_db = do_database
  1011. @completion
  1012. def complete_database(self, text, line, begidx, endidx):
  1013. if text == "-":
  1014. return PWManOpts.rawOptTemplates(self.__database_opts)
  1015. if len(text) == 2 and text.startswith("-"):
  1016. return [ text + " " ]
  1017. opts = PWManOpts.parse(line, self.__database_opts, ignoreFirst=True, softFail=True)
  1018. if opts.error:
  1019. opt, error = opts.error
  1020. if error == "no_arg" and opt == "-f":
  1021. return self.__getPathCompletions(text)
  1022. return []
  1023. optName, value = opts.atCmdIndex(PWManOpts.calcParamIndex(line, endidx))
  1024. if optName == "-f":
  1025. return self.__getPathCompletions(text)
  1026. paramIdx = opts.getComplParamIdx(text)
  1027. if paramIdx == 0:
  1028. # Database name
  1029. return self.__getDatabaseCompletions(text)
  1030. return []
  1031. complete_db = complete_database
  1032. __dbdump_opts = ("-s", "-h", "-c")
  1033. def do_dbdump(self, params):
  1034. """--- Dump the pwman SQL database ---
  1035. Command: dbdump [OPTS] [FILEPATH]
  1036. If FILEPATH is given, the database is dumped
  1037. unencrypted to the file.
  1038. If FILEPATH is omitted, the database is dumped
  1039. unencrypted to stdout.
  1040. OPTS may be one of:
  1041. -s Dump format SQL. (default)
  1042. -h Dump format human readable text.
  1043. -c Dump format CSV.
  1044. WARNING: The database dump is not encrypted.
  1045. Aliases: None
  1046. """
  1047. opts = PWManOpts.parse(params, self.__dbdump_opts)
  1048. if opts.nrParams > 1:
  1049. self._err("dbdump", "Too many arguments.")
  1050. optFmtSqlDump = "-s" in opts
  1051. optFmtHumanReadable = "-h" in opts
  1052. optFmtCsv = "-c" in opts
  1053. numFmtOpts = int(optFmtSqlDump) + int(optFmtHumanReadable) + int(optFmtCsv)
  1054. if not 0 <= numFmtOpts <= 1:
  1055. self._err("dbdump", "Multiple format OPTions. "
  1056. "Only one is allowed.")
  1057. if numFmtOpts == 0:
  1058. optFmtSqlDump = True
  1059. dumpFile = opts.getParam(0)
  1060. try:
  1061. if optFmtSqlDump:
  1062. dump = self.__db.sqlPlainDump() + b"\n"
  1063. elif optFmtHumanReadable:
  1064. dump = self.__db.dumpEntries(showTotpKey=True)
  1065. dump = dump.encode("UTF-8") + b"\n"
  1066. elif optFmtCsv:
  1067. dump = self.__db.dumpEntriesCsv(showTotpKey=True)
  1068. dump = dump.encode("UTF-8")
  1069. else:
  1070. assert(0)
  1071. if dumpFile:
  1072. with open(dumpFile, "wb") as f:
  1073. f.write(dump)
  1074. else:
  1075. stdout(dump)
  1076. except UnicodeError as e:
  1077. self._err("dbdump", "Unicode error.")
  1078. except IOError as e:
  1079. self._err("dbdump", "Failed to write dump: %s" % e.strerror)
  1080. @completion
  1081. def complete_dbdump(self, text, line, begidx, endidx):
  1082. if text == "-":
  1083. return PWManOpts.rawOptTemplates(self.__dbdump_opts)
  1084. if len(text) == 2 and text.startswith("-"):
  1085. return [ text + " " ]
  1086. opts = PWManOpts.parse(line, self.__dbdump_opts, ignoreFirst=True, softFail=True)
  1087. if opts.error:
  1088. return []
  1089. paramIdx = opts.getComplParamIdx(text)
  1090. if paramIdx == 0:
  1091. # filepath
  1092. return self.__getPathCompletions(text)
  1093. return []
  1094. def do_dbimport(self, params):
  1095. """--- Import an SQL database dump ---
  1096. Command: dbimport FILEPATH
  1097. Import the FILEPATH into the current database.
  1098. The database is cleared before importing the file!
  1099. Aliases: None
  1100. """
  1101. try:
  1102. if not params.strip():
  1103. raise IOError("FILEPATH is empty.")
  1104. with open(params, "rb") as f:
  1105. data = f.read().decode("UTF-8")
  1106. self.__db.importSqlScript(data)
  1107. self._info("dbimport", "success.")
  1108. except (CSQLError, IOError, UnicodeError) as e:
  1109. self._err("dbimport", "Failed to import dump: %s" % str(e))
  1110. @completion
  1111. def complete_dbimport(self, text, line, begidx, endidx):
  1112. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  1113. if paramIdx == 0:
  1114. return self.__getPathCompletions(text)
  1115. return []
  1116. def do_drop(self, params):
  1117. """--- Drop all uncommitted changes ---
  1118. Command: drop
  1119. Aliases: None
  1120. """
  1121. self.__db.dropUncommitted()
  1122. def do_close(self, params):
  1123. """--- Close a database ---
  1124. Command: close [!] [NAME]
  1125. If NAME is not given, then this closes the currently selected database.
  1126. If NAME is given, then this closes the named database.
  1127. If ! is specified, then the uncommitted changes will be dropped.
  1128. If the currently used database is closed, the selected database
  1129. will be switched to 'main'.
  1130. The 'main' database can only be closed last,
  1131. which in turn closes the application.
  1132. Aliases: None
  1133. """
  1134. flunk = params.startswith("!")
  1135. if flunk:
  1136. params = params[1:].strip()
  1137. name = params if params else self.__selDbName
  1138. if name == "main" and len(self.__dbs) > 1:
  1139. self._err("close", "The 'main' database can only be closed last")
  1140. db = self._getDb(name)
  1141. if db is None:
  1142. self._err("close", "The database '%s' does not exist" % name)
  1143. if db.isDirty():
  1144. if not flunk:
  1145. self._err("close", "The database '%s' contains "
  1146. "uncommitted changes" % name)
  1147. db.flunkDirty()
  1148. if len(self.__dbs) > 1:
  1149. self.__dbs.pop(name)
  1150. if self.__selDbName == name:
  1151. self.__selDbName = "main"
  1152. else:
  1153. raise self.Quit()
  1154. @completion
  1155. def complete_close(self, text, line, begidx, endidx):
  1156. if text == "!":
  1157. return [ text + " " ]
  1158. opts = PWManOpts.parse(line, (), ignoreFirst=True, softFail=True)
  1159. if opts.error:
  1160. return []
  1161. paramIdx = opts.getComplParamIdx(text)
  1162. if paramIdx == 0 or (paramIdx == 1 and opts.getParam(0) == "!"):
  1163. # Database name
  1164. return self.__getDatabaseCompletions(text)
  1165. return []
  1166. __find_opts = ("-c", "-t", "-u", "-p", "-b", "-a", "-A", "-r")
  1167. def do_find(self, params):
  1168. """--- Search the database ---
  1169. Command: find [OPTS] [IN_CATEGORY] PATTERN
  1170. Searches the database for patterns. If 'IN_CATEGORY' is given, only search
  1171. in the specified category.
  1172. PATTERN may either use SQL LIKE wildcards (without -r)
  1173. or Python Regular Expression special characters (with -r).
  1174. OPTS may be one or multiple of:
  1175. -c Match 'category' (only if no IN_CATEGORY parameter)
  1176. -t Match 'title' (*)
  1177. -u Match 'user' (*)
  1178. -p Match 'password' (*)
  1179. -b Match 'bulk' (*)
  1180. -a Match 'attribute data' (*)
  1181. -A Match 'attribute name'
  1182. -r Use Python Regular Expression matching
  1183. (*) = These OPTS are enabled by default, if and only if
  1184. none of them are specified by the user.
  1185. Aliases: f
  1186. """
  1187. opts = PWManOpts.parse(params, self.__find_opts)
  1188. mCategory = "-c" in opts
  1189. mTitle = "-t" in opts
  1190. mUser = "-u" in opts
  1191. mPw = "-p" in opts
  1192. mBulk = "-b" in opts
  1193. mAttrData = "-a" in opts
  1194. mAttrName = "-A" in opts
  1195. regexp = "-r" in opts
  1196. if not any( (mTitle, mUser, mPw, mBulk, mAttrData) ):
  1197. mTitle, mUser, mPw, mBulk, mAttrData = (True,) * 5
  1198. if opts.nrParams < 1 or opts.nrParams > 2:
  1199. self._err("find", "Invalid parameters.")
  1200. inCategory = opts.getParam(0) if opts.nrParams > 1 else None
  1201. pattern = opts.getParam(1) if opts.nrParams > 1 else opts.getParam(0)
  1202. if inCategory and mCategory:
  1203. self._err("find", "-c and [IN_CATEGORY] cannot be used at the same time.")
  1204. entries = self.__db.findEntries(pattern=pattern,
  1205. useRegexp=regexp,
  1206. inCategory=inCategory,
  1207. matchCategory=mCategory,
  1208. matchTitle=mTitle,
  1209. matchUser=mUser,
  1210. matchPw=mPw,
  1211. matchBulk=mBulk,
  1212. matchAttrName=mAttrName,
  1213. matchAttrData=mAttrData)
  1214. if not entries:
  1215. self._err("find", "'%s' not found" % pattern)
  1216. for entry in entries:
  1217. self._info(None, self.__db.dumpEntry(entry))
  1218. do_f = do_find
  1219. @completion
  1220. def complete_find(self, text, line, begidx, endidx):
  1221. if text == "-":
  1222. return PWManOpts.rawOptTemplates(self.__find_opts)
  1223. if len(text) == 2 and text.startswith("-"):
  1224. return [ text + " " ]
  1225. opts = PWManOpts.parse(line, self.__find_opts, ignoreFirst=True, softFail=True)
  1226. if opts.error:
  1227. return []
  1228. paramIdx = opts.getComplParamIdx(text)
  1229. if paramIdx == 0:
  1230. # category
  1231. return self.__getCategoryCompletions(text)
  1232. return []
  1233. complete_f = complete_find
  1234. def do_totp(self, params):
  1235. """--- Generate a TOTP token ---
  1236. Command: totp [CATEGORY TITLE] OR [TITLE]
  1237. Generates a token using the Time-Based One-Time Password Algorithm.
  1238. Aliases: t
  1239. """
  1240. first, second = PWManOpts.parseParams(params, 0, 2)
  1241. if not first:
  1242. self._err("totp", "First parameter is required.")
  1243. if second:
  1244. category, title = first, second
  1245. else:
  1246. entries = self.__db.findEntries(first, matchTitle=True)
  1247. if not entries:
  1248. self._err("totp", "Entry title not found.")
  1249. return
  1250. elif len(entries) == 1:
  1251. category = entries[0].category
  1252. title = entries[0].title
  1253. else:
  1254. self._err("totp", "Entry title ambiguous.")
  1255. return
  1256. entry = self.__db.getEntry(category, title)
  1257. if not entry:
  1258. self._err("totp", "'%s/%s' not found" % (category, title))
  1259. entryTotp = self.__db.getEntryTotp(entry)
  1260. if not entryTotp:
  1261. self._err("totp", "'%s/%s' does not have "
  1262. "TOTP key information" % (category, title))
  1263. try:
  1264. token = totp(key=entryTotp.key,
  1265. nrDigits=entryTotp.digits,
  1266. hmacHash=entryTotp.hmacHash)
  1267. except OtpError as e:
  1268. self._err("totp", "Failed to generate TOTP: %s" % str(e))
  1269. self._info(None, "%s" % token)
  1270. do_t = do_totp
  1271. complete_totp = __complete_category_title
  1272. complete_t = complete_totp
  1273. def do_edit_totp(self, params):
  1274. """--- Edit TOTP key and parameters ---
  1275. Command: edit_totp category title [KEY] [DIGITS] [HASH]
  1276. Set Time-Based One-Time Password Algorithm key and parameters.
  1277. If KEY is not provided, the TOTP parameters for this entry are deleted.
  1278. DIGITS default to 6, if not provided.
  1279. HASH defaults to SHA1, if not provided.
  1280. Aliases: et
  1281. """
  1282. category, title, key, digits, _hash = PWManOpts.parseParams(params, 0, 5)
  1283. if not category:
  1284. self._err("edit_totp", "Category parameter is required.")
  1285. if not title:
  1286. self._err("edit_totp", "Title parameter is required.")
  1287. entry = self.__db.getEntry(category, title)
  1288. if not entry:
  1289. self._err("edit_totp", "'%s/%s' not found" % (category, title))
  1290. entryTotp = self.__db.getEntryTotp(entry)
  1291. if not entryTotp:
  1292. entryTotp = PWManEntryTOTP(key=None, entry=entry)
  1293. entryTotp.key = key
  1294. if digits:
  1295. try:
  1296. entryTotp.digits = int(digits)
  1297. except ValueError:
  1298. self._err("edit_totp", "Invalid digits parameter.")
  1299. if _hash:
  1300. entryTotp.hmacHash = _hash
  1301. try:
  1302. # Check parameters.
  1303. totp(key=entryTotp.key,
  1304. nrDigits=entryTotp.digits,
  1305. hmacHash=entryTotp.hmacHash)
  1306. except OtpError as e:
  1307. self._err("edit_totp", "TOTP error: %s" % str(e))
  1308. self.__db.setEntryTotp(entryTotp)
  1309. do_et = do_edit_totp
  1310. @completion
  1311. def complete_edit_totp(self, text, line, begidx, endidx):
  1312. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  1313. if paramIdx in (0, 1):
  1314. return self.__complete_category_title(text, line, begidx, endidx)
  1315. category, title = PWManOpts.parseComplParams(line, 0, 2)
  1316. if category and title:
  1317. entry = self.__db.getEntry(category, title)
  1318. if entry:
  1319. entryTotp = self.__db.getEntryTotp(entry)
  1320. if entryTotp:
  1321. if paramIdx == 2: # key
  1322. return [ escapeCmd(entryTotp.key) + " " ]
  1323. elif paramIdx == 3: # digits
  1324. return [ escapeCmd(str(entryTotp.digits)) + " " ]
  1325. elif paramIdx == 4: # hash
  1326. return [ escapeCmd(entryTotp.hmacHash) + " " ]
  1327. return []
  1328. complete_et = complete_edit_totp
  1329. def do_edit_attr(self, params):
  1330. """--- Edit an entry attribute ---
  1331. Command: edit_attr category title NAME [DATA]
  1332. Edit or delete an entry attribute.
  1333. Aliases: ea
  1334. """
  1335. category, title, name, data = PWManOpts.parseParams(params, 0, 4)
  1336. if not category:
  1337. self._err("edit_attr", "Category parameter is required.")
  1338. if not title:
  1339. self._err("edit_attr", "Title parameter is required.")
  1340. entry = self.__db.getEntry(category, title)
  1341. if not entry:
  1342. self._err("edit_attr", "'%s/%s' not found" % (category, title))
  1343. entryAttr = self.__db.getEntryAttr(entry, name)
  1344. if not entryAttr:
  1345. entryAttr = PWManEntryAttr(name=name, entry=entry)
  1346. entryAttr.data = data
  1347. self.__db.setEntryAttr(entryAttr)
  1348. do_ea = do_edit_attr
  1349. @completion
  1350. def complete_edit_attr(self, text, line, begidx, endidx):
  1351. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  1352. if paramIdx in (0, 1):
  1353. return self.__complete_category_title(text, line, begidx, endidx)
  1354. category, title, name = PWManOpts.parseComplParams(line, 0, 3)
  1355. return self.__getEntryAttrCompletions(category, title, name,
  1356. doName=(paramIdx == 2),
  1357. doData=(paramIdx == 3),
  1358. text=text)
  1359. complete_ea = complete_edit_attr
  1360. __diff_opts = ("-u", "-c", "-n")
  1361. def do_diff(self, params):
  1362. """--- Diff the current database to another database ---
  1363. Command: diff [OPTS] [DATABASE_FILE]
  1364. If no DATABASE_FILE is provided: Diffs the latest changes in the
  1365. currently open database to the committed changes in the current database.
  1366. This can be used to review changes before commit.
  1367. If DATABASE_FILE is provided: Diffs the latest changes in the
  1368. currently opened database to the contents of DATABASE_FILE.
  1369. OPTS may be one of:
  1370. -u Generate a unified diff (default if no OPT is given).
  1371. -c Generate a context diff
  1372. -n Generate an ndiff
  1373. Aliases: None
  1374. """
  1375. opts = PWManOpts.parse(params, self.__diff_opts)
  1376. if opts.nrParams > 1:
  1377. self._err("diff", "Too many arguments.")
  1378. optUnified = "-u" in opts
  1379. optContext = "-c" in opts
  1380. optNdiff = "-n" in opts
  1381. numFmtOpts = int(optUnified) + int(optContext) + int(optNdiff)
  1382. if not 0 <= numFmtOpts <= 1:
  1383. self._err("diff", "Multiple format OPTions. "
  1384. "Only one is allowed.")
  1385. if numFmtOpts == 0:
  1386. optUnified = True
  1387. dbFile = opts.getParam(0)
  1388. try:
  1389. if dbFile:
  1390. path = pathlib.Path(dbFile)
  1391. if not path.exists():
  1392. self._err("diff", "'%s' does not exist." % path)
  1393. passphrase = readPassphrase(
  1394. "Master passphrase of '%s'" % path,
  1395. verify=False)
  1396. if passphrase is None:
  1397. self._err("diff", "Could not get passphrase.")
  1398. oldDb = PWManDatabase(filename=path,
  1399. passphrase=passphrase,
  1400. readOnly=True)
  1401. else:
  1402. oldDb = self.__db.getOnDiskDb()
  1403. diff = PWManDatabaseDiff(db=self.__db, oldDb=oldDb)
  1404. if optUnified:
  1405. diffText = diff.getUnifiedDiff()
  1406. elif optContext:
  1407. diffText = diff.getContextDiff()
  1408. elif optNdiff:
  1409. diffText = diff.getNdiffDiff()
  1410. else:
  1411. assert(0)
  1412. self._info(None, diffText)
  1413. except PWManError as e:
  1414. self._err("diff", "Failed: %s" % str(e))
  1415. @completion
  1416. def complete_diff(self, text, line, begidx, endidx):
  1417. if text == "-":
  1418. return PWManOpts.rawOptTemplates(self.__diff_opts)
  1419. if len(text) == 2 and text.startswith("-"):
  1420. return [ text + " " ]
  1421. opts = PWManOpts.parse(line, self.__diff_opts, ignoreFirst=True, softFail=True)
  1422. if opts.error:
  1423. return []
  1424. paramIdx = opts.getComplParamIdx(text)
  1425. if paramIdx == 0:
  1426. # database file path
  1427. return self.__getPathCompletions(text)
  1428. return []
  1429. def __mayQuit(self):
  1430. if self.__db.isDirty():
  1431. self._warn(None,
  1432. "Warning: Uncommitted changes. Operation not performed.\n"
  1433. "Use command 'commit' to write the changes to the database.\n"
  1434. "Use command 'quit!' to quit without saving.")
  1435. return False
  1436. return True
  1437. def flunkDirty(self):
  1438. self.__db.flunkDirty()
  1439. def interactive(self):
  1440. self.__isInteractive = True
  1441. try:
  1442. while True:
  1443. try:
  1444. self.cmdloop()
  1445. break
  1446. except self.Quit as e:
  1447. if self.__mayQuit():
  1448. self.do_cls("")
  1449. break
  1450. except EscapeError as e:
  1451. self._warn(None, str(e))
  1452. except self.CommandError as e:
  1453. print(str(e), file=sys.stderr)
  1454. except (KeyboardInterrupt, EOFError) as e:
  1455. print("")
  1456. except CSQLError as e:
  1457. self._warn(None, "SQL error: %s" % str(e))
  1458. finally:
  1459. self.__isInteractive = False
  1460. def runOneCommand(self, command):
  1461. self.__isInteractive = False
  1462. try:
  1463. self.onecmd(command)
  1464. except self.Quit as e:
  1465. raise PWManError("Quit command executed in non-interactive mode.")
  1466. except (EscapeError, self.CommandError) as e:
  1467. raise PWManError(str(e))
  1468. except (KeyboardInterrupt, EOFError) as e:
  1469. raise PWManError("Interrupted.")
  1470. except CSQLError as e:
  1471. raise PWManError("SQL error: %s" % str(e))