ui.py 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715
  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, deepcopy
  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.__dbs[self.__selDbName]
  326. def __updatePrompt(self):
  327. if len(self.__dbs) > 1:
  328. dbName = self.__selDbName
  329. lim = 20
  330. if len(dbName) > lim - 3:
  331. dbName = dbName[:lim-3] + "..."
  332. else:
  333. dbName = ""
  334. dirty = any(db.isDirty() for db in self.__dbs.values())
  335. self.prompt = "%spwman%s%s$ " % (
  336. "*" if dirty else "",
  337. "/" if dbName else "",
  338. dbName
  339. )
  340. @classmethod
  341. def _err(cls, source, message):
  342. source = (" " + source + ":") if source else ""
  343. raise cls.CommandError("***%s %s" % (source, message))
  344. @classmethod
  345. def _warn(cls, source, message):
  346. source = (" " + source + ":") if source else ""
  347. print("***%s %s" % (source, message))
  348. @classmethod
  349. def _info(cls, source, message):
  350. source = ("+++ " + source + ": ") if source else ""
  351. print("%s%s" % (source, message))
  352. def precmd(self, line):
  353. self._timeout.poke()
  354. first = PWManOpts.parseParam(line, 0, unescape=False)
  355. if first.endswith('?'):
  356. return "help %s" % first[:-1]
  357. return line
  358. def postcmd(self, stop, line):
  359. self.__updatePrompt()
  360. self._timeout.poke()
  361. def default(self, line):
  362. extra = "\nType 'help' for more help." if self.__isInteractive else ""
  363. self._err(None, "Unknown command: %s%s" % (line, extra))
  364. def emptyline(self):
  365. self._timeout.poke()
  366. # Don't repeat the last command
  367. @completion
  368. def __complete_category_title(self, text, line, begidx, endidx):
  369. # Generic [category] [title] completion
  370. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  371. if paramIdx == 0:
  372. # Category completion
  373. return self.__getCategoryCompletions(text)
  374. elif paramIdx == 1:
  375. # Entry title completion
  376. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  377. text)
  378. return []
  379. @completion
  380. def __complete_category_title_item(self, text, line, begidx, endidx):
  381. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  382. if paramIdx in (0, 1):
  383. return self.__complete_category_title(text, line, begidx, endidx)
  384. category, title, item = PWManOpts.parseComplParams(line, 0, 3)
  385. cmpl = []
  386. if paramIdx == 2:
  387. cmpl.extend(escapeCmd(n) + " "
  388. for n in ("user", "password", "bulk", "totpkey")
  389. if n.startswith(item))
  390. cmpl.extend(self.__getEntryAttrCompletions(category, title, item,
  391. doName=(paramIdx == 2),
  392. doData=False,
  393. text=text))
  394. return cmpl
  395. def __getCategoryCompletions(self, text, db=None):
  396. db = db or self.__db
  397. return [ escapeCmd(n) + " "
  398. for n in db.getCategoryNames()
  399. if n.startswith(text) ]
  400. def __getEntryTitleCompletions(self, category, text, db=None):
  401. db = db or self.__db
  402. return [ escapeCmd(t) + " "
  403. for t in db.getEntryTitles(category)
  404. if t.startswith(text) ]
  405. def __getEntryAttrCompletions(self, category, title, name, doName, doData, text, db=None):
  406. db = db or self.__db
  407. if category and title:
  408. entry = db.getEntry(category, title)
  409. if entry:
  410. if doName: # complete name
  411. entryAttrs = db.getEntryAttrs(entry)
  412. if entryAttrs:
  413. return [ escapeCmd(entryAttr.name) + " "
  414. for entryAttr in entryAttrs
  415. if entryAttr.name.startswith(name) ]
  416. elif doData: # complete data
  417. entryAttr = db.getEntryAttr(entry, name)
  418. if entryAttr:
  419. return [ escapeCmd(entryAttr.data) + " " ]
  420. return []
  421. def __getDatabaseCompletions(self, text):
  422. return [ escapeCmd(n) + " "
  423. for n in self.__dbs.keys()
  424. if n.startswith(text) ]
  425. def __getPathCompletions(self, text):
  426. """Return an escaped file system path completion.
  427. 'text' is the unescaped partial path string.
  428. """
  429. try:
  430. path = pathlib.Path(text)
  431. trailingChar = text[-1] if text else ""
  432. sep = os.path.sep
  433. base = path.parts[-1] if path.parts else ""
  434. dirPath = pathlib.Path(*path.parts[:-1])
  435. dirPathListing = [ f for f in dirPath.iterdir()
  436. if f.parts[-1].startswith(base) ]
  437. if (path.is_dir() and
  438. (trailingChar in (sep, "/", "\\") or
  439. len(dirPathListing) <= 1)):
  440. # path is an unambiguous directory.
  441. # Show its contents.
  442. useListing = path.iterdir()
  443. else:
  444. # path is a file or an ambiguous directory.
  445. # Show the alternatives.
  446. useListing = dirPathListing
  447. return [ escapeCmd(str(f)) + (escapeCmd(sep) if f.is_dir() else " ")
  448. for f in useListing ]
  449. except OSError:
  450. pass
  451. return []
  452. cmdHelpShow = (
  453. ("list", ("ls", "cat"), "List/print entry contents"),
  454. ("find", ("f",), "Search the database for patterns"),
  455. ("totp", ("t",), "Generate TOTP token"),
  456. ("diff", (), "Show the database differences"),
  457. )
  458. cmdHelpEdit = (
  459. ("new", ("n", "add"), "Create new entry"),
  460. ("edit_user", ("eu",), "Edit the 'user' field of an entry"),
  461. ("edit_pw", ("ep",), "Edit the 'password' field of an entry"),
  462. ("edit_bulk", ("eb",), "Edit the 'bulk' field of an entry"),
  463. ("edit_totp", ("et",), "Edit the TOTP key and parameters"),
  464. ("edit_attr", ("ea",), "Edit an entry attribute"),
  465. ("move", ("mv", "rename"), "Move/rename an existing entry"),
  466. ("copy", ("cp",), "Copy an existing entry or category"),
  467. ("remove", ("rm", "del"), "Remove an existing entry"),
  468. )
  469. cmdHelpDatabase = (
  470. ("database", ("db",), "Open or select another database"),
  471. ("commit", ("c", "w"), "Commit/write selected db to disk"),
  472. ("drop", (), "Drop uncommitted changes in selected db"),
  473. ("close", (), "Close a database"),
  474. ("dbdump", (), "Dump the selected database"),
  475. ("dbimport", (), "Import a database dump file"),
  476. ("masterp", (), "Change the master passphrase"),
  477. )
  478. cmdHelpMisc = (
  479. ("help", ("h",), "Show help about commands"),
  480. ("quit", ("q", "exit", "^D"), "Quit pwman"),
  481. ("cls", (), "Clear screen"),
  482. )
  483. def do_help(self, params):
  484. """--- Shows help text about a command ---
  485. Command: help [COMMAND]
  486. If COMMAND is not given: Show a command summary.
  487. If COMMAND is given: Show detailed help about that command.
  488. Aliases: h
  489. """
  490. if params:
  491. Cmd.do_help(self, params)
  492. return
  493. def printCmdHelp(cmdHelp):
  494. for cmd, aliases, desc in cmdHelp:
  495. spc = " " * (10 - len(cmd))
  496. msg = " %s%s%s" % (cmd, spc, desc)
  497. if aliases:
  498. msg += " " * (52 - len(msg))
  499. msg += " Alias%s: %s" %\
  500. ("es" if len(aliases) > 1 else "",
  501. ", ".join(aliases))
  502. self._info(None, msg)
  503. self._info(None, "\nSearching/listing commands:")
  504. printCmdHelp(self.cmdHelpShow)
  505. self._info(None, "\nEditing commands:")
  506. printCmdHelp(self.cmdHelpEdit)
  507. self._info(None, "\nDatabase commands:")
  508. printCmdHelp(self.cmdHelpDatabase)
  509. self._info(None, "\nMisc commands:")
  510. printCmdHelp(self.cmdHelpMisc)
  511. self._info(None, "\nType 'command?' or 'help command' for more help on a command.")
  512. do_h = do_help
  513. def do_quit(self, params):
  514. """--- Exit pwman ---
  515. Command: quit [!]
  516. Use the exclamation mark to force quit and discard changes.
  517. Aliases: q exit ^D
  518. """
  519. if params == "!":
  520. for db in self.__dbs.values():
  521. db.flunkDirty()
  522. raise self.Quit()
  523. do_q = do_quit
  524. do_exit = do_quit
  525. do_EOF = do_quit
  526. def do_cls(self, params):
  527. """--- Clear console screen ---
  528. Command: cls
  529. Clear the console screen.
  530. Note that this does not clear a possibly existing
  531. 'screen' session buffer or other advanced console buffers.
  532. Aliases: None
  533. """
  534. clearScreen()
  535. __commit_opts = ("-a",)
  536. def do_commit(self, params):
  537. """--- Write changes to the database file(s) ---
  538. Command: commit
  539. Options:
  540. -a Commit all open databases.
  541. Aliases: c w
  542. """
  543. opts = PWManOpts.parse(params, self.__commit_opts)
  544. dbs = self.__dbs.values() if "-a" in opts else [ self.__db ]
  545. try:
  546. for db in dbs:
  547. db.commit()
  548. except PWManError as e:
  549. self._err("commit", str(e))
  550. do_c = do_commit
  551. do_w = do_commit
  552. @completion
  553. def complete_commit(self, text, line, begidx, endidx):
  554. if text == "-":
  555. return PWManOpts.rawOptTemplates(self.__commit_opts)
  556. return []
  557. complete_c = complete_commit
  558. complete_w = complete_commit
  559. def do_masterp(self, params):
  560. """--- Change the master passphrase ---
  561. Command: masterp
  562. Aliases: None
  563. """
  564. p = readPassphrase("Current master passphrase")
  565. if p != self.__db.getPassphrase():
  566. time.sleep(1)
  567. self._warn(None, "Passphrase mismatch! ")
  568. return
  569. p = readPassphrase("Master passphrase", verify=True)
  570. if p is None:
  571. self._info(None, "Passphrase not changed.")
  572. return
  573. if p != self.__db.getPassphrase():
  574. self.__db.setPassphrase(p)
  575. def do_list(self, params):
  576. """--- Print a listing ---
  577. Command: list [category] [title] [item]
  578. If a category is given as parameter, list the
  579. contents of the category. If category and entry
  580. are given, list the contents of the entry.
  581. If item is given, then only list one specific content item.
  582. Item may be one of: user, password, bulk, totpkey or any attribute name.
  583. Aliases: ls cat
  584. """
  585. category, title, item = PWManOpts.parseParams(params, 0, 3)
  586. if not category and not title and not item:
  587. self._info(None, "Categories:")
  588. self._info(None, "\t" + "\n\t".join(self.__db.getCategoryNames()))
  589. elif category and not title and not item:
  590. self._info(None, "Entries in category '%s':" % category)
  591. self._info(None, "\t" + "\n\t".join(self.__db.getEntryTitles(category)))
  592. elif category and title and not item:
  593. entry = self.__db.getEntry(category, title)
  594. if entry:
  595. self._info(None, self.__db.dumpEntry(entry))
  596. else:
  597. self._err("list", "'%s/%s' not found" % (category, title))
  598. elif category and title and item:
  599. entry = self.__db.getEntry(category, title)
  600. if entry:
  601. if item == "user":
  602. if not entry.user:
  603. self._err("list", "'%s/%s' has no 'user' field." % (
  604. category, title))
  605. self._info(None, entry.user)
  606. elif item == "password":
  607. if not entry.pw:
  608. self._err("list", "'%s/%s' has no 'password' field." % (
  609. category, title))
  610. self._info(None, entry.pw)
  611. elif item == "bulk":
  612. bulk = self.__db.getEntryBulk(entry)
  613. if not bulk:
  614. self._err("list", "'%s/%s' has no 'bulk' field." % (
  615. category, title))
  616. self._info(None, bulk.data)
  617. elif item == "totpkey":
  618. entryTotp = self.__db.getEntryTotp(entry)
  619. if not entryTotp:
  620. self._err("list", "'%s/%s' has no 'TOTP key'." % (
  621. category, title))
  622. self._info(None, "TOTP key: %s (base32 encoding)" % entryTotp.key)
  623. self._info(None, "TOTP digits: %d" % entryTotp.digits)
  624. self._info(None, "TOTP hash: %s" % entryTotp.hmacHash)
  625. else: # attribute
  626. attr = self.__db.getEntryAttr(entry, item)
  627. if not attr:
  628. self._err("list", "'%s/%s' has no attribute '%s'." % (
  629. category, title, item))
  630. self._info(None, attr.data)
  631. else:
  632. self._err("list", "'%s/%s' not found" % (category, title))
  633. else:
  634. self._err("list", "Invalid parameter")
  635. do_ls = do_list
  636. do_cat = do_list
  637. complete_list = __complete_category_title_item
  638. complete_ls = complete_list
  639. complete_cat = complete_list
  640. def do_new(self, params):
  641. """--- Create a new entry ---
  642. Command: new [category] [title] [user] [password]
  643. Create a new database entry. If no parameters are given,
  644. they are asked for interactively.
  645. Aliases: n add
  646. """
  647. if params:
  648. category, title, user, pw = PWManOpts.parseParams(params, 0, 4)
  649. else:
  650. self._info("new", "Create new entry:")
  651. category = input("\tCategory: ")
  652. title = input("\tEntry title: ")
  653. user = input("\tUsername: ")
  654. pw = input("\tPassword: ")
  655. if not category or not title:
  656. self._err("new", "Invalid parameters. "
  657. "Need to supply category and title.")
  658. entry = PWManEntry(category=category, title=title, user=user, pw=pw)
  659. try:
  660. self.__db.addEntry(entry)
  661. except (PWManError) as e:
  662. self._err("new", str(e))
  663. do_n = do_new
  664. do_add = do_new
  665. complete_new = __complete_category_title
  666. complete_n = complete_new
  667. complete_add = complete_new
  668. def __do_edit_entry(self, params, commandName,
  669. entry2data, data2entry):
  670. category, title = PWManOpts.parseParams(params, 0, 2)
  671. if not category or not title:
  672. self._err(commandName, "Invalid parameters. "
  673. "Need to supply category and title.")
  674. newData = PWManOpts.skipParams(params, 2).strip()
  675. try:
  676. self.__db.editEntry(data2entry(category, title, newData))
  677. except (PWManError) as e:
  678. self._err(commandName, str(e))
  679. def do_edit_user(self, params):
  680. """--- Edit the 'user' field of an existing entry ---
  681. Command: edit_user category title NEWDATA...
  682. Change the 'user' field of an existing database entry.
  683. NEWDATA is the new data to write into the 'user' field.
  684. The NEWDATA must _not_ be escaped (however, category and
  685. title must be escaped).
  686. Aliases: eu
  687. """
  688. self.__do_edit_entry(params, "edit_user",
  689. lambda entry: entry.user,
  690. lambda cat, tit, data: PWManEntry(cat, tit, user=data))
  691. do_eu = do_edit_user
  692. @completion
  693. def complete_edit_user(self, text, line, begidx, endidx):
  694. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  695. if paramIdx == 0:
  696. # Category completion
  697. return self.__getCategoryCompletions(text)
  698. elif paramIdx == 1:
  699. # Entry title completion
  700. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  701. text)
  702. elif paramIdx == 2:
  703. # User data
  704. entry = self.__db.getEntry(PWManOpts.parseComplParam(line, 0),
  705. PWManOpts.parseComplParam(line, 1))
  706. return [ escapeCmd(entry.user) ]
  707. return []
  708. complete_eu = complete_edit_user
  709. def do_edit_pw(self, params):
  710. """--- Edit the 'password' field of an existing entry ---
  711. Command: edit_pw category title NEWDATA...
  712. Change the 'password' field of an existing database entry.
  713. NEWDATA is the new data to write into the 'password' field.
  714. The NEWDATA must _not_ be escaped (however, category and
  715. title must be escaped).
  716. Aliases: ep
  717. """
  718. self.__do_edit_entry(params, "edit_pw",
  719. lambda entry: entry.pw,
  720. lambda cat, tit, data: PWManEntry(cat, tit, pw=data))
  721. do_ep = do_edit_pw
  722. @completion
  723. def complete_edit_pw(self, text, line, begidx, endidx):
  724. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  725. if paramIdx == 0:
  726. # Category completion
  727. return self.__getCategoryCompletions(text)
  728. elif paramIdx == 1:
  729. # Entry title completion
  730. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  731. text)
  732. elif paramIdx == 2:
  733. # Password data
  734. entry = self.__db.getEntry(PWManOpts.parseComplParam(line, 0),
  735. PWManOpts.parseComplParam(line, 1))
  736. return [ escapeCmd(entry.pw) ]
  737. return []
  738. complete_ep = complete_edit_pw
  739. def do_edit_bulk(self, params):
  740. """--- Edit the 'bulk' field of an existing entry ---
  741. Command: edit_bulk category title NEWDATA...
  742. Change the 'bulk' field of an existing database entry.
  743. NEWDATA is the new data to write into the 'bulk' field.
  744. The NEWDATA must _not_ be escaped (however, category and
  745. title must be escaped).
  746. Aliases: eb
  747. """
  748. category, title = PWManOpts.parseParams(params, 0, 2)
  749. data = PWManOpts.skipParams(params, 2).strip()
  750. if not category:
  751. self._err("edit_bulk", "Category parameter is required.")
  752. if not title:
  753. self._err("edit_bulk", "Title parameter is required.")
  754. entry = self.__db.getEntry(category, title)
  755. if not entry:
  756. self._err("edit_bulk", "'%s/%s' not found" % (category, title))
  757. entryBulk = self.__db.getEntryBulk(entry)
  758. if not entryBulk:
  759. entryBulk = PWManEntryBulk(entry=entry)
  760. entryBulk.data = data
  761. self.__db.setEntryBulk(entryBulk)
  762. do_eb = do_edit_bulk
  763. @completion
  764. def complete_edit_bulk(self, text, line, begidx, endidx):
  765. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  766. if paramIdx == 0:
  767. # Category completion
  768. return self.__getCategoryCompletions(text)
  769. elif paramIdx == 1:
  770. # Entry title completion
  771. return self.__getEntryTitleCompletions(PWManOpts.parseComplParam(line, 0),
  772. text)
  773. elif paramIdx == 2:
  774. # Bulk data
  775. entry = self.__db.getEntry(PWManOpts.parseComplParam(line, 0),
  776. PWManOpts.parseComplParam(line, 1))
  777. if entry:
  778. entryBulk = self.__db.getEntryBulk(entry)
  779. if entryBulk:
  780. return [ escapeCmd(entryBulk.data) ]
  781. return []
  782. complete_eb = complete_edit_bulk
  783. def do_remove(self, params):
  784. """--- Remove an existing entry ---
  785. Command: remove category [title]
  786. Remove an existing database entry.
  787. Aliases: rm del
  788. """
  789. category, title = PWManOpts.parseParams(params, 0, 2)
  790. if not category:
  791. self._err("remove", "Category parameter is required.")
  792. if not title:
  793. # Remove whole category
  794. for title in self.__db.getEntryTitles(category):
  795. p = "%s %s" % (escapeCmd(category),
  796. escapeCmd(title))
  797. self._info("remove", "running command: remove %s" % p)
  798. self.do_remove(p)
  799. return
  800. try:
  801. self.__db.delEntry(PWManEntry(category, title))
  802. except (PWManError) as e:
  803. self._err("remove", str(e))
  804. do_rm = do_remove
  805. do_del = do_remove
  806. complete_remove = __complete_category_title
  807. complete_rm = complete_remove
  808. complete_del = complete_remove
  809. __move_opts = ("-s:", "-d:")
  810. def do_move(self, params):
  811. """--- Move/rename an existing entry or a category ---
  812. Move/rename an existing entry:
  813. Command: move CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  814. (NEW_TITLE defaults to TITLE)
  815. Move all entries from one category into another category.
  816. Command: move FROM_CATEGORY TO_CATEGORY
  817. Move an entry from one database to another:
  818. Command: move -s main -d other CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  819. (NEW_TITLE defaults to TITLE)
  820. Move all entries from a category from one database into another database:
  821. Command: move -s main -d other FROM_CATEGORY [TO_CATEGORY]
  822. (TO_CATEGORY defaults to FROM_CATEGORY)
  823. Options:
  824. -s SOURCE_DATABASE_NAME
  825. -d DESTINATION_DATABASE_NAME
  826. Databases default to the currently selected database.
  827. The named databases must be open. See 'database' command.
  828. Aliases: mv rename
  829. """
  830. opts = PWManOpts.parse(params, self.__move_opts)
  831. sourceDbName = opts.getOpt("-s", default=self.__selDbName)
  832. sourceDb = self.__dbs.get(sourceDbName, None)
  833. if sourceDb is None:
  834. self._err("move", "Source database '%s' does not exist" % sourceDbName)
  835. destDbName = opts.getOpt("-d", default=self.__selDbName)
  836. destDb = self.__dbs.get(destDbName, None)
  837. if destDb is None:
  838. self._err("move", "Destination database '%s' does not exist" % destDbName)
  839. if opts.nrParams in (3, 4):
  840. # Entry rename/move
  841. fromCategory, fromTitle, toCategory, toTitle =\
  842. (opts.getParam(0), opts.getParam(1),
  843. opts.getParam(2), opts.getParam(3))
  844. toTitle = toTitle or fromTitle
  845. if sourceDb is destDb and fromCategory == toCategory and fromTitle == toTitle:
  846. self._info("move", "Nothing changed. Not moving anything.")
  847. return
  848. entry = sourceDb.getEntry(fromCategory, fromTitle)
  849. oldEntry = deepcopy(entry)
  850. if not entry:
  851. self._err("move", "Source entry does not exist.")
  852. try:
  853. if sourceDb is destDb:
  854. # Move in one DB.
  855. sourceDb.moveEntry(entry, toCategory, toTitle)
  856. else:
  857. # Move between different DBs.
  858. entry.entryId = None
  859. entry.category = toCategory
  860. entry.title = toTitle
  861. destDb.addEntry(entry)
  862. sourceDb.delEntry(oldEntry)
  863. except (PWManError) as e:
  864. self._err("move", str(e))
  865. elif (sourceDb is destDb and opts.nrParams == 2) or\
  866. (sourceDb is not destDb and opts.nrParams in (1, 2)):
  867. # Whole category move.
  868. fromCategory, toCategory = opts.getParam(0), opts.getParam(1)
  869. toCategory = toCategory or fromCategory
  870. try:
  871. if sourceDb is destDb:
  872. # Category move in one DB.
  873. sourceDb.moveEntries(fromCategory, toCategory)
  874. else:
  875. # Category move between DBs.
  876. for fromTitle in sourceDb.getEntryTitles(fromCategory):
  877. self._info("move",
  878. "running command: move -s %s -d %s %s %s %s %s" % (
  879. escapeCmd(sourceDbName), escapeCmd(destDbName),
  880. escapeCmd(fromCategory), escapeCmd(fromTitle),
  881. escapeCmd(toCategory), escapeCmd(fromTitle)))
  882. entry = sourceDb.getEntry(fromCategory, fromTitle)
  883. oldEntry = deepcopy(entry)
  884. entry.entryId = None
  885. entry.category = toCategory
  886. destDb.addEntry(entry)
  887. sourceDb.delEntry(oldEntry)
  888. except (PWManError) as e:
  889. self._err("move", str(e))
  890. else:
  891. self._err("move", "Invalid parameters.")
  892. do_mv = do_move
  893. do_rename = do_move
  894. @completion
  895. def complete_move(self, text, line, begidx, endidx):
  896. if text == "-":
  897. return PWManOpts.rawOptTemplates(self.__move_opts)
  898. if len(text) == 2 and text.startswith("-"):
  899. return [ text + " " ]
  900. dbOpts = ("-s", "-d")
  901. opts = PWManOpts.parse(line, self.__move_opts, ignoreFirst=True, softFail=True)
  902. if opts.error:
  903. opt, error = opts.error
  904. if error == "no_arg" and opt in dbOpts:
  905. return self.__getDatabaseCompletions(text)
  906. return []
  907. optName, value = opts.atCmdIndex(PWManOpts.calcParamIndex(line, endidx))
  908. if optName in dbOpts:
  909. return self.__getDatabaseCompletions(text)
  910. sourceDbName = opts.getOpt("-s", default=self.__selDbName)
  911. sourceDb = self.__dbs.get(sourceDbName, None)
  912. if sourceDb is None:
  913. return []
  914. destDbName = opts.getOpt("-d", default=self.__selDbName)
  915. destDb = self.__dbs.get(destDbName, None)
  916. if destDb is None:
  917. return []
  918. paramIdx = opts.getComplParamIdx(text)
  919. if paramIdx == 0:
  920. # Category completion
  921. return self.__getCategoryCompletions(text, db=sourceDb)
  922. elif paramIdx == 1:
  923. # Entry title completion
  924. category = opts.getParam(0)
  925. if category:
  926. compl = self.__getEntryTitleCompletions(category, text, db=sourceDb)
  927. if compl:
  928. return compl
  929. # Category completion
  930. return self.__getCategoryCompletions(text, db=destDb)
  931. elif paramIdx == 2:
  932. # Category completion
  933. return self.__getCategoryCompletions(text, db=destDb)
  934. elif paramIdx == 3:
  935. # Entry title completion
  936. category = opts.getParam(2)
  937. if category:
  938. return self.__getEntryTitleCompletions(category, text, db=destDb)
  939. return []
  940. complete_mv = complete_move
  941. complete_rename = complete_move
  942. __copy_opts = ("-s:", "-d:")
  943. def do_copy(self, params):
  944. """--- Copy an entry or a category ---
  945. Copy an existing entry:
  946. Command: copy CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  947. (NEW_TITLE defaults to TITLE)
  948. Copy all entries from a category into another category:
  949. Command: copy FROM_CATEGORY TO_CATEGORY
  950. Copy an entry from one database to another:
  951. Command: copy -s main -d other CATEGORY TITLE TO_CATEGORY [NEW_TITLE]
  952. (NEW_TITLE defaults to TITLE)
  953. Copy all entries from a category from one database into another database:
  954. Command: copy -s main -d other FROM_CATEGORY [TO_CATEGORY]
  955. (TO_CATEGORY defaults to FROM_CATEGORY)
  956. Options:
  957. -s SOURCE_DATABASE_NAME
  958. -d DESTINATION_DATABASE_NAME
  959. Databases default to the currently selected database.
  960. The named databases must be open. See 'database' command.
  961. Aliases: cp
  962. """
  963. opts = PWManOpts.parse(params, self.__copy_opts)
  964. sourceDbName = opts.getOpt("-s", default=self.__selDbName)
  965. sourceDb = self.__dbs.get(sourceDbName, None)
  966. if sourceDb is None:
  967. self._err("copy", "Source database '%s' does not exist" % sourceDbName)
  968. destDbName = opts.getOpt("-d", default=self.__selDbName)
  969. destDb = self.__dbs.get(destDbName, None)
  970. if destDb is None:
  971. self._err("copy", "Destination database '%s' does not exist" % destDbName)
  972. if opts.nrParams in (3, 4):
  973. # Entry copy
  974. fromCategory, fromTitle, toCategory, toTitle =\
  975. (opts.getParam(0), opts.getParam(1),
  976. opts.getParam(2), opts.getParam(3))
  977. toTitle = toTitle or fromTitle
  978. if sourceDb is destDb and fromCategory == toCategory and fromTitle == toTitle:
  979. self._info("copy", "Copy source and target are identical.")
  980. return
  981. entry = sourceDb.getEntry(fromCategory, fromTitle)
  982. if not entry:
  983. self._err("copy", "Source entry does not exist.")
  984. try:
  985. entry.entryId = None
  986. entry.category = toCategory
  987. entry.title = toTitle
  988. destDb.addEntry(entry)
  989. except (PWManError) as e:
  990. self._err("copy", str(e))
  991. elif (sourceDb is destDb and opts.nrParams == 2) or\
  992. (sourceDb is not destDb and opts.nrParams in (1, 2)):
  993. # Category copy
  994. fromCategory, toCategory = opts.getParam(0), opts.getParam(1)
  995. toCategory = toCategory or fromCategory
  996. try:
  997. for fromTitle in sourceDb.getEntryTitles(fromCategory):
  998. self._info("copy",
  999. "running command: copy -s %s -d %s %s %s %s %s" % (
  1000. escapeCmd(sourceDbName), escapeCmd(destDbName),
  1001. escapeCmd(fromCategory), escapeCmd(fromTitle),
  1002. escapeCmd(toCategory), escapeCmd(fromTitle)))
  1003. entry = sourceDb.getEntry(fromCategory, fromTitle)
  1004. entry.entryId = None
  1005. entry.category = toCategory
  1006. destDb.addEntry(entry)
  1007. except (PWManError) as e:
  1008. self._err("copy", str(e))
  1009. else:
  1010. self._err("copy", "Invalid parameters.")
  1011. do_cp = do_copy
  1012. complete_copy = complete_move # copy and move are equal w.r.t. completion
  1013. complete_cp = complete_copy
  1014. __database_opts = ("-f:",)
  1015. def do_database(self, params):
  1016. """--- Open a database or switch to an already opened database ---
  1017. Command: database [-f FILEPATH] [NAME]
  1018. If neither FILEPATH nor NAME are given, then
  1019. a list of all currently opened databases will be printed.
  1020. The currently selected database will be marked with [@].
  1021. All databases with uncommitted changes will be marked with [*].
  1022. If only NAME is given, then the selected database will
  1023. be switched to the named one. NAME must already be open.
  1024. A new database can be opened with -f FILEPATH.
  1025. NAME is optional in this case.
  1026. The selected database will be switched to the newly opened one.
  1027. Aliases: db
  1028. """
  1029. opts = PWManOpts.parse(params, self.__database_opts)
  1030. path = opts.getOpt("-f")
  1031. name = opts.getParam(0)
  1032. if path:
  1033. if opts.nrParams not in (0, 1):
  1034. self._err("database", "Invalid parameters.")
  1035. # Open a new db.
  1036. path = pathlib.Path(path)
  1037. name = name or path.name
  1038. if name == "main":
  1039. self._err("database",
  1040. "The database name 'main' is reserved. "
  1041. "Please select another name.")
  1042. if name in self.__dbs:
  1043. self._err("database",
  1044. ("The database name '%' is already used. "
  1045. "Please select another name.") % name)
  1046. try:
  1047. passphrase = readPassphrase(
  1048. "Master passphrase of '%s'" % path,
  1049. verify=not path.exists())
  1050. if passphrase is None:
  1051. self._err("database", "Could not get passphrase.")
  1052. db = PWManDatabase(filename=path,
  1053. passphrase=passphrase,
  1054. readOnly=False)
  1055. except PWManError as e:
  1056. self._err("database", str(e))
  1057. self.__dbs[name] = db
  1058. self.__selDbName = name
  1059. elif opts.nrParams == 1:
  1060. # Switch selected db to NAME.
  1061. if name not in self.__dbs:
  1062. self._err("database", "The database '%s' does not exist." % name)
  1063. if name != self.__selDbName:
  1064. self.__selDbName = name
  1065. elif opts.nrParams == 0:
  1066. # Print db list.
  1067. for name, db in self.__dbs.items():
  1068. flags = "@" if db is self.__db else " "
  1069. flags += "*" if db.isDirty() else " "
  1070. path = db.getFilename()
  1071. self._info(None, "[%s] %s: %s" % (
  1072. flags, name, path))
  1073. else:
  1074. self._err("database", "Invalid parameters.")
  1075. do_db = do_database
  1076. @completion
  1077. def complete_database(self, text, line, begidx, endidx):
  1078. if text == "-":
  1079. return PWManOpts.rawOptTemplates(self.__database_opts)
  1080. if len(text) == 2 and text.startswith("-"):
  1081. return [ text + " " ]
  1082. opts = PWManOpts.parse(line, self.__database_opts, ignoreFirst=True, softFail=True)
  1083. if opts.error:
  1084. opt, error = opts.error
  1085. if error == "no_arg" and opt == "-f":
  1086. return self.__getPathCompletions(text)
  1087. return []
  1088. optName, value = opts.atCmdIndex(PWManOpts.calcParamIndex(line, endidx))
  1089. if optName == "-f":
  1090. return self.__getPathCompletions(text)
  1091. paramIdx = opts.getComplParamIdx(text)
  1092. if paramIdx == 0:
  1093. # Database name
  1094. return self.__getDatabaseCompletions(text)
  1095. return []
  1096. complete_db = complete_database
  1097. __dbdump_opts = ("-s", "-h", "-c")
  1098. def do_dbdump(self, params):
  1099. """--- Dump the pwman SQL database ---
  1100. Command: dbdump [OPTS] [FILEPATH]
  1101. If FILEPATH is given, the database is dumped
  1102. unencrypted to the file.
  1103. If FILEPATH is omitted, the database is dumped
  1104. unencrypted to stdout.
  1105. OPTS may be one of:
  1106. -s Dump format SQL. (default)
  1107. -h Dump format human readable text.
  1108. -c Dump format CSV.
  1109. WARNING: The database dump is not encrypted.
  1110. Aliases: None
  1111. """
  1112. opts = PWManOpts.parse(params, self.__dbdump_opts)
  1113. if opts.nrParams > 1:
  1114. self._err("dbdump", "Too many arguments.")
  1115. optFmtSqlDump = "-s" in opts
  1116. optFmtHumanReadable = "-h" in opts
  1117. optFmtCsv = "-c" in opts
  1118. numFmtOpts = int(optFmtSqlDump) + int(optFmtHumanReadable) + int(optFmtCsv)
  1119. if not 0 <= numFmtOpts <= 1:
  1120. self._err("dbdump", "Multiple format OPTions. "
  1121. "Only one is allowed.")
  1122. if numFmtOpts == 0:
  1123. optFmtSqlDump = True
  1124. dumpFile = opts.getParam(0)
  1125. try:
  1126. if optFmtSqlDump:
  1127. dump = self.__db.sqlPlainDump() + b"\n"
  1128. elif optFmtHumanReadable:
  1129. dump = self.__db.dumpEntries(showTotpKey=True)
  1130. dump = dump.encode("UTF-8") + b"\n"
  1131. elif optFmtCsv:
  1132. dump = self.__db.dumpEntriesCsv(showTotpKey=True)
  1133. dump = dump.encode("UTF-8")
  1134. else:
  1135. assert(0)
  1136. if dumpFile:
  1137. with open(dumpFile, "wb") as f:
  1138. f.write(dump)
  1139. else:
  1140. stdout(dump)
  1141. except UnicodeError as e:
  1142. self._err("dbdump", "Unicode error.")
  1143. except IOError as e:
  1144. self._err("dbdump", "Failed to write dump: %s" % e.strerror)
  1145. @completion
  1146. def complete_dbdump(self, text, line, begidx, endidx):
  1147. if text == "-":
  1148. return PWManOpts.rawOptTemplates(self.__dbdump_opts)
  1149. if len(text) == 2 and text.startswith("-"):
  1150. return [ text + " " ]
  1151. opts = PWManOpts.parse(line, self.__dbdump_opts, ignoreFirst=True, softFail=True)
  1152. if opts.error:
  1153. return []
  1154. paramIdx = opts.getComplParamIdx(text)
  1155. if paramIdx == 0:
  1156. # filepath
  1157. return self.__getPathCompletions(text)
  1158. return []
  1159. def do_dbimport(self, params):
  1160. """--- Import an SQL database dump ---
  1161. Command: dbimport FILEPATH
  1162. Import the FILEPATH into the current database.
  1163. The database is cleared before importing the file!
  1164. Aliases: None
  1165. """
  1166. try:
  1167. if not params.strip():
  1168. raise IOError("FILEPATH is empty.")
  1169. with open(params, "rb") as f:
  1170. data = f.read().decode("UTF-8")
  1171. self.__db.importSqlScript(data)
  1172. self._info("dbimport", "success.")
  1173. except (CSQLError, IOError, UnicodeError) as e:
  1174. self._err("dbimport", "Failed to import dump: %s" % str(e))
  1175. @completion
  1176. def complete_dbimport(self, text, line, begidx, endidx):
  1177. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  1178. if paramIdx == 0:
  1179. return self.__getPathCompletions(text)
  1180. return []
  1181. def do_drop(self, params):
  1182. """--- Drop all uncommitted changes ---
  1183. Command: drop
  1184. Aliases: None
  1185. """
  1186. self.__db.dropUncommitted()
  1187. def do_close(self, params):
  1188. """--- Close a database ---
  1189. Command: close [!] [NAME]
  1190. If NAME is not given, then this closes the currently selected database.
  1191. If NAME is given, then this closes the named database.
  1192. If ! is specified, then the uncommitted changes will be dropped.
  1193. If the currently used database is closed, the selected database
  1194. will be switched to 'main'.
  1195. The 'main' database can only be closed last,
  1196. which in turn closes the application.
  1197. Aliases: None
  1198. """
  1199. flunk = params.startswith("!")
  1200. if flunk:
  1201. params = params[1:].strip()
  1202. name = params if params else self.__selDbName
  1203. if name == "main" and len(self.__dbs) > 1:
  1204. self._err("close", "The 'main' database can only be closed last")
  1205. db = self.__dbs.get(name, None)
  1206. if db is None:
  1207. self._err("close", "The database '%s' does not exist" % name)
  1208. if db.isDirty():
  1209. if not flunk:
  1210. self._err("close", "The database '%s' contains "
  1211. "uncommitted changes" % name)
  1212. db.flunkDirty()
  1213. if len(self.__dbs) > 1:
  1214. self.__dbs.pop(name)
  1215. if self.__selDbName == name:
  1216. self.__selDbName = "main"
  1217. else:
  1218. raise self.Quit()
  1219. @completion
  1220. def complete_close(self, text, line, begidx, endidx):
  1221. if text == "!":
  1222. return [ text + " " ]
  1223. opts = PWManOpts.parse(line, (), ignoreFirst=True, softFail=True)
  1224. if opts.error:
  1225. return []
  1226. paramIdx = opts.getComplParamIdx(text)
  1227. if paramIdx == 0 or (paramIdx == 1 and opts.getParam(0) == "!"):
  1228. # Database name
  1229. return self.__getDatabaseCompletions(text)
  1230. return []
  1231. __find_opts = ("-c", "-t", "-u", "-p", "-b", "-a", "-A", "-r")
  1232. def do_find(self, params):
  1233. """--- Search the database ---
  1234. Command: find [OPTS] [IN_CATEGORY] PATTERN
  1235. Searches the database for patterns. If 'IN_CATEGORY' is given, only search
  1236. in the specified category.
  1237. PATTERN may either use SQL LIKE wildcards (without -r)
  1238. or Python Regular Expression special characters (with -r).
  1239. OPTS may be one or multiple of:
  1240. -c Match 'category' (only if no IN_CATEGORY parameter)
  1241. -t Match 'title' (*)
  1242. -u Match 'user' (*)
  1243. -p Match 'password' (*)
  1244. -b Match 'bulk' (*)
  1245. -a Match 'attribute data' (*)
  1246. -A Match 'attribute name'
  1247. -r Use Python Regular Expression matching
  1248. (*) = These OPTS are enabled by default, if and only if
  1249. none of them are specified by the user.
  1250. Aliases: f
  1251. """
  1252. opts = PWManOpts.parse(params, self.__find_opts)
  1253. mCategory = "-c" in opts
  1254. mTitle = "-t" in opts
  1255. mUser = "-u" in opts
  1256. mPw = "-p" in opts
  1257. mBulk = "-b" in opts
  1258. mAttrData = "-a" in opts
  1259. mAttrName = "-A" in opts
  1260. regexp = "-r" in opts
  1261. if not any( (mTitle, mUser, mPw, mBulk, mAttrData) ):
  1262. mTitle, mUser, mPw, mBulk, mAttrData = (True,) * 5
  1263. if opts.nrParams < 1 or opts.nrParams > 2:
  1264. self._err("find", "Invalid parameters.")
  1265. inCategory = opts.getParam(0) if opts.nrParams > 1 else None
  1266. pattern = opts.getParam(1) if opts.nrParams > 1 else opts.getParam(0)
  1267. if inCategory and mCategory:
  1268. self._err("find", "-c and [IN_CATEGORY] cannot be used at the same time.")
  1269. entries = self.__db.findEntries(pattern=pattern,
  1270. useRegexp=regexp,
  1271. inCategory=inCategory,
  1272. matchCategory=mCategory,
  1273. matchTitle=mTitle,
  1274. matchUser=mUser,
  1275. matchPw=mPw,
  1276. matchBulk=mBulk,
  1277. matchAttrName=mAttrName,
  1278. matchAttrData=mAttrData)
  1279. if not entries:
  1280. self._err("find", "'%s' not found" % pattern)
  1281. for entry in entries:
  1282. self._info(None, self.__db.dumpEntry(entry))
  1283. do_f = do_find
  1284. @completion
  1285. def complete_find(self, text, line, begidx, endidx):
  1286. if text == "-":
  1287. return PWManOpts.rawOptTemplates(self.__find_opts)
  1288. if len(text) == 2 and text.startswith("-"):
  1289. return [ text + " " ]
  1290. opts = PWManOpts.parse(line, self.__find_opts, ignoreFirst=True, softFail=True)
  1291. if opts.error:
  1292. return []
  1293. paramIdx = opts.getComplParamIdx(text)
  1294. if paramIdx == 0:
  1295. # category
  1296. return self.__getCategoryCompletions(text)
  1297. return []
  1298. complete_f = complete_find
  1299. def do_totp(self, params):
  1300. """--- Generate a TOTP token ---
  1301. Command: totp [CATEGORY TITLE] OR [TITLE]
  1302. Generates a token using the Time-Based One-Time Password Algorithm.
  1303. Aliases: t
  1304. """
  1305. first, second = PWManOpts.parseParams(params, 0, 2)
  1306. if not first:
  1307. self._err("totp", "First parameter is required.")
  1308. if second:
  1309. category, title = first, second
  1310. else:
  1311. entries = self.__db.findEntries(first, matchTitle=True)
  1312. if not entries:
  1313. self._err("totp", "Entry title not found.")
  1314. return
  1315. elif len(entries) == 1:
  1316. category = entries[0].category
  1317. title = entries[0].title
  1318. else:
  1319. self._err("totp", "Entry title ambiguous.")
  1320. return
  1321. entry = self.__db.getEntry(category, title)
  1322. if not entry:
  1323. self._err("totp", "'%s/%s' not found" % (category, title))
  1324. entryTotp = self.__db.getEntryTotp(entry)
  1325. if not entryTotp:
  1326. self._err("totp", "'%s/%s' does not have "
  1327. "TOTP key information" % (category, title))
  1328. try:
  1329. token = totp(key=entryTotp.key,
  1330. nrDigits=entryTotp.digits,
  1331. hmacHash=entryTotp.hmacHash)
  1332. except OtpError as e:
  1333. self._err("totp", "Failed to generate TOTP: %s" % str(e))
  1334. self._info(None, "%s" % token)
  1335. do_t = do_totp
  1336. complete_totp = __complete_category_title
  1337. complete_t = complete_totp
  1338. def do_edit_totp(self, params):
  1339. """--- Edit TOTP key and parameters ---
  1340. Command: edit_totp category title [KEY] [DIGITS] [HASH]
  1341. Set Time-Based One-Time Password Algorithm key and parameters.
  1342. If KEY is not provided, the TOTP parameters for this entry are deleted.
  1343. DIGITS default to 6, if not provided.
  1344. HASH defaults to SHA1, if not provided.
  1345. Aliases: et
  1346. """
  1347. category, title, key, digits, _hash = PWManOpts.parseParams(params, 0, 5)
  1348. if not category:
  1349. self._err("edit_totp", "Category parameter is required.")
  1350. if not title:
  1351. self._err("edit_totp", "Title parameter is required.")
  1352. entry = self.__db.getEntry(category, title)
  1353. if not entry:
  1354. self._err("edit_totp", "'%s/%s' not found" % (category, title))
  1355. entryTotp = self.__db.getEntryTotp(entry)
  1356. if not entryTotp:
  1357. entryTotp = PWManEntryTOTP(key=None, entry=entry)
  1358. entryTotp.key = key
  1359. if digits:
  1360. try:
  1361. entryTotp.digits = int(digits)
  1362. except ValueError:
  1363. self._err("edit_totp", "Invalid digits parameter.")
  1364. if _hash:
  1365. entryTotp.hmacHash = _hash
  1366. try:
  1367. # Check parameters.
  1368. totp(key=entryTotp.key,
  1369. nrDigits=entryTotp.digits,
  1370. hmacHash=entryTotp.hmacHash)
  1371. except OtpError as e:
  1372. self._err("edit_totp", "TOTP error: %s" % str(e))
  1373. self.__db.setEntryTotp(entryTotp)
  1374. do_et = do_edit_totp
  1375. @completion
  1376. def complete_edit_totp(self, text, line, begidx, endidx):
  1377. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  1378. if paramIdx in (0, 1):
  1379. return self.__complete_category_title(text, line, begidx, endidx)
  1380. category, title = PWManOpts.parseComplParams(line, 0, 2)
  1381. if category and title:
  1382. entry = self.__db.getEntry(category, title)
  1383. if entry:
  1384. entryTotp = self.__db.getEntryTotp(entry)
  1385. if entryTotp:
  1386. if paramIdx == 2: # key
  1387. return [ escapeCmd(entryTotp.key) + " " ]
  1388. elif paramIdx == 3: # digits
  1389. return [ escapeCmd(str(entryTotp.digits)) + " " ]
  1390. elif paramIdx == 4: # hash
  1391. return [ escapeCmd(entryTotp.hmacHash) + " " ]
  1392. return []
  1393. complete_et = complete_edit_totp
  1394. def do_edit_attr(self, params):
  1395. """--- Edit an entry attribute ---
  1396. Command: edit_attr category title NAME [DATA]
  1397. Edit or delete an entry attribute.
  1398. Aliases: ea
  1399. """
  1400. category, title, name, data = PWManOpts.parseParams(params, 0, 4)
  1401. if not category:
  1402. self._err("edit_attr", "Category parameter is required.")
  1403. if not title:
  1404. self._err("edit_attr", "Title parameter is required.")
  1405. entry = self.__db.getEntry(category, title)
  1406. if not entry:
  1407. self._err("edit_attr", "'%s/%s' not found" % (category, title))
  1408. entryAttr = self.__db.getEntryAttr(entry, name)
  1409. if not entryAttr:
  1410. entryAttr = PWManEntryAttr(name=name, entry=entry)
  1411. entryAttr.data = data
  1412. self.__db.setEntryAttr(entryAttr)
  1413. do_ea = do_edit_attr
  1414. @completion
  1415. def complete_edit_attr(self, text, line, begidx, endidx):
  1416. paramIdx = PWManOpts.calcParamIndex(line, endidx)
  1417. if paramIdx in (0, 1):
  1418. return self.__complete_category_title(text, line, begidx, endidx)
  1419. category, title, name = PWManOpts.parseComplParams(line, 0, 3)
  1420. return self.__getEntryAttrCompletions(category, title, name,
  1421. doName=(paramIdx == 2),
  1422. doData=(paramIdx == 3),
  1423. text=text)
  1424. complete_ea = complete_edit_attr
  1425. __diff_opts = ("-u", "-c", "-n")
  1426. def do_diff(self, params):
  1427. """--- Diff the current database to another database ---
  1428. Command: diff [OPTS] [DATABASE_FILE]
  1429. If no DATABASE_FILE is provided: Diffs the latest changes in the
  1430. currently open database to the committed changes in the current database.
  1431. This can be used to review changes before commit.
  1432. If DATABASE_FILE is provided: Diffs the latest changes in the
  1433. currently opened database to the contents of DATABASE_FILE.
  1434. OPTS may be one of:
  1435. -u Generate a unified diff (default if no OPT is given).
  1436. -c Generate a context diff
  1437. -n Generate an ndiff
  1438. Aliases: None
  1439. """
  1440. opts = PWManOpts.parse(params, self.__diff_opts)
  1441. if opts.nrParams > 1:
  1442. self._err("diff", "Too many arguments.")
  1443. optUnified = "-u" in opts
  1444. optContext = "-c" in opts
  1445. optNdiff = "-n" in opts
  1446. numFmtOpts = int(optUnified) + int(optContext) + int(optNdiff)
  1447. if not 0 <= numFmtOpts <= 1:
  1448. self._err("diff", "Multiple format OPTions. "
  1449. "Only one is allowed.")
  1450. if numFmtOpts == 0:
  1451. optUnified = True
  1452. dbFile = opts.getParam(0)
  1453. try:
  1454. if dbFile:
  1455. path = pathlib.Path(dbFile)
  1456. if not path.exists():
  1457. self._err("diff", "'%s' does not exist." % path)
  1458. passphrase = readPassphrase(
  1459. "Master passphrase of '%s'" % path,
  1460. verify=False)
  1461. if passphrase is None:
  1462. self._err("diff", "Could not get passphrase.")
  1463. oldDb = PWManDatabase(filename=path,
  1464. passphrase=passphrase,
  1465. readOnly=True)
  1466. else:
  1467. oldDb = self.__db.getOnDiskDb()
  1468. diff = PWManDatabaseDiff(db=self.__db, oldDb=oldDb)
  1469. if optUnified:
  1470. diffText = diff.getUnifiedDiff()
  1471. elif optContext:
  1472. diffText = diff.getContextDiff()
  1473. elif optNdiff:
  1474. diffText = diff.getNdiffDiff()
  1475. else:
  1476. assert(0)
  1477. self._info(None, diffText)
  1478. except PWManError as e:
  1479. self._err("diff", "Failed: %s" % str(e))
  1480. @completion
  1481. def complete_diff(self, text, line, begidx, endidx):
  1482. if text == "-":
  1483. return PWManOpts.rawOptTemplates(self.__diff_opts)
  1484. if len(text) == 2 and text.startswith("-"):
  1485. return [ text + " " ]
  1486. opts = PWManOpts.parse(line, self.__diff_opts, ignoreFirst=True, softFail=True)
  1487. if opts.error:
  1488. return []
  1489. paramIdx = opts.getComplParamIdx(text)
  1490. if paramIdx == 0:
  1491. # database file path
  1492. return self.__getPathCompletions(text)
  1493. return []
  1494. def __mayQuit(self):
  1495. if self.__db.isDirty():
  1496. self._warn(None,
  1497. "Warning: Uncommitted changes. Operation not performed.\n"
  1498. "Use command 'commit' to write the changes to the database.\n"
  1499. "Use command 'quit!' to quit without saving.")
  1500. return False
  1501. return True
  1502. def flunkDirty(self):
  1503. self.__db.flunkDirty()
  1504. def interactive(self):
  1505. self.__isInteractive = True
  1506. try:
  1507. while True:
  1508. try:
  1509. self.cmdloop()
  1510. break
  1511. except self.Quit as e:
  1512. if self.__mayQuit():
  1513. self.do_cls("")
  1514. break
  1515. except EscapeError as e:
  1516. self._warn(None, str(e))
  1517. except self.CommandError as e:
  1518. print(str(e), file=sys.stderr)
  1519. except (KeyboardInterrupt, EOFError) as e:
  1520. print("")
  1521. except CSQLError as e:
  1522. self._warn(None, "SQL error: %s" % str(e))
  1523. finally:
  1524. self.__isInteractive = False
  1525. def runOneCommand(self, command):
  1526. self.__isInteractive = False
  1527. try:
  1528. self.onecmd(command)
  1529. except self.Quit as e:
  1530. raise PWManError("Quit command executed in non-interactive mode.")
  1531. except (EscapeError, self.CommandError) as e:
  1532. raise PWManError(str(e))
  1533. except (KeyboardInterrupt, EOFError) as e:
  1534. raise PWManError("Interrupted.")
  1535. except CSQLError as e:
  1536. raise PWManError("SQL error: %s" % str(e))