ui.py 49 KB

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